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, handlePostOperation Enforces 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 <br>- Hide financial, risk, or compliance fields from frontline users <br>- Enforce regional or business-unit access boundaries <br>- 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 handlePostOperation Adds calculated, derived, or externally sourced fields to the response without changing the base TMF632 schema. - Add an Account health score <br>- Flag preferred, strategic, or high-risk Accounts <br>- Include aggregated revenue or usage insights <br>- Attach hierarchy or entitlement information Delivers richer and more contextual Account data without requiring changes to the shared schema.
Dynamic Query Customization customiseGraphQLQuery Modifies 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 <br>- Include hierarchy or entitlement fields for portals <br>- Add organization-specific Account fields <br>- 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 applyCustomValidations Evaluates 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 <br>- Reject requests from unauthorized or unverified channels <br>- Enforce lifecycle-state restrictions<br>- Validate tenant or business-unit ownership Rejects invalid requests early, maintains business consistency, and avoids unnecessary downstream processing.
External-ID Resolution transformRequest Resolves 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 <br>- Resolve an ERP or billing-system identifier <br>- Map a partner-system key to a Salesforce Account ID<br>- 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.

global class AccountMgmtTransformRequestExt implements comms_apex_ext.IAccountManagementGET {
    global static Map<String, Object> transformRequest(Map<String, Object> context) {
        if (context == null) {
            return context;
        }


        Object idObj = context.get('id');
        if (idObj == null || !(idObj instanceof String)) {
            return context;
        }
        String idValue = (String) idObj;


        // Already a Salesforce id (15 or 18 chars) — leave it alone.
        if (idValue.length() == 15 || idValue.length() == 18) {
            return context;
        }


        // Otherwise treat it as an external billing-account number and resolve it.
        List<BillingAccount> accounts = [
            SELECT Id
            FROM BillingAccount
            WHERE ExtlBillAccountNumber__c = :idValue
            LIMIT 1
        ];
        if (!accounts.isEmpty()) {
            context.put('id', accounts[0].Id);
        }


        return context;
    }

    // ... other hooks return null / empty map ...
}

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.

global class AccountMgmtAccessValidationExt implements comms_apex_ext.IAccountManagementGET {

    global static Map<String, Object> applyCustomValidations(Map<String, Object> context) {
        String billingAccountId = (String) context.get('id');
        String userId = UserInfo.getUserId();
        Map<String, Object> validationResult = new Map<String, Object>();
        try {
            User currentUser = [SELECT Id, Profile.Name FROM User WHERE Id = :userId LIMIT 1];


            if ('Customer Service Rep'.equals(currentUser.Profile.Name) && billingAccountId != null) {
                BillingAccount ba = [
                    SELECT Id, PrimaryBillAddrCountry
                    FROM BillingAccount
                    WHERE Id = :billingAccountId
                    LIMIT 1
                ];

                if ('US'.equals(ba.PrimaryBillAddrCountry)) {
                    validationResult.put('validationStatus', 'PASS');
                    validationResult.put('validationMessage', 'User has access to this billing account');
                } else {
                    validationResult.put('validationStatus', 'FAIL');
                    validationResult.put('validationMessage', 'User does not have access to billing accounts in this region');
                    validationResult.put('validationDetails', new Map<String, Object>{
                        'denialReason'  => 'REGION_RESTRICTION',
                        'accountRegion' => ba.PrimaryBillAddrCountry
                    });
                }
            } else {
                validationResult.put('validationStatus', 'PASS');
                validationResult.put('validationMessage', 'User profile has unrestricted access');
            }
        } catch (Exception e) {
            validationResult.put('validationStatus', 'FAIL');
            validationResult.put('validationMessage', 'Validation error: ' + e.getMessage());
            validationResult.put('validationDetails', new Map<String, Object>{
                'errorType' => e.getTypeName()
            });
        }


        return validationResult;
    }
    // ... other hooks return null / empty map ...
}

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.

global class AccountMgmtQueryCustomizeExt implements comms_apex_ext.IAccountManagementGET {

    global static Map<String, Object> customiseGraphQLQuery(
        Map<String, Object> querySpec,
        Map<String, Object> context
    ) {
        return new Map<String, Object>{
            'nodes' => new List<Object>{
                new Map<String, Object>{
                    'path'      => 'billingAccount',
                    'addFields' => new List<String>{ 'CustomField__c', 'ExtlBillFrequency' },
                    'addFilters' => new Map<String, Object>{
                        'clearExistingFilters' => false,
                        'filterGraphQlSnippet' => '{ Status: { eq: \"Active\" } }'
                    },
                    'orderBy' => new Map<String, Object>{
                        'clearExistingOrderBy' => true,
                        'orderGraphQlSnippet'  => '{ Name: { order: ASC } }'
                    }
                }
            }
        };
    }
    // ... other hooks return null / empty map ...
}

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).

global class AccountMgmtResponseEnrichExt implements comms_apex_ext.IAccountManagementGET {
    global static Map<String, Object> handlePostOperation(
        Map<String, Object> graphQLResult,
        Map<String, Object> tmfResponse,
        Map<String, Object> context
    ) {
        if (tmfResponse == null) {
            return null;
        }

        if (tmfResponse.containsKey('recordList')) {
            // LIST response
            List<Object> recordList = (List<Object>) tmfResponse.get('recordList');
            if (recordList == null || recordList.isEmpty()) {
                return null;
            }
            List<Object> updatedRecordList = new List<Object>();
            for (Object recordObj : recordList) {
                Map<String, Object> record = new Map<String, Object>((Map<String, Object>) recordObj);
                enrich(record);
                updatedRecordList.add(record);
            }
            Map<String, Object> updatedPayload = new Map<String, Object>(tmfResponse);
            updatedPayload.put('recordList', updatedRecordList);
            return new Map<String, Object>{
                'payloadOverwritten' => true,
                'updatedPayload'     => updatedPayload
            };
        } else {
            // SINGLE response
            Map<String, Object> updatedPayload = new Map<String, Object>(tmfResponse);
            enrich(updatedPayload);
            return new Map<String, Object>{
                'payloadOverwritten' => true,
                'updatedPayload'     => updatedPayload
            };
        }
    }
    private static void enrich(Map<String, Object> record) {
        record.put('paymentStatus', 'Paid');
        record.put('retrievedBy', UserInfo.getName());
        record.put('lastRetrievedAt',
            DateTime.now().format('yyyy-MM-dd\'T\'HH:mm:ss\'Z\''));
    }
    // ... other hooks return null / empty map ...
}

Full Implementation Example

Here's a complete sample Apex implementation using all supported hooks.

/**
 * Complete TMF666 Account Management GET API Extension
 * Demonstrates transformRequest, applyCustomValidations, customiseGraphQLQuery, and handlePostOperation.
 */
global class AccountManagementGETExtension implements comms_apex_ext.IAccountManagementGET {


    /** HOOK 1: Resolve external id → Salesforce id (pass-through here) */
    global static Map<String, Object> transformRequest(Map<String, Object> context) {
        return context;
    }


    /** HOOK 2: Not applicable for Account Management */
    global static Map<String, Boolean> configureDefaultValidations(
        Map<String, Boolean> defaultValidationConfiguration,
        Map<String, Object> context
    ) {
        return new Map<String, Boolean>();
    }


    /** HOOK 3: Role-based access control */
    global static Map<String, Object> applyCustomValidations(Map<String, Object> context) {
        String billingAccountId = (String) context.get('id');
        String userId = UserInfo.getUserId();
        Map<String, Object> result = new Map<String, Object>();


        try {
            User currentUser = [SELECT Id, Profile.Name FROM User WHERE Id = :userId LIMIT 1];
            if ('Customer Service Rep'.equals(currentUser.Profile.Name) && billingAccountId != null) {
                BillingAccount ba = [
                    SELECT Id, PrimaryBillAddrCountry FROM BillingAccount
                    WHERE Id = :billingAccountId LIMIT 1
                ];
                if ('US'.equals(ba.PrimaryBillAddrCountry)) {
                    result.put('validationStatus', 'PASS');
                } else {
                    result.put('validationStatus', 'FAIL');
                    result.put('validationMessage', 'Access denied - region not allowed');
                    result.put('validationDetails', new Map<String, Object>{ 'denialReason' => 'REGION_RESTRICTION' });
                }
            } else {
                result.put('validationStatus', 'PASS');
            }
        } catch (Exception e) {
            result.put('validationStatus', 'FAIL');
            result.put('validationMessage', 'Validation error: ' + e.getMessage());
        }
        return result;
    }


    /** HOOK 4: Add fields + filter */
    global static Map<String, Object> customiseGraphQLQuery(
        Map<String, Object> querySpec,
        Map<String, Object> context
    ) {
        return new Map<String, Object>{
            'nodes' => new List<Object>{
                new Map<String, Object>{
                    'path'      => 'billingAccount',
                    'addFields' => new List<String>{ 'CustomField__c' },
                    'addFilters' => new Map<String, Object>{
                        'clearExistingFilters' => false,
                        'filterGraphQlSnippet' => '{ Status: { eq: \"Active\" } }'
                    }
                }
            }
        };
    }


    /** HOOK 5: Enrich the response */
    global static Map<String, Object> handlePostOperation(
        Map<String, Object> graphQLResult,
        Map<String, Object> tmfResponse,
        Map<String, Object> context
    ) {
        if (tmfResponse == null) {
            return null;
        }
        if (tmfResponse.containsKey('recordList')) {
            List<Object> recordList = (List<Object>) tmfResponse.get('recordList');
            if (recordList == null || recordList.isEmpty()) {
                return null;
            }
            List<Object> updated = new List<Object>();
            for (Object recordObj : recordList) {
                Map<String, Object> record = new Map<String, Object>((Map<String, Object>) recordObj);
                record.put('source', 'apex_extensibility');
                updated.add(record);
            }
            Map<String, Object> updatedPayload = new Map<String, Object>(tmfResponse);
            updatedPayload.put('recordList', updated);
            return new Map<String, Object>{ 'payloadOverwritten' => true, 'updatedPayload' => updatedPayload };
        } else {
            tmfResponse.put('source', 'apex_extensibility');
            return new Map<String, Object>{ 'payloadOverwritten' => true, 'updatedPayload' => tmfResponse };
        }
    }
}


Response Structure

Single Billing Account Response

{
  "id": "15ixx0000004C92AAE",
  "href": "/connect/comms/accountmanagement/v5/billingaccount/15ixx0000004C92AAE",
  "name": "UpdatedAcct001",
  "state": "Active",
  "accountType": "Residential",
  "lastUpdate": "2026-04-09T05:25:10.000Z",
  "billStructure": {
    "format": { "name": "Detailed", "@type": "BillFormat" },
    "presentationMedia": [
      { "name": "Email", "@type": "BillPresentationMedia" }
    ],
    "cycleSpecification": { "frequency": "Monthly", "@type": "BillingCycleSpecification" },
    "@type": "BillStructure"
  },
  "taxExemption": [
    {
      "certificateNumber": "TAX-EXEMPT-001",
      "validFor": {
        "startDateTime": "2025-01-01T00:00:00.000Z",
        "endDateTime": "2026-12-31T00:00:00.000Z"
      },
      "reason": "Government",
      "@type": "TaxExemption"
    }
  ],
  "relatedParty": [
    {
      "role": "Financially Responsible Party",
      "partyOrPartyRole": {
        "id": "001xx000003HJdrAAG",
        "href": "/services/data/v68.0/sobjects/Account/001xx000003HJdrAAG",
        "name": "Acme Corp",
        "@type": "PartyRef",
        "@referredType": "Organization"
      },
      "@type": "RelatedPartyRefOrRelatedPartyRoleRef"
    }
  ],
  "contact": [
    {
      "id": "003xx000004WpZwAAK",
      "contactName": "John Doe",
      "contactType": "Bill To",
      "validFor": { "endDateTime": "2099-12-31T00:00:00.000Z" },
      "contactMedium": [
        {
          "id": "15ixx0000004C92AAE-cm-1",
          "contactType": "billing email",
          "preferred": true,
          "emailAddress": "billing@acme.com",
          "@type": "EmailContactMedium"
        },
        {
          "id": "15ixx0000004C92AAE-cm-2",
          "contactType": "billing address",
          "preferred": true,
          "city": "San Francisco",
          "country": "US",
          "postCode": "94105",
          "stateOrProvince": "CA",
          "street1": "123 Main St",
          "@type": "GeographicAddressContactMedium"
        }
      ],
      "@type": "Contact"
    }
  ],
  "@type": "BillingAccount"

List Response (GraphQL)

Default (without pageInfo) — a bare JSON array of Individual objects:

[
  {
    "@type": "BillingAccount",
    "lastUpdate": "2026-04-09T10:06:37.000Z",
    "name": "Home Account",
    "id": "15ixx0000004CAeAAM",
    "href": "/connect/comms/accountmanagement/v5/billingaccount/15ixx0000004CAeAAM",
    "state": "Inactive",
    "relatedParty": [
      {
        "role": "Financially Responsible Party",
        "partyOrPartyRole": {
          "id": "001xx000003HJdrAAG",
          "href": "/services/data/v68.0/sobjects/Account/001xx000003HJdrAAG",
          "name": "Acme Corp",
          "@type": "PartyRef",
          "@referredType": "Organization"
        },
        "@type": "RelatedPartyRefOrRelatedPartyRoleRef"
      }
    ]
  },
  {
    "@type": "BillingAccount",
    "lastUpdate": "2026-04-14T18:43:10.000Z",
    "name": "Test Any Role BA",
    "id": "15ixx0000004FjcAAE",
    "href": "/connect/comms/accountmanagement/v5/billingaccount/15ixx0000004FjcAAE",
    "state": "Inactive",
    "relatedParty": [
      {
        "role": "Financially Responsible Party",
        "partyOrPartyRole": {
          "id": "001xx000003HP8BAAW",
          "href": "/services/data/v68.0/sobjects/Account/001xx000003HP8BAAW",
          "name": "Test Acme Corp",
          "@type": "PartyRef",
          "@referredType": "Organization"
        },
        "@type": "RelatedPartyRefOrRelatedPartyRoleRef"
      }
    ]
  }
]


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):

query {
myAccounts: accounts {
id
name
}
}

Transformation Instructions (JSON):

{
"nodes": [
{
"path": "myAccounts",
"addFields": ["email", "phone"],
"addFilters": {
"clearExistingFilters": true,
"filterGraphQlSnippet": "{ status: { eq: \"ACTIVE\" } }"
}
}
]
}

Result (GraphQL):

query {
myAccounts: accounts(where: { status: { eq: "ACTIVE" } }) {
id
name
email
phone
}
}

Query Enhancements: Fields, Filters, and Ordering

Original Query (GraphQL):

query {
myAccounts: accounts {
id
name
}
}

Transformation Instructions (JSON):

{
"nodes": [
{
"path": "myAccounts",
"addFields": ["revenue", "industry"],
"addFilters": {
"clearExistingFilters": true,
"filterGraphQlSnippet": "{
and: [
{ status: { eq: \"ACTIVE\" } },
{ revenue: { gte: 1000000 } }
]
}"
},
"orderBy": {
"clearExistingOrderBy": true,
"orderGraphQlSnippet": "{ revenue: { order: DESC } }"
}
}
]
}

Result (GraphQL):

query {
myAccounts: accounts(
where: {
and: [
{ status: { eq: "ACTIVE" } },
{ revenue: { gte: 1000000 } }
]
},
orderBy: { revenue: { order: DESC } }
) {
id
name
revenue
industry
}
}

Query Enhancements: Multiple Query Paths with Different Transformations

Original Query (GraphQL):

query {
myAccounts: accounts {
id
name
myContacts: contacts {
id
name
}
}
}

Transformation Instructions (JSON):

{
"nodes": [
{
"path": "myAccounts",
"addFields": ["email"],
"addFilters": {
"clearExistingFilters": true,
"filterGraphQlSnippet": "{ status: { eq: \"ACTIVE\" } }"
}
},
{
"path": "myAccounts.myContacts",
"addFields": ["email", "phone"],
"orderBy": {
"clearExistingOrderBy": true,
"orderGraphQlSnippet": "{ name: { order: ASC } }"
}
}
]
}

Result (GraphQL):

query {
myAccounts: accounts(where: { status: { eq: "ACTIVE" } }) {
id
name
email
myContacts: contacts(orderBy: { name: { order: ASC } }) {
id
name
email
phone
}
}
}

Query Enhancements: Fields and Snippets

Original Query (GraphQL):

query {
myAccounts: accounts {
id
name
}
}

Transformation Instructions (JSON):

{
"nodes": [
{
"path": "myAccounts",
"addFields": ["email"]
},
{
"path": "metrics",
"insertGraphQlSnippet": "{
metrics: systemMetrics {
totalCount
activeCount
lastSyncTime
}
}"
}
]
}

Result (GraphQL):

query {
myAccounts: accounts {
id
name
email
}
metrics: systemMetrics {
totalCount
activeCount
lastSyncTime
}
}