IAccountManagementGET Apex Interface
The IAccountManagementGET Apex interface provides extensibility for GET operations through Apex pre- and post-hooks and GraphQL query customization. Implementations can validate incoming requests, modify query structures, and refine the response to support business-specific retrieval rules while maintaining TMF-compliant behavior.
This interface supports the following hooks.
Account Retrieve Operation Use Cases
Use Case Hook(s) Description Example Scenarios Benefit Role-Based Data Filtering applyCustomValidations, handlePostOperationEnforces access rules before execution and filters sensitive fields or records after retrieval. Access can be evaluated using the caller’s role, region, business unit, channel, or request context. - Restrict access to strategic or high-value Accounts - Hide financial, risk, or compliance fields from frontline users - Enforce regional or business-unit access boundaries - Remove sensitive fields from partner-facing responses Enforces data governance and security through both pre-query authorization and post-query response filtering. Custom Field Enrichment handlePostOperationAdds calculated, derived, or externally sourced fields to the response without changing the base TMF632 schema. - Add an Account health score - Flag preferred, strategic, or high-risk Accounts - Include aggregated revenue or usage insights - Attach hierarchy or entitlement information Delivers richer and more contextual Account data without requiring changes to the shared schema. Dynamic Query Customization customiseGraphQLQueryModifies the GraphQL query before execution by adding fields, relationships, filters, or server-side constraints according to the client or request context. - Return a minimal field set to mobile clients - Include hierarchy or entitlement fields for portals - Add organization-specific Account fields - Apply tenant, region, status, or channel filters automatically Supports multiple client requirements through a single API and reduces the need for client-specific endpoints or API versions. Business-Rule Validation applyCustomValidationsEvaluates organization-specific business, eligibility, and access rules before the GraphQL query executes. Invalid requests are rejected early with a clear error response. - Require an active Account before returning related services - Reject requests from unauthorized or unverified channels - Enforce lifecycle-state restrictions - Validate tenant or business-unit ownership Rejects invalid requests early, maintains business consistency, and avoids unnecessary downstream processing. External-ID Resolution transformRequestResolves a business key, legacy identifier, or external-system reference to the canonical Salesforce Account ID and adds it to the shared request context as { id }. - Retrieve an Account by customer number - Resolve an ERP or billing-system identifier - Map a partner-system key to a Salesforce Account ID - Translate a migrated legacy identifier Allows clients to fetch Accounts by meaningful business keys instead of requiring a Salesforce ID.
transformRequest
This hook transforms the request context before processing any operation. It is invoked early in the request lifecycle, allowing implementers to adjust the context for subsequent extensibility hooks.
Hook Method
Map<String, Object> transformRequest(Map<String, Object> context)
Sample Apex Implementation
Resolve an external Account Number to a Billing Account ID.
1 global class AccountMgmtTransformRequestExt implements comms_apex_ext.IAccountManagementGET {
2 global static Map<String, Object> transformRequest(Map<String, Object> context) {
3 if (context == null) {
4 return context;
5 }
6
7
8 Object idObj = context.get('id');
9 if (idObj == null || !(idObj instanceof String)) {
10 return context;
11 }
12 String idValue = (String) idObj;
13
14
15 // Already a Salesforce id (15 or 18 chars) — leave it alone.
16 if (idValue.length() == 15 || idValue.length() == 18) {
17 return context;
18 }
19
20
21 // Otherwise treat it as an external billing-account number and resolve it.
22 List<BillingAccount> accounts = [
23 SELECT Id
24 FROM BillingAccount
25 WHERE ExtlBillAccountNumber__c = :idValue
26 LIMIT 1
27 ];
28 if (!accounts.isEmpty()) {
29 context.put('id', accounts[0].Id);
30 }
31
32
33 return context;
34 }
35
36 // ... other hooks return null / empty map ...
37 }
applyCustomValidations
This hook validates custom business logic before retrieving customer record. If validation fails, it rejects the request and returns an error response.
The handler processes return values as follows.
Success:
Return a map with validationStatus set to “pass” (case-insensitive).
The API request continues normally.
Other fields in the map are logged but not used.
Failure:
Return a map with validationStatus set to “fail” (case-insensitive).
The API request is terminated immediately.
Raise a ValidationException containing
message: value from validationMessage key (or “Validation failed” by default)
details: value from validationDetails key (optional)
The client receives an HTTP 400 error response.
Hook Method
Map<String, Object> applyCustomValidations(Map<String, Object> context)
Sample Apex Implementation
Restrict billing account visibility for customer service representatives.
1 global class AccountMgmtAccessValidationExt implements comms_apex_ext.IAccountManagementGET {
2
3 global static Map<String, Object> applyCustomValidations(Map<String, Object> context) {
4 String billingAccountId = (String) context.get('id');
5 String userId = UserInfo.getUserId();
6 Map<String, Object> validationResult = new Map<String, Object>();
7 try {
8 User currentUser = [SELECT Id, Profile.Name FROM User WHERE Id = :userId LIMIT 1];
9
10
11 if ('Customer Service Rep'.equals(currentUser.Profile.Name) && billingAccountId != null) {
12 BillingAccount ba = [
13 SELECT Id, PrimaryBillAddrCountry
14 FROM BillingAccount
15 WHERE Id = :billingAccountId
16 LIMIT 1
17 ];
18
19 if ('US'.equals(ba.PrimaryBillAddrCountry)) {
20 validationResult.put('validationStatus', 'PASS');
21 validationResult.put('validationMessage', 'User has access to this billing account');
22 } else {
23 validationResult.put('validationStatus', 'FAIL');
24 validationResult.put('validationMessage', 'User does not have access to billing accounts in this region');
25 validationResult.put('validationDetails', new Map<String, Object>{
26 'denialReason' => 'REGION_RESTRICTION',
27 'accountRegion' => ba.PrimaryBillAddrCountry
28 });
29 }
30 } else {
31 validationResult.put('validationStatus', 'PASS');
32 validationResult.put('validationMessage', 'User profile has unrestricted access');
33 }
34 } catch (Exception e) {
35 validationResult.put('validationStatus', 'FAIL');
36 validationResult.put('validationMessage', 'Validation error: ' + e.getMessage());
37 validationResult.put('validationDetails', new Map<String, Object>{
38 'errorType' => e.getTypeName()
39 });
40 }
41
42
43 return validationResult;
44 }
45 // ... other hooks return null / empty map ...
46 }
customiseGraphQLQuery
This hook modifies the GraphQL query before execution to add fields, filters, or transformations.
Hook Method
Map<String, Object> customiseGraphQLQuery(Map<String, Object> graphQLAsMap, Map<String, Object> context)
Sample Apex Implementation
Add custom fields, filter and ordering.
1 global class AccountMgmtQueryCustomizeExt implements comms_apex_ext.IAccountManagementGET {
2
3 global static Map<String, Object> customiseGraphQLQuery(
4 Map<String, Object> querySpec,
5 Map<String, Object> context
6 ) {
7 return new Map<String, Object>{
8 'nodes' => new List<Object>{
9 new Map<String, Object>{
10 'path' => 'billingAccount',
11 'addFields' => new List<String>{ 'CustomField__c', 'ExtlBillFrequency' },
12 'addFilters' => new Map<String, Object>{
13 'clearExistingFilters' => false,
14 'filterGraphQlSnippet' => '{ Status: { eq: \"Active\" } }'
15 },
16 'orderBy' => new Map<String, Object>{
17 'clearExistingOrderBy' => true,
18 'orderGraphQlSnippet' => '{ Name: { order: ASC } }'
19 }
20 }
21 }
22 };
23 }
24 // ... other hooks return null / empty map ...
25 }
handlePostOperation
This hook post-processes and transforms the API response after data retrieval.
Hook Method
Map<String, Object> handlePostOperation(Map<String, Object> graphQLQueryResultAsMap, Map<String, Object> constructedTMFResponse, Map<String, Object> context)
Sample Apex Implementation
Enrich responses with calculated fields (single and list-aware).
1 global class AccountMgmtResponseEnrichExt implements comms_apex_ext.IAccountManagementGET {
2 global static Map<String, Object> handlePostOperation(
3 Map<String, Object> graphQLResult,
4 Map<String, Object> tmfResponse,
5 Map<String, Object> context
6 ) {
7 if (tmfResponse == null) {
8 return null;
9 }
10
11 if (tmfResponse.containsKey('recordList')) {
12 // LIST response
13 List<Object> recordList = (List<Object>) tmfResponse.get('recordList');
14 if (recordList == null || recordList.isEmpty()) {
15 return null;
16 }
17 List<Object> updatedRecordList = new List<Object>();
18 for (Object recordObj : recordList) {
19 Map<String, Object> record = new Map<String, Object>((Map<String, Object>) recordObj);
20 enrich(record);
21 updatedRecordList.add(record);
22 }
23 Map<String, Object> updatedPayload = new Map<String, Object>(tmfResponse);
24 updatedPayload.put('recordList', updatedRecordList);
25 return new Map<String, Object>{
26 'payloadOverwritten' => true,
27 'updatedPayload' => updatedPayload
28 };
29 } else {
30 // SINGLE response
31 Map<String, Object> updatedPayload = new Map<String, Object>(tmfResponse);
32 enrich(updatedPayload);
33 return new Map<String, Object>{
34 'payloadOverwritten' => true,
35 'updatedPayload' => updatedPayload
36 };
37 }
38 }
39 private static void enrich(Map<String, Object> record) {
40 record.put('paymentStatus', 'Paid');
41 record.put('retrievedBy', UserInfo.getName());
42 record.put('lastRetrievedAt',
43 DateTime.now().format('yyyy-MM-dd\'T\'HH:mm:ss\'Z\''));
44 }
45 // ... other hooks return null / empty map ...
46 }
Full Implementation Example
Here’s a complete sample Apex implementation using all supported hooks.
1 /**
2 * Complete TMF666 Account Management GET API Extension
3 * Demonstrates transformRequest, applyCustomValidations, customiseGraphQLQuery, and handlePostOperation.
4 */
5 global class AccountManagementGETExtension implements comms_apex_ext.IAccountManagementGET {
6
7
8 /** HOOK 1: Resolve external id → Salesforce id (pass-through here) */
9 global static Map<String, Object> transformRequest(Map<String, Object> context) {
10 return context;
11 }
12
13
14 /** HOOK 2: Not applicable for Account Management */
15 global static Map<String, Boolean> configureDefaultValidations(
16 Map<String, Boolean> defaultValidationConfiguration,
17 Map<String, Object> context
18 ) {
19 return new Map<String, Boolean>();
20 }
21
22
23 /** HOOK 3: Role-based access control */
24 global static Map<String, Object> applyCustomValidations(Map<String, Object> context) {
25 String billingAccountId = (String) context.get('id');
26 String userId = UserInfo.getUserId();
27 Map<String, Object> result = new Map<String, Object>();
28
29
30 try {
31 User currentUser = [SELECT Id, Profile.Name FROM User WHERE Id = :userId LIMIT 1];
32 if ('Customer Service Rep'.equals(currentUser.Profile.Name) && billingAccountId != null) {
33 BillingAccount ba = [
34 SELECT Id, PrimaryBillAddrCountry FROM BillingAccount
35 WHERE Id = :billingAccountId LIMIT 1
36 ];
37 if ('US'.equals(ba.PrimaryBillAddrCountry)) {
38 result.put('validationStatus', 'PASS');
39 } else {
40 result.put('validationStatus', 'FAIL');
41 result.put('validationMessage', 'Access denied - region not allowed');
42 result.put('validationDetails', new Map<String, Object>{ 'denialReason' => 'REGION_RESTRICTION' });
43 }
44 } else {
45 result.put('validationStatus', 'PASS');
46 }
47 } catch (Exception e) {
48 result.put('validationStatus', 'FAIL');
49 result.put('validationMessage', 'Validation error: ' + e.getMessage());
50 }
51 return result;
52 }
53
54
55 /** HOOK 4: Add fields + filter */
56 global static Map<String, Object> customiseGraphQLQuery(
57 Map<String, Object> querySpec,
58 Map<String, Object> context
59 ) {
60 return new Map<String, Object>{
61 'nodes' => new List<Object>{
62 new Map<String, Object>{
63 'path' => 'billingAccount',
64 'addFields' => new List<String>{ 'CustomField__c' },
65 'addFilters' => new Map<String, Object>{
66 'clearExistingFilters' => false,
67 'filterGraphQlSnippet' => '{ Status: { eq: \"Active\" } }'
68 }
69 }
70 }
71 };
72 }
73
74
75 /** HOOK 5: Enrich the response */
76 global static Map<String, Object> handlePostOperation(
77 Map<String, Object> graphQLResult,
78 Map<String, Object> tmfResponse,
79 Map<String, Object> context
80 ) {
81 if (tmfResponse == null) {
82 return null;
83 }
84 if (tmfResponse.containsKey('recordList')) {
85 List<Object> recordList = (List<Object>) tmfResponse.get('recordList');
86 if (recordList == null || recordList.isEmpty()) {
87 return null;
88 }
89 List<Object> updated = new List<Object>();
90 for (Object recordObj : recordList) {
91 Map<String, Object> record = new Map<String, Object>((Map<String, Object>) recordObj);
92 record.put('source', 'apex_extensibility');
93 updated.add(record);
94 }
95 Map<String, Object> updatedPayload = new Map<String, Object>(tmfResponse);
96 updatedPayload.put('recordList', updated);
97 return new Map<String, Object>{ 'payloadOverwritten' => true, 'updatedPayload' => updatedPayload };
98 } else {
99 tmfResponse.put('source', 'apex_extensibility');
100 return new Map<String, Object>{ 'payloadOverwritten' => true, 'updatedPayload' => tmfResponse };
101 }
102 }
103 }
Response Structure
Single Billing Account Response
1 {
2 "id": "15ixx0000004C92AAE",
3 "href": "/connect/comms/accountmanagement/v5/billingaccount/15ixx0000004C92AAE",
4 "name": "UpdatedAcct001",
5 "state": "Active",
6 "accountType": "Residential",
7 "lastUpdate": "2026-04-09T05:25:10.000Z",
8 "billStructure": {
9 "format": { "name": "Detailed", "@type": "BillFormat" },
10 "presentationMedia": [
11 { "name": "Email", "@type": "BillPresentationMedia" }
12 ],
13 "cycleSpecification": { "frequency": "Monthly", "@type": "BillingCycleSpecification" },
14 "@type": "BillStructure"
15 },
16 "taxExemption": [
17 {
18 "certificateNumber": "TAX-EXEMPT-001",
19 "validFor": {
20 "startDateTime": "2025-01-01T00:00:00.000Z",
21 "endDateTime": "2026-12-31T00:00:00.000Z"
22 },
23 "reason": "Government",
24 "@type": "TaxExemption"
25 }
26 ],
27 "relatedParty": [
28 {
29 "role": "Financially Responsible Party",
30 "partyOrPartyRole": {
31 "id": "001xx000003HJdrAAG",
32 "href": "/services/data/v68.0/sobjects/Account/001xx000003HJdrAAG",
33 "name": "Acme Corp",
34 "@type": "PartyRef",
35 "@referredType": "Organization"
36 },
37 "@type": "RelatedPartyRefOrRelatedPartyRoleRef"
38 }
39 ],
40 "contact": [
41 {
42 "id": "003xx000004WpZwAAK",
43 "contactName": "John Doe",
44 "contactType": "Bill To",
45 "validFor": { "endDateTime": "2099-12-31T00:00:00.000Z" },
46 "contactMedium": [
47 {
48 "id": "15ixx0000004C92AAE-cm-1",
49 "contactType": "billing email",
50 "preferred": true,
51 "emailAddress": "billing@acme.com",
52 "@type": "EmailContactMedium"
53 },
54 {
55 "id": "15ixx0000004C92AAE-cm-2",
56 "contactType": "billing address",
57 "preferred": true,
58 "city": "San Francisco",
59 "country": "US",
60 "postCode": "94105",
61 "stateOrProvince": "CA",
62 "street1": "123 Main St",
63 "@type": "GeographicAddressContactMedium"
64 }
65 ],
66 "@type": "Contact"
67 }
68 ],
69 "@type": "BillingAccount"
List Response (GraphQL)
Default (without pageInfo) — a bare JSON array of Individual objects:
1 [
2 {
3 "@type": "BillingAccount",
4 "lastUpdate": "2026-04-09T10:06:37.000Z",
5 "name": "Home Account",
6 "id": "15ixx0000004CAeAAM",
7 "href": "/connect/comms/accountmanagement/v5/billingaccount/15ixx0000004CAeAAM",
8 "state": "Inactive",
9 "relatedParty": [
10 {
11 "role": "Financially Responsible Party",
12 "partyOrPartyRole": {
13 "id": "001xx000003HJdrAAG",
14 "href": "/services/data/v68.0/sobjects/Account/001xx000003HJdrAAG",
15 "name": "Acme Corp",
16 "@type": "PartyRef",
17 "@referredType": "Organization"
18 },
19 "@type": "RelatedPartyRefOrRelatedPartyRoleRef"
20 }
21 ]
22 },
23 {
24 "@type": "BillingAccount",
25 "lastUpdate": "2026-04-14T18:43:10.000Z",
26 "name": "Test Any Role BA",
27 "id": "15ixx0000004FjcAAE",
28 "href": "/connect/comms/accountmanagement/v5/billingaccount/15ixx0000004FjcAAE",
29 "state": "Inactive",
30 "relatedParty": [
31 {
32 "role": "Financially Responsible Party",
33 "partyOrPartyRole": {
34 "id": "001xx000003HP8BAAW",
35 "href": "/services/data/v68.0/sobjects/Account/001xx000003HP8BAAW",
36 "name": "Test Acme Corp",
37 "@type": "PartyRef",
38 "@referredType": "Organization"
39 },
40 "@type": "RelatedPartyRefOrRelatedPartyRoleRef"
41 }
42 ]
43 }
44 ]
GraphQL Query Transformations
Query transformations provide the ability to enhance GraphQL queries by adding fields, applying filters, or modifying the sort order, while preserving the original query definition.
Query Enhancements: Fields and Filters
Original Query (GraphQL):
1 query {
2 myAccounts: accounts {
3 id
4 name
5 }
6 }
Transformation Instructions (JSON):
1 {
2 "nodes": [
3 {
4 "path": "myAccounts",
5 "addFields": ["email", "phone"],
6 "addFilters": {
7 "clearExistingFilters": true,
8 "filterGraphQlSnippet": "{ status: { eq: \"ACTIVE\" } }"
9 }
10 }
11 ]
12 }
Result (GraphQL):
1 query {
2 myAccounts: accounts(where: { status: { eq: "ACTIVE" } }) {
3 id
4 name
5 email
6 phone
7 }
8 }
Query Enhancements: Fields, Filters, and Ordering
Original Query (GraphQL):
1 query {
2 myAccounts: accounts {
3 id
4 name
5 }
6 }
Transformation Instructions (JSON):
1 {
2 "nodes": [
3 {
4 "path": "myAccounts",
5 "addFields": ["revenue", "industry"],
6 "addFilters": {
7 "clearExistingFilters": true,
8 "filterGraphQlSnippet": "{
9 and: [
10 { status: { eq: \"ACTIVE\" } },
11 { revenue: { gte: 1000000 } }
12 ]
13 }"
14 },
15 "orderBy": {
16 "clearExistingOrderBy": true,
17 "orderGraphQlSnippet": "{ revenue: { order: DESC } }"
18 }
19 }
20 ]
21 }
Result (GraphQL):
1 query {
2 myAccounts: accounts(
3 where: {
4 and: [
5 { status: { eq: "ACTIVE" } },
6 { revenue: { gte: 1000000 } }
7 ]
8 },
9 orderBy: { revenue: { order: DESC } }
10 ) {
11 id
12 name
13 revenue
14 industry
15 }
16 }
Query Enhancements: Multiple Query Paths with Different Transformations
Original Query (GraphQL):
1 query {
2 myAccounts: accounts {
3 id
4 name
5 myContacts: contacts {
6 id
7 name
8 }
9 }
10 }
Transformation Instructions (JSON):
1 {
2 "nodes": [
3 {
4 "path": "myAccounts",
5 "addFields": ["email"],
6 "addFilters": {
7 "clearExistingFilters": true,
8 "filterGraphQlSnippet": "{ status: { eq: \"ACTIVE\" } }"
9 }
10 },
11 {
12 "path": "myAccounts.myContacts",
13 "addFields": ["email", "phone"],
14 "orderBy": {
15 "clearExistingOrderBy": true,
16 "orderGraphQlSnippet": "{ name: { order: ASC } }"
17 }
18 }
19 ]
20 }
Result (GraphQL):
1 query {
2 myAccounts: accounts(where: { status: { eq: "ACTIVE" } }) {
3 id
4 name
5 email
6 myContacts: contacts(orderBy: { name: { order: ASC } }) {
7 id
8 name
9 email
10 phone
11 }
12 }
13 }
Query Enhancements: Fields and Snippets
Original Query (GraphQL):
1 query {
2 myAccounts: accounts {
3 id
4 name
5 }
6 }
Transformation Instructions (JSON):
1 {
2 "nodes": [
3 {
4 "path": "myAccounts",
5 "addFields": ["email"]
6 },
7 {
8 "path": "metrics",
9 "insertGraphQlSnippet": "{
10 metrics: systemMetrics {
11 totalCount
12 activeCount
13 lastSyncTime
14 }
15 }"
16 }
17 ]
18 }
Result (GraphQL):
1 query {
2 myAccounts: accounts {
3 id
4 name
5 email
6 }
7 metrics: systemMetrics {
8 totalCount
9 activeCount
10 lastSyncTime
11 }
12 }