IProductOrderPATCH Apex Interface

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.

  • customiseGraphQLQuery
  • customiseMutationPayload

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 validationStatus set to any value other than "fail" (case-insensitive), or an empty map.

Failure

  • Return a top-level map with validationStatus set to "fail".
  • Nested "result" is silently dropped.
  • The client receives an HTTP error (default 422 unless validationDetails overrides).

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;
    }
}

See Also