The IProductOrderPATCH Apex interface supports extensibility for PATCH operations by allowing validation, transformation, and post-processing of update requests. Unlike GET and DELETE, PATCH does not build a GraphQL document. It uses a declarative service-call pipeline with a single step named placeSupplemental. Implementations can enforce business rules, rewrite the outbound payload, and run side effects while the client-visible order is re-fetched via GET.
This interface supports the following hooks.
- transformRequest
- configureDefaultValidations
- applyCustomValidations
- preServiceHook
- handlePostOperation
Note
These hooks are not used for Product Order PATCH. If implemented, the methods are required to compile, but they are never invoked.
customiseGraphQLQuerycustomiseMutationPayload
Rewrite the outbound payload in preServiceHook via modifiedPayload.
{id} is an OrderNumber. Draft orders amend in place via OrderIngestionServiceImpl.ingestOrder. Activated orders create a supplemental order via PlaceSupplementalTransactionService. Flipping isDraft in modifiedPayload redirects that routing.
Update Lifecycle Use Cases and Hook Mapping
The following table lists common use cases and hooks required for the PATCH operation.
| Use Case | Hook(s) | Description | Example Scenarios | Benefit |
|---|---|---|---|---|
| Resolve a PO- number to OrderNumber | transformRequest |
Mutations to id and requestBody are written back before validation and the pipeline. |
• PO-* → Order.OrderNumber via PoNumber |
Canonical id before placeSupplemental |
| Strip a locked field | transformRequest |
Remove fields before the change graph is built. preServiceHook sees an already-built changeGraph. |
• Drop priceBookName |
Prevents updates to protected catalog fields |
| Require description when cancelling | applyCustomValidations |
Only PATCH hook that surfaces an HTTP error. Return top-level validationStatus = "fail". Nested result is silently dropped. |
• state = Cancelled with blank description |
Fail closed before the service call |
| Force in-place draft ingest | preServiceHook |
serviceName is always placeSupplemental. Payload: {relatedTransactionId, changeGraph, isDraft}. |
• Set isDraft true so a supplemental order is never created |
Org-wide routing policy |
| Dry-run without placing the transaction | preServiceHook |
skipCall: true is not an HTTP error. Exceptions are swallowed (WARNING). |
• dryRun on request body |
Preview without persistence |
| Audit after PATCH | handlePostOperation |
Return discarded. constructedTMFResponse is the request body, not the patched order. Client body is GET re-fetch. |
• Telemetry, correlation ids | Side effects without shaping the HTTP body |
transformRequest
This hook transforms the request context before processing any operation. Put derived values on requestBody here — the change graph does not exist yet.
Hook Method
Map<String, Object> transformRequest(Map<String, Object> context)
Sample Apex Implementation
global static Map<String, Object> transformRequest(Map<String, Object> context) {
Map<String, Object> body = (Map<String, Object>) context.get('requestBody');
if (body != null && body.containsKey('priceBookName')) {
body.remove('priceBookName');
}
String requestedId = (String) context.get('id');
if (requestedId != null && requestedId.startsWith('PO-')) {
Order o = [SELECT OrderNumber FROM Order WHERE PoNumber = :requestedId LIMIT 1];
context.put('id', o.OrderNumber);
}
return context;
}
configureDefaultValidations
Default spec: {"productOrderItemsValidation": true}. All other PATCH fields are optional at the framework layer. Return a map toggling that flag.
Hook Method
Map<String, Boolean> configureDefaultValidations(Map<String, Boolean> defaultValidationConfiguration, Map<String, Object> context)
applyCustomValidations
This hook validates custom business logic before updating a Product Order. If validation fails, it rejects the request and returns an error response.
The handler processes return values as follows.
Success
- Return a map with
validationStatusset to any value other than"fail"(case-insensitive), or an empty map.
Failure
- Return a top-level map with
validationStatusset to"fail". - Nested
"result"is silently dropped. - The client receives an HTTP error (default 422 unless
validationDetailsoverrides).
Do not use skipCall or thrown exceptions in preServiceHook as HTTP errors — both are silent.
Hook Method
Map<String, Object> applyCustomValidations(Map<String, Object> context)
Sample Apex Implementation
global static Map<String, Object> applyCustomValidations(Map<String, Object> context) {
Map<String, Object> body = (Map<String, Object>) context.get('requestBody');
if (body != null && body.get('state') == 'Cancelled'
&& String.isBlank((String) body.get('description'))) {
return new Map<String, Object>{
'validationStatus' => 'fail',
'validationMessage' => 'A description is required when cancelling an order via PATCH'
};
}
return new Map<String, Object>();
}
preServiceHook
The executor runs twice (finalPayload re-invocation). First call builds {relatedTransactionId, changeGraph, isDraft}. Apex may rewrite that map. Second call executes doPlaceTransaction(finalPayload).
changeGraph is a live SObjectGraphRequest — treat as opaque unless replacing it. Prefer scalar keys such as isDraft.
Hook Method
Map<String, Object> preServiceHook(String serviceName, Map<String, Object> payload, Map<String, Object> executionContext, Map<String, Object> metadata)
Sample Apex Implementation
global static Map<String, Object> preServiceHook(String serviceName, Map<String, Object> payload,
Map<String, Object> executionContext, Map<String, Object> metadata) {
Map<String, Object> body = (Map<String, Object>) executionContext.get('requestBody');
if (body != null && body.get('dryRun') == true) {
return new Map<String, Object>{ 'skipCall' => true };
}
if (serviceName == 'placeSupplemental' && payload != null) {
payload.put('isDraft', true); // never create a supplemental order in this org
return new Map<String, Object>{ 'skipCall' => false, 'modifiedPayload' => payload };
}
return new Map<String, Object>{ 'skipCall' => false, 'modifiedPayload' => payload };
}
handlePostOperation
graphQLQueryResultAsMap is synthetic {"data": placeSupplementalResult} (not GraphQL). constructedTMFResponse is the request body. Return is discarded. A non-empty return map fires extensibility telemetry.
Hook Method
Map<String, Object> handlePostOperation(Map<String, Object> graphQLQueryResultAsMap, Map<String, Object> constructedTMFResponse, Map<String, Object> context)
Sample Apex Implementation
global static Map<String, Object> handlePostOperation(
Map<String, Object> graphQLQueryResultAsMap,
Map<String, Object> constructedTMFResponse,
Map<String, Object> context) {
System.debug('ProductOrder PATCH by ' + UserInfo.getName());
return null; // discarded; client body comes from GET re-fetch
}
Full Implementation Example
global class ProductOrderPATCHExtension implements comms_apex_ext.IProductOrderPATCH {
global static Map<String, Object> transformRequest(Map<String, Object> context) {
Map<String, Object> body = (Map<String, Object>) context.get('requestBody');
if (body != null && body.containsKey('priceBookName')) {
body.remove('priceBookName');
}
return context;
}
global static Map<String, Boolean> configureDefaultValidations(
Map<String, Boolean> defaultValidationConfiguration, Map<String, Object> context) {
return defaultValidationConfiguration;
}
global static Map<String, Object> applyCustomValidations(Map<String, Object> context) {
return new Map<String, Object>();
}
global static Map<String, Object> customiseGraphQLQuery(
Map<String, Object> graphQLAsMap, Map<String, Object> context) {
return new Map<String, Object>(); // never invoked
}
global static Map<String, Object> customiseMutationPayload(
Map<String, Object> mutationGraphQLPayload, Map<String, Object> context) {
return new Map<String, Object>(); // never invoked
}
global static Map<String, Object> preServiceHook(String serviceName, Map<String, Object> payload,
Map<String, Object> executionContext, Map<String, Object> metadata) {
return new Map<String, Object>{ 'skipCall' => false, 'modifiedPayload' => payload };
}
global static Map<String, Object> handlePostOperation(
Map<String, Object> graphQLQueryResultAsMap,
Map<String, Object> constructedTMFResponse,
Map<String, Object> context) {
return null;
}
}