IProductOrderPOST Apex Interface

The IProductOrderPOST Apex interface enables customization of POST operations using Apex lifecycle hooks. One handler routes into four mutually exclusive sub-flows from productOrderItem[].action (and product.id). Implementations can validate and enrich creation, amendment, renewal, or cancellation payloads, and run side effects after the pipeline, 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 POST. If implemented, the methods are required to compile, but they are never invoked (constructGraphQL / constructMutationGraphQL return null).

  • customiseGraphQLQuery
  • customiseMutationPayload

Rewrite the outbound payload in preServiceHook via modifiedPayload.

Sub-flows 

Mixed actions (add+renew, cancel+anything, and similar) are rejected by validateSingleActionType before any Apex hook.

Sub-flowHow it is detectedPipeline steps
Standard create/submitDefault when no amend/renew/cancel actionorderIngestionactivateOrdersubmitOrder (last two skipped if the org has Quotes Tax)
AmendmentAny item action=amend, or a non-cancel/non-renew item with existing product.idinitiateAmendmentactivateOrdersubmitOrder
RenewalAny item action=renew (checked first)initiateRenewalactivateOrdersubmitOrder
CancellationAny item action=cancelinitiateCancellationactivateOrdersubmitOrder

preServiceHook fires up to three times per request (once per step) using finalPayload re-invocation: build payload → Apex → execute with modifiedPayload. Switch on serviceName.

Creation Lifecycle Use Cases and Hook Mapping 

The following table lists common use cases for the POST operation.

Use CaseHook(s)DescriptionExample ScenariosBenefit
Force a sub-flowtransformRequestRuns before sub-flow detection. Mutating productOrderItem[].action (or injecting product.id) changes which pipeline is built.• Set action=amend when product.id is present
• Route cancel vs renew vs amend to a single action
Avoid mixed-action rejection and pick the correct pipeline
Enforce create rulesapplyCustomValidationsOnly loud HTTP error path. Return top-level validationStatus / validationMessage. Nested result is silently dropped.• Require requestedStartDate on add items
• Duplicate amendment/renewal checks the handler does not run
Prevents invalid orders before service calls
Inject ingestion preferencespreServiceHook (orderIngestion)Add scalar keys on the initiating payload. orderGraph is a live Java object — prefer scalars.pricingPref, taxPrefChannel-specific pricing/tax without forking the handler
Override amendment quantity deltaspreServiceHook (initiateAmendment)Mutate quantityChanges before execute.• Org rounding rulesKeep quantity math consistent
Hold activated but unsubmittedpreServiceHook (submitOrder)skipCall: true is not an HTTP error. Skipping an initiating step leaves a null order id. Skipping submitOrder is the only coherent partial skip.• Nightly batch submitPartial pipeline without a broken order
Audit after POSThandlePostOperationReturn is discarded. Client body is GET re-fetch. A non-empty map fires telemetry (up to 3× from preServiceHook).• Platform events, correlation idsSide effects without shaping the HTTP body

transformRequest 

This hook transforms the request context before processing any operation. It is invoked before sub-flow detection.

Hook Method 

Map<String, Object> transformRequest(Map<String, Object> context)

Sample Apex Implementation 

1global static Map<String, Object> transformRequest(Map<String, Object> context) {
2    Map<String, Object> body = (Map<String, Object>) context.get('requestBody');
3    if (body == null || !(body.get('productOrderItem') instanceof List<Object>)) {
4        return context;
5    }
6    for (Object itemObj : (List<Object>) body.get('productOrderItem')) {
7        Map<String, Object> item = (Map<String, Object>) itemObj;
8        Map<String, Object> product = (Map<String, Object>) item.get('product');
9        if (product != null && product.get('id') != null && item.get('action') == null) {
10            item.put('action', 'amend');
11        }
12    }
13    return context;
14}

configureDefaultValidations 

Default spec (all true): orderLevelAttributesValidation, relatedPartyValidation, productOrderItemsValidation. Return a map toggling those flags.

Handler validateAmendmentItems / validateRenewalItems / validateCancellationItems do not run in the normal request flow (isXRequest is still false during validation). Duplicate critical checks in applyCustomValidations. Missing product.id is enforced later in payload builders as HTTP 400.

Hook Method 

Map<String, Boolean> configureDefaultValidations(Map<String, Boolean> defaultValidationConfiguration, Map<String, Object> context)

applyCustomValidations 

This hook validates custom business logic before the service-call pipeline. 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 any value other than "fail" (case-insensitive), or an empty map.
  • The API request continues normally.

Failure

  • Return a top-level map with validationStatus set to "fail". Nested "result" is silently dropped.
  • The API request is terminated immediately (ValidationException). Default mapping is HTTP 422 unless validationDetails overrides.

Hook Method 

Map<String, Object> applyCustomValidations(Map<String, Object> context)

Sample Apex Implementation 

1global static Map<String, Object> applyCustomValidations(Map<String, Object> context) {
2    Map<String, Object> body = (Map<String, Object>) context.get('requestBody');
3    if (body == null) {
4        return new Map<String, Object>();
5    }
6    Object items = body.get('productOrderItem');
7    if (items instanceof List<Object> && body.get('requestedStartDate') == null) {
8        for (Object itemObj : (List<Object>) items) {
9            Map<String, Object> item = (Map<String, Object>) itemObj;
10            if ('add' == item.get('action') || item.get('action') == null) {
11                return new Map<String, Object>{
12                    'validationStatus'  => 'fail',
13                    'validationMessage' => 'requestedStartDate is required when adding new items'
14                };
15            }
16        }
17    }
18    return new Map<String, Object>();
19}

preServiceHook 

This hook runs once per pipeline step (up to three times). Return {skipCall, modifiedPayload}. Exceptions are swallowed per step (WARNING); the original payload is used. skipCall is not an HTTP error.

serviceNamePayload shapeNotes
orderIngestion{orderGraph}Standard create. Prefer scalar keys such as pricingPref
initiateAmendment{amendAssetIds, amendStartDate, amendOutputType, itemQuantities, quantityChanges}Amendment initiating step
initiateRenewal{renewAssetIds, renewStartDate, renewOutputType, renewEndDate?}Renewal initiating step
initiateCancellation{cancelAssetIds, cancelStartDate, cancelOutputType}Cancellation initiating step
activateOrder{orderId, orderNumber}Shared. Skipped on standard flow when org has Quotes Tax
submitOrder{orderId, orderNumber}Shared. skipCall here is the only coherent partial skip

Hook Method 

Map<String, Object> preServiceHook(String serviceName, Map<String, Object> payload, Map<String, Object> executionContext, Map<String, Object> metadata)

Sample Apex Implementation 

1global static Map<String, Object> preServiceHook(String serviceName, Map<String, Object> payload,
2        Map<String, Object> executionContext, Map<String, Object> metadata) {
3    if (serviceName == 'orderIngestion' && payload != null) {
4        payload.put('pricingPref', 'CatalogRates');
5        payload.put('taxPref', 'SkipSync');
6        return new Map<String, Object>{ 'skipCall' => false, 'modifiedPayload' => payload };
7    }
8    if (serviceName == 'initiateAmendment' && payload != null
9            && payload.get('quantityChanges') instanceof Map<String, Object>) {
10        Map<String, Object> changes = (Map<String, Object>) payload.get('quantityChanges');
11        payload.put('quantityChanges', changes);
12        return new Map<String, Object>{ 'skipCall' => false, 'modifiedPayload' => payload };
13    }
14    if (serviceName == 'submitOrder') {
15        return new Map<String, Object>{ 'skipCall' => true }; // hold for nightly batch
16    }
17    return new Map<String, Object>{ 'skipCall' => false, 'modifiedPayload' => payload };
18}

handlePostOperation 

graphQLQueryResultAsMap is synthetic {"data": flowResult} from orderIngestionResult / initiateAmendmentResult / initiateRenewalResult / initiateCancellationResult. constructedTMFResponse is the request body. Return is discarded. The client representation is GET retrieveResource.

Hook Method 

Map<String, Object> handlePostOperation(Map<String, Object> graphQLQueryResultAsMap, Map<String, Object> constructedTMFResponse, Map<String, Object> context)

Sample Apex Implementation 

1global static Map<String, Object> handlePostOperation(
2        Map<String, Object> graphQLQueryResultAsMap,
3        Map<String, Object> constructedTMFResponse,
4        Map<String, Object> context) {
5    System.debug('ProductOrder POST completed by ' + UserInfo.getName());
6    return null; // discarded; client body is GET re-fetch
7}

Full Implementation Example 

1global class ProductOrderPOSTExtension implements comms_apex_ext.IProductOrderPOST {
2
3    global static Map<String, Object> transformRequest(Map<String, Object> context) {
4        Map<String, Object> body = (Map<String, Object>) context.get('requestBody');
5        if (body == null || !(body.get('productOrderItem') instanceof List<Object>)) {
6            return context;
7        }
8        for (Object itemObj : (List<Object>) body.get('productOrderItem')) {
9            Map<String, Object> item = (Map<String, Object>) itemObj;
10            Map<String, Object> product = (Map<String, Object>) item.get('product');
11            if (product != null && product.get('id') != null && item.get('action') == null) {
12                item.put('action', 'amend');
13            }
14        }
15        return context;
16    }
17
18    global static Map<String, Boolean> configureDefaultValidations(
19            Map<String, Boolean> defaultValidationConfiguration, Map<String, Object> context) {
20        return defaultValidationConfiguration;
21    }
22
23    global static Map<String, Object> applyCustomValidations(Map<String, Object> context) {
24        return new Map<String, Object>();
25    }
26
27    global static Map<String, Object> customiseGraphQLQuery(
28            Map<String, Object> graphQLAsMap, Map<String, Object> context) {
29        return new Map<String, Object>(); // never invoked
30    }
31
32    global static Map<String, Object> customiseMutationPayload(
33            Map<String, Object> mutationGraphQLPayload, Map<String, Object> context) {
34        return new Map<String, Object>(); // never invoked
35    }
36
37    global static Map<String, Object> preServiceHook(String serviceName, Map<String, Object> payload,
38            Map<String, Object> executionContext, Map<String, Object> metadata) {
39        if (serviceName == 'orderIngestion' && payload != null) {
40            payload.put('pricingPref', 'CatalogRates');
41            return new Map<String, Object>{ 'skipCall' => false, 'modifiedPayload' => payload };
42        }
43        return new Map<String, Object>{ 'skipCall' => false, 'modifiedPayload' => payload };
44    }
45
46    global static Map<String, Object> handlePostOperation(
47            Map<String, Object> graphQLQueryResultAsMap,
48            Map<String, Object> constructedTMFResponse,
49            Map<String, Object> context) {
50        return null;
51    }
52}

See Also