The IAccountManagementPATCH Apex interface provides extensibility for PATCH 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.
- transformRequest
- configureDefaultValidations
- applyCustomValidations
- customiseMutationPayload
- handlePostOperation
Update Operation Use Cases
| Use Case | Hook(s) | Description | Example Scenarios | Benefit |
|---|---|---|---|---|
| Resolve an External Key to the Salesforce ID | transformRequest |
Resolves a caller-provided business key or external identifier to the canonical Salesforce Account ID before validation and update processing begins. | - Resolve an ERP customer number <br>- Map a billing-system identifier to an Account ID<br>- Translate a partner-specific key <br>- Resolve a legacy Account reference | Allows callers to address Account records using their own identifiers instead of requiring a Salesforce ID. |
| Default or Enrich Fields Before Update | transformRequest |
Normalizes the incoming partial update and supplies default, derived, or contextual values before validation. This reduces the amount of data clients must provide and ensures consistent input. | - Normalize phone numbers and addresses <br>- Derive a region from the country code <br>- Supply a source-system value from the request context<br>- Standardize status or classification values | Reduces client payload complexity and ensures consistent Account data before it is written. |
| Validate State Transitions and Business Rules | applyCustomValidations |
Evaluates the proposed partial update against organization-specific lifecycle rules and business constraints before the mutation is built. | - Reject an invalid Active to Closed transition <br>- Prevent status changes while open balances exist <br>- Require a reason when suspending an Account <br>- Restrict sensitive updates to authorized roles |
Rejects invalid partial updates early and protects Account lifecycle and data integrity. |
| Enable Conditional Validators for Partial Updates | configureDefaultValidations |
Enables validation flags according to the fields included in the partial update. Validators for nested objects run only when the corresponding data is present. | - Validate relatedParty when included <br>- Validate Contact sub-objects when supplied <br>- Enable address validation only when an address changes <br>- Apply stricter checks when regulated fields are updated |
Applies relevant validation without requiring absent fields or running unnecessary checks during partial updates. |
| Auto-Populate or Override Mutation Fields | customiseMutationPayload |
Adds or replaces fields in the GraphQL mutation payload before execution, including organization-specific values that are not represented in the TMF schema. | - Set lastModifiedBy and lastModifiedAt<br>- Add a source-system or correlation ID<br>- Override a derived lifecycle status <br>- Populate internal classification or compliance fields |
Persists organization-specific fields and provenance beyond the standard TMF schema. |
| Update Related Records Not Covered by PATCH | customiseMutationPayload (insertGraphQlSnippet) |
Inserts an additional GraphQL operation into the mutation so related records can be updated atomically with the Account. | - Add or remove Account-Contact junctions <br>- Update relationship roles <br>- Modify related-party associations<br>- Synchronize Account hierarchy links | Updates junctions and other related records in the same mutation, reducing extra API calls and inconsistent intermediate states. |
| Create an Audit Trail or Trigger Downstream Processing | handlePostOperation |
Runs after the update mutation succeeds and uses the operation result to perform compliance logging, publish events, or invoke downstream integrations. | - Write an Account-update audit record <br>- Publish an Account-changed event<br>- Synchronize changes with billing or CRM systems <br>- Trigger compliance review or notifications<br>- Invalidate related caches or indexes | Supports compliance and integration side effects while keeping them separate from the core update operation. |
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 identifier into the Salesforce ID.
global class AccountMgmtTransformRequestPatchExt implements comms_apex_ext.IAccountManagementPATCH {
global static Map<String, Object> transformRequest(Map<String, Object> context) {
if (context == null) {
return context;
}
// If the caller addressed the record by an external key, resolve it to the SF id.
Object idObj = context.get('id');
if (idObj instanceof String && ((String) idObj).startsWith('EXT-')) {
String externalKey = (String) idObj;
List<BillingAccount> matches = [
SELECT Id FROM BillingAccount WHERE ExtlBillingAccountId__c = :externalKey LIMIT 1
];
if (!matches.isEmpty()) {
context.put('id', matches[0].Id);
}
}
return context;
}
// ... other hooks return null / empty map ...
}
configureDefaultValidations
This hook enables, disables, or modifies the built-in validation rules before they execute.
Hook Method
Map<String, Boolean> configureDefaultValidations(Map<String, Boolean> defaultValidationConfiguration, Map<String, Object> context)
Sample Apex Implementation
Enable conditional validators for PATCH.
global class AccountMgmtValidationConfigPatchExt implements comms_apex_ext.IAccountManagementPATCH {
global static Map<String, Boolean> configureDefaultValidations(
Map<String, Boolean> defaultValidationConfiguration,
Map<String, Object> context
) {
// Start from the passed-in default and enable only the conditional validators.
Map<String, Boolean> spec = defaultValidationConfiguration != null
? defaultValidationConfiguration.clone()
: new Map<String, Boolean>();
spec.put('mandatoryFieldsValidation', false); // keep OFF for merge-patch
spec.put('relatedPartyValidation', true); // conditional: only fires if relatedParty is present
spec.put('contactValidation', true); // conditional: only fires if contact is present
return spec;
}
// ... other hooks return null / empty map ...
}
note: Do not enable mandatoryFieldsValidation for a typical PATCH request. It requires a non-empty relatedParty array and name, which merge-patch callers may omit, causing valid partial updates to be rejected. The PATCH handler only updates BillingAccount scalar fields; relatedParty and contact values in the request are validated, if those validators are enabled, but are not persisted as junction changes.
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
Enforce a valid state transition.
global class AccountMgmtCustomValidationPatchExt implements comms_apex_ext.IAccountManagementPATCH {
global static Map<String, Object> applyCustomValidations(Map<String, Object> context) {
Map<String, Object> requestBody = (Map<String, Object>) context.get('requestBody');
Map<String, Object> result = new Map<String, Object>();
if (requestBody != null && 'Closed'.equalsIgnoreCase(String.valueOf(requestBody.get('state')))) {
result.put('validationStatus', 'fail');
result.put('validationMessage',
'Custom validation failed: billing accounts cannot be Closed via the API.');
result.put('validationDetails', new Map<String, Object>{
'rejectedState' => requestBody.get('state'),
'rule' => 'NO_API_CLOSE'
});
return result;
}
result.put('validationStatus', 'pass');
return result;
}
// ... other hooks return null / empty map ...
}
customiseMutationPayload
This hook modifies the GraphQL query before execution to add fields, filters, or transformations.
Hook Method
Map<String, Object> customiseMutationPayload(Map<String, Object> graphQLAsMap,Map<String, Object> context)
Sample Apex Implementation
Add a field to update mutation.
global class AccountMgmtMutationPayloadPatchExt implements comms_apex_ext.IAccountManagementPATCH {
global static Map<String, Object> customiseMutationPayload(
Map<String, Object> mutationGraphQLPayload,
Map<String, Object> context
) {
Map<String, Object> node = new Map<String, Object>{
'path' => 'billingAccountUpdate',
'addFields' => new Map<String, Object>{ 'CustomerClass' => 'Commercial' }
};
return new Map<String, Object>{ 'nodes' => new List<Object>{ node } };
}
// ... 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
Audit-log the updated Billing Account.
global class AccountMgmtPostOpAuditPatchExt implements comms_apex_ext.IAccountManagementPATCH {
global static Map<String, Object> handlePostOperation(
Map<String, Object> graphQLResult,
Map<String, Object> tmfResponse,
Map<String, Object> context
) {
try {
String updatedId = (String) context.get('id');
System.debug('BillingAccount updated via TMF666 PATCH by '
+ UserInfo.getName() + ': ' + updatedId);
} catch (Exception e) {
System.debug('Audit logging failed: ' + e.getMessage());
}
// Return value is ignored for PATCH.
return null;
}
// ... other hooks return null / empty map ...
}
Full Implementation Example
Here's a complete sample Apex implementation using all supported hooks.
/**
* Complete TMF666 Account Management PATCH API Extension
* Demonstrates transformRequest, configureDefaultValidations, applyCustomValidations,
* customiseMutationPayload, and handlePostOperation.
*/
global class AccountManagementPATCHExtension implements comms_apex_ext.IAccountManagementPATCH {
/** HOOK 1: Resolve an external id into the Salesforce record id */
global static Map<String, Object> transformRequest(Map<String, Object> context) {
if (context == null) { return context; }
Object idObj = context.get('id');
if (idObj instanceof String && ((String) idObj).startsWith('EXT-')) {
List<BillingAccount> m = [
SELECT Id FROM BillingAccount WHERE ExtlBillingAccountId__c = :((String) idObj) LIMIT 1
];
if (!m.isEmpty()) { context.put('id', m[0].Id); }
}
return context;
}
/** HOOK 2: Keep mandatory off; enable conditional validators for the partial update */
global static Map<String, Boolean> configureDefaultValidations(
Map<String, Boolean> defaultValidationConfiguration,
Map<String, Object> context
) {
Map<String, Boolean> spec = defaultValidationConfiguration != null
? defaultValidationConfiguration.clone()
: new Map<String, Boolean>();
spec.put('mandatoryFieldsValidation', false);
spec.put('relatedPartyValidation', true);
spec.put('contactValidation', true);
return spec;
}
/** HOOK 3: Block Closing an account via the API */
global static Map<String, Object> applyCustomValidations(Map<String, Object> context) {
Map<String, Object> body = (Map<String, Object>) context.get('requestBody');
Map<String, Object> result = new Map<String, Object>();
if (body != null && 'Closed'.equalsIgnoreCase(String.valueOf(body.get('state')))) {
result.put('validationStatus', 'fail');
result.put('validationMessage', 'billing accounts cannot be Closed via the API.');
return result;
}
result.put('validationStatus', 'pass');
return result;
}
/** HOOK 4: Force CustomerClass on the updated record */
global static Map<String, Object> customiseMutationPayload(
Map<String, Object> mutationGraphQLPayload,
Map<String, Object> context
) {
return new Map<String, Object>{
'nodes' => new List<Object>{
new Map<String, Object>{
'path' => 'billingAccountUpdate',
'addFields' => new Map<String, Object>{ 'CustomerClass' => 'Commercial' }
}
}
};
}
/** HOOK 5: Audit (return ignored for PATCH) */
global static Map<String, Object> handlePostOperation(
Map<String, Object> graphQLResult,
Map<String, Object> tmfResponse,
Map<String, Object> context
) {
System.debug('BillingAccount updated via TMF666 PATCH by ' + UserInfo.getName());
return null;
}
/** NOT APPLICABLE for PATCH — never invoked */
global static Map<String, Object> customiseGraphQLQuery(
Map<String, Object> querySpec,
Map<String, Object> context
) {
return new Map<String, Object>();
}
}