The ICustomerManagementPATCH interface supports extensibility for PATCH operations by allowing validation, transformation, and post-processing of update requests. Implementations can enforce business rules, adjust mutation payloads, and refine the resulting responses maintaining TMF-compliant behavior.
This interface supports the following hooks.
note: These hook are not used for the Customer Management API. If implemented, the method may be invoked, but its return values are ignored.
configureDefaultValidationscustomiseGraphQLQuery
Update Lifecycle Use Cases and Hook Mapping
The following table lists common use cases and hooks required for PATCH operation.
| Use Case | Hook(s) | Description <div style="width:290px"></div> | Example Scenarios <div style="width:290px"></div> | Benefit <div style="width:290px"></div> |
|---|---|---|---|---|
| Enforce Status Transitions | applyCustomValidations |
Validates requested status changes before updating the record to ensure transitions adhere to defined lifecycle rules, regulatory constraints, or business policies. Stops invalid or unauthorized state changes. | • Prevent transition from Inactive → Active without required approvals• Block changes to Suspended unless preconditions are met• Enforce linear lifecycle progression (e.g., Pending → Active, but not Closed → Active) | Prevents invalid or noncompliant state transitions and preserves data integrity |
| Maintain Audit Trail | customiseMutationPayload |
Automatically enriches the update payload with audit metadata before persistence. Ensures all modifications are traceable and compliant with auditing standards. | • Add “modifiedBy” and timestamp fields• Track system-initiated vs. user-initiated changes• Add audit IDs for downstream monitoring systems | Provides a complete and reliable audit trail for internal governance and external compliance needs |
| Validate Data Format | applyCustomValidations |
Ensures updated data—such as contact details, identifiers, or structured fields—meets required formatting rules before the update is processed. Prevents malformed or inconsistent data from entering the system. | • Validate phone number format• Confirm email structure and domain correctness• Ensure address fields follow standardized formats | Improves data quality, prevents inconsistencies, and enhances downstream system reliability |
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
public 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;
// Check if the ID looks like a Salesforce ID (starts with 001, 003, etc.)
// If not, assume it's an AccountNumber and resolve it
if (idValue.length() == 15 || idValue.length() == 18) {
// Looks like a Salesforce ID, return context as-is
return context;
}
// Query Account objects where AccountNumber matches the provided ID
List<Account> accounts = [
SELECT Id, AccountNumber
FROM Account
WHERE AccountNumber = :idValue
LIMIT 1
];
// If found, replace the ID in context with the Salesforce ID
if (!accounts.isEmpty() && accounts[0].AccountNumber != null) {
context.put('id', accounts[0].Id);
}
return context;
}
applyCustomValidations
This hook validates custom business logic before updating a customer data. If validation fails, rejects the request and returns an error response.
The handler processes return values as follows.
Success:
- Return a map with validationStatus set to any value other than "fail" (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
public Map<String, Object> applyCustomValidations(Map<String, Object> context) {
// Input context:
// {
// api: 'CustomerManagement',
// version: '4.0',
// customerId: '001xx000003DHP',
// requestBody: { name: 'Updated Name', status: 'Inactive' }
// }
String customerId = (String) context.get('id');
Map<String, Object> requestBody = (Map<String, Object>) context.get('requestBody');
Map<String, Object> validationResult = new Map<String, Object>();
try {
String newStatus = (String) requestBody.get('status');
// Validate status transition
if (newStatus != null) {
Account currentAccount = [SELECT Id, Status__c FROM Account WHERE Id = :customerId LIMIT 1];
if (!isValidStatusTransition(currentAccount.Status__c, newStatus)) {
validationResult.put('validationStatus', 'FAIL');
validationResult.put('validationMessage', 'Invalid status transition from ' + currentAccount.Status__c + ' to ' + newStatus);
validationResult.put('validationDetails', new Map<String, Object>{
'currentStatus' => currentAccount.Status__c,
'requestedStatus' => newStatus,
'allowedTransitions' => getAllowedTransitions(currentAccount.Status__c)
});
return validationResult;
}
}
// Validate contact information if provided
Map<String, Object> contactMedium = (Map<String, Object>) requestBody.get('contactMedium');
if (contactMedium != null && !isValidContactMedium(contactMedium)) {
validationResult.put('validationStatus', 'FAIL');
validationResult.put('validationMessage', 'Invalid contact medium format');
return validationResult;
}
// PASS: All validations passed
validationResult.put('validationStatus', 'PASS');
validationResult.put('validationMessage', 'Customer update validation passed');
} catch (Exception e) {
// FAIL: Error during validation
validationResult.put('validationStatus', 'FAIL');
validationResult.put('validationMessage', 'Validation error: ' + e.getMessage());
}
return validationResult;
}
private Boolean isValidStatusTransition(String currentStatus, String newStatus) {
// Define valid status transitions
Map<String, Set<String>> validTransitions = new Map<String, Set<String>>{
'Active' => new Set<String>{'Inactive', 'Suspended'},
'Inactive' => new Set<String>{'Active'},
'Suspended' => new Set<String>{'Active', 'Inactive'}
};
Set<String> allowed = validTransitions.get(currentStatus);
return allowed != null && allowed.contains(newStatus);
}
private List<String> getAllowedTransitions(String currentStatus) {
Map<String, List<String>> transitions = new Map<String, List<String>>{
'Active' => new List<String>{'Inactive', 'Suspended'},
'Inactive' => new List<String>{'Active'},
'Suspended' => new List<String>{'Active', 'Inactive'}
};
return transitions.get(currentStatus);
}
private Boolean isValidContactMedium(Map<String, Object> contactMedium) {
String mediumType = (String) contactMedium.get('mediumType');
return mediumType != null && !mediumType.isBlank();
}
customiseMutationPayload
This hook modifies the mutation payload before updating a customer record. Use this to add computed fields, transform values, or apply business logic transformations.
Hook Method
Map<String, Object> customiseMutationPayload(Map<String, Object> request, Map<String, Object> context)
Sample Apex Implementation
/**
* Customizes the mutation payload before the GraphQL mutation is executed.
* Reads Description and Industry from the request body in context and adds them
* to the Account node at alias path "customerAccount".
*
* @param request The mutation request payload
* @param context Request context containing API details and request body
* @return Map containing mutation transformation instructions with nodes array:
* - nodes[0]: Contains path='customerAccount' and addFields with Description and Industry
*/
public Map<String, Object> customiseMutationPayload(Map<String, Object> request, Map<String, Object> context) {
// Return mutation transformation instructions: add Account fields at alias path "customerAccount"
Map<String, Object> nodes0 = new Map<String, Object>();
nodes0.put('path', 'customerAccount');
Map<String, Object> addFields = new Map<String, Object>();
// Read Description and Industry from request body in context
Map<String, Object> requestBody = (Map<String, Object>)context.get('requestBody');
if (requestBody != null) {
String description = (String)requestBody.get('description');
String industry = (String)requestBody.get('industry');
// Add Description to Account if present in request
if (description != null) {
addFields.put('Description', description);
}
// Add Industry to Account if present in request
if (industry != null) {
addFields.put('Industry', industry);
}
}
nodes0.put('addFields', addFields);
// Build nodes array containing the transformation instructions
List<Object> nodes = new List<Object>();
nodes.add(nodes0);
// Return result with nodes array
Map<String, Object> result = new Map<String, Object>();
result.put('nodes', nodes);
return result;
}
handlePostOperation
This hook post-processes and transforms the API response after data is updated.
Hook Method
Map<String, Object> handlePostOperation(Map<String, Object> graphQLQueryResultAsMap, Map<String, Object> constructedTMFResponse, Map<String, Object> context)
Full Implementation Example
Here's a complete sample Apex implementation using all supported hooks.
/**
* Complete Customer Management POST API Extension
* Demonstrates all three applicable lifecycle hooks with business logic
*/
public class CustomerManagementPATCHExtension implements comms_apex_ext.ICustomerManagementPATCH {
/**
* Hook 1: Apply custom validations
*/
public Map<String, Object> applyCustomValidations(Map<String, Object> context) {
Map<String, Object> requestBody = (Map<String, Object>) context.get('requestBody');
Map<String, Object> validationResult = new Map<String, Object>();
try {
String customerName = (String) requestBody.get('name');
String customerType = (String) requestBody.get('type');
// Validate required fields
if (String.isBlank(customerName)) {
validationResult.put('validationStatus', 'FAIL');
validationResult.put('validationMessage', 'Customer name is required');
return validationResult;
}
// Validate customer type
if (!isValidCustomerType(customerType)) {
validationResult.put('validationStatus', 'FAIL');
validationResult.put('validationMessage', 'Invalid customer type');
return validationResult;
}
// Check for duplicates
List<Account> existingAccounts = [SELECT Id FROM Account WHERE Name = :customerName LIMIT 1];
if (!existingAccounts.isEmpty()) {
validationResult.put('validationStatus', 'FAIL');
validationResult.put('validationMessage', 'Customer with this name already exists');
return validationResult;
}
validationResult.put('validationStatus', 'PASS');
validationResult.put('validationMessage', 'Validation passed');
} catch (Exception e) {
validationResult.put('validationStatus', 'FAIL');
validationResult.put('validationMessage', 'Validation error: ' + e.getMessage());
}
return validationResult;
}
/**
* Hook 2: Customize mutation payload
*/
/**
* Customizes the mutation payload before the GraphQL mutation is executed.
* Reads Description and Industry from the request body in context and adds them
* to the Account node at alias path "customerAccount".
*
* @param request The mutation request payload
* @param context Request context containing API details and request body
* @return Map containing mutation transformation instructions with nodes array:
* - nodes[0]: Contains path='customerAccount' and addFields with Description and Industry
*/
public Map<String, Object> customiseMutationPayload(Map<String, Object> request, Map<String, Object> context) {
// Return mutation transformation instructions: add Account fields at alias path "customerAccount"
Map<String, Object> nodes0 = new Map<String, Object>();
nodes0.put('path', 'customerAccount');
Map<String, Object> addFields = new Map<String, Object>();
// Read Description and Industry from request body in context
Map<String, Object> requestBody = (Map<String, Object>)context.get('requestBody');
if (requestBody != null) {
String description = (String)requestBody.get('description');
String industry = (String)requestBody.get('industry');
// Add Description to Account if present in request
if (description != null) {
addFields.put('Description', description);
}
// Add Industry to Account if present in request
if (industry != null) {
addFields.put('Industry', industry);
}
}
nodes0.put('addFields', addFields);
// Build nodes array containing the transformation instructions
List<Object> nodes = new List<Object>();
nodes.add(nodes0);
// Return result with nodes array
Map<String, Object> result = new Map<String, Object>();
result.put('nodes', nodes);
return result;
}
/**
* Hook 3: Post-process response
*/
public Map<String, Object> handlePostOperation(
Map<String, Object> graphQLQueryResultAsMap,
Map<String, Object> constructedTMFResponse,
Map<String, Object> context
) {
return null;
}
/**
* NOT APPLICABLE: configureDefaultValidations is not used for Customer Management API
*/
public Map<String, Boolean> configureDefaultValidations(
Map<String, Boolean> defaultValidationConfiguration,
Map<String, Object> context
) {
return null;
}
private Boolean isValidCustomerType(String type) {
Set<String> validTypes = new Set<String>{'Customer', 'Prospect', 'Partner'};
return validTypes.contains(type);
}
private String determineCustomerSegment(String name) {
if (name.length() > 20) {
return 'Enterprise';
} else if (name.length() > 10) {
return 'Mid-Market';
} else {
return 'SMB';
}
}
**
* NOT APPLICABLE: customiseGraphQLQuery is not used for Customer Management API
*/
public Map<String, Object> customiseGraphQLQuery(
Map<String, Object> graphQLAsMap,
Map<String, Object> context
) {
return null;
}
}
GraphQL Mutation - Update Operations
Update mutations modify existing records and return the updated records with the specified output fields.
Update Enhancements: Add Output Fields
Original Mutation (GraphQL):
mutation UpdateCustomerAndRelatedEntities {
uiapi {
customerAccount: AccountUpdate(input: {
Id: "001xx000003DHP"
Account: { Name: "Updated Name" }
}) {
Record {
Id
Name { value }
}
}
}
}
Transformation Instructions (JSON):
{
"nodes": [{
"path": "uiapi.customerAccount",
"addFields": {
"LastModifiedDate": "2024-01-15",
"ModifiedBy": "user123",
"Status": "Updated"
}
}]
}
Result (GraphQL):
mutation UpdateCustomerAndRelatedEntities {
uiapi {
customerAccount: AccountUpdate(input: {
Id: "001xx000003DHP"
Account: { Name: "Updated Name" }
}) {
Record {
Id
Name { value }
LastModifiedDate
ModifiedBy
Status
}
}
}
}
Update Enhancements: Modify Input – Add Fields
Original Mutation (GraphQL):
mutation UpdateCustomerAndRelatedEntities {
uiapi {
customerAccount: AccountUpdate(input: {
Id: "001xx000003DHP"
Account: { Name: "Updated Name" }
}) {
Record {
Id
Name { value }
}
}
}
}
Transformation Instructions (JSON - Add Input Fields):
{
"nodes": [{
"path": "uiapi.customerAccount",
"modifyInput": {
"addInputFields": {
"Industry": "Technology",
"Status": "Active"
}
}
}]
}
Result (GraphQL):
mutation UpdateCustomerAndRelatedEntities {
uiapi {
customerAccount: AccountUpdate(input: {
Id: "001xx000003DHP"
Account: {
Name: "Updated Name",
Industry: "Technology",
Status: "Active"
}
}) {
Record {
Id
Name { value }
}
}
}
}
Update Enhancements: Modify Input and Add Output Fields
Transformation Instructions (JSON):
{
"nodes": [{
"path": "uiapi.customerAccount",
"addFields": {
"LastModifiedDate": "2024-01-15",
"ChangeLog": "Updated via API"
},
"modifyInput": {
"addInputFields": {
"UpdateReason": "Bulk Update",
"UpdateSource": "API"
},
"inputModifications": {
"Status": "Active"
}
}
}]
}
Result (GraphQL):
mutation UpdateCustomerAndRelatedEntities {
uiapi {
customerAccount: AccountUpdate(input: {
Id: "001xx000003DHP"
Account: {
Name: "Updated Name",
Status: "Active",
UpdateReason: "Bulk Update",
UpdateSource: "API"
}
}) {
Record {
Id
Name { value }
LastModifiedDate
ChangeLog
}
}
}
}