ITroubleTicketManagementGET Apex Interface

The ITroubleTicketManagementGET Apex interface provides extensibility for TMF621 GET 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.

note: The configureDefaultValidations hook is not used for the Trouble Ticket Management API. If implemented, the method may be invoked, but its return values are ignored.

Retrieval Lifecycle Use Cases and Hook Mapping

This table lists common use cases and the hooks required for the GET operation.

Use Case Hook(s) Description <div style="width:290px"></div> Example Scenarios <div style="width:290px"></div> Benefit <div style="width:290px"></div>
Role-Based Data Filtering applyCustomValidations, handlePostOperation Enforces access rules by validating user permissions before execution and filtering sensitive data after retrieval. Supports role-based visibility at field and record levels. • Restrict access to VIP or high-value customer profiles• Hide sensitive fields (e.g., credit score) for frontline agents• Enforce region-based access boundaries Strengthens data governance and ensures secure, compliant access to trouble tickets
Custom Field Enrichment handlePostOperation Adds calculated or derived fields to API responses without changing the base TMF schema, allowing customers to extend or modify as needed. Enables enrichment using internal business rules or external systems. • Add customer health score• Flag “preferred customer” status• Include aggregated financial or usage insights Delivers richer, more contextual responses while preserving TMF schema integrity
Dynamic Query Customization customiseGraphQLQuery Modifies GraphQL queries dynamically based on client or request context by adding fields, filters, or transformations. • Mobile app requests only essential fields• Portal requires additional entitlement or hierarchy fields• Apply filters automatically for partner channels Supports diverse client requirements using a single API and reduces need for API versioning
Business Rule Validation applyCustomValidations Applies business and eligibility rules before processing requests, stopping invalid operations early in the lifecycle. • Ensure customer is “active” before retrieving services• Reject unauthorized or unverified channel requests• Enforce lifecycle or dependency checks Provides strong business consistency and reduces downstream errors by blocking invalid requests early
CaseNumber to Id Resolution transformRequest Resolve CaseNumber identifiers to Salesforce Ids for API compatibility. • An external system sends a CaseNumber (e.g., 00001027), which is resolved to a Salesforce record ID for API processing Support external systems that use CaseNumber instead of Salesforce Id

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 500, 00X, etc.)
    // If not, assume it's a CaseNumber and resolve it
    if (idValue.length() == 15 || idValue.length() == 18) {
        // Looks like a Salesforce ID, return context as-is
        return context;
    }
    // Query Case objects where CaseNumber matches the provided ID
    List<Case> cases = [
        SELECT Id, CaseNumber
        FROM Case
        WHERE CaseNumber = :idValue
        LIMIT 1
    ];
    // If found, replace the ID in context with the Salesforce ID
    if (!cases.isEmpty() && cases[0].CaseNumber != null) {
        context.put('id', cases[0].Id);
    }
    return context;
}

applyCustomValidations

This hook validates custom business logic before retrieving trouble ticket data. 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

public Map<String, Object> applyCustomValidations(Map<String, Object> context) {
    // Input context:
    // {
    //   api: 'TroubleTicketManagement',
    //   version: '5.0',
    //   id: '00001027',
    //   userId: '005xx000001Sv5',
    //   fields: ['Id', 'Subject', 'Status']
    // }

    String troubleTicketId = (String) context.get('id');
    String userId = (String) context.get('userId');

    Map<String, Object> validationResult = new Map<String, Object>();

    try {
        // Check if user has access to this trouble ticket
        User currentUser = [SELECT Id, Profile.Name FROM User WHERE Id = :userId LIMIT 1];
        Case troubleTicket = [SELECT Id, Status, CaseNumber FROM Case WHERE CaseNumber = :troubleTicketId LIMIT 1];

        if ('Customer Service Rep'.equals(currentUser.Profile.Name)) {
            // Validate that Customer Service Reps cannot access closed tickets
            if ('Closed'.equals(troubleTicket.Status)) {
                // FAIL: User does not have access to closed tickets
                validationResult.put('validationStatus', 'FAIL');
                validationResult.put('validationMessage', 'User does not have access to closed trouble tickets');
                validationResult.put('validationDetails', new Map<String, Object>{
                    'denialReason' => 'STATUS_RESTRICTION',
                    'userProfile' => 'Customer Service Rep',
                    'ticketStatus' => troubleTicket.Status
                });
            } else {
                // PASS: User has access to non-closed tickets
                validationResult.put('validationStatus', 'PASS');
                validationResult.put('validationMessage', 'User has access to this trouble ticket');
            }
        } else {
            // PASS: Other profiles have unrestricted access
            validationResult.put('validationStatus', 'PASS');
            validationResult.put('validationMessage', 'User profile has unrestricted access');
        }
    } catch (Exception e) {
        // FAIL: Error during validation
        validationResult.put('validationStatus', 'FAIL');
        validationResult.put('validationMessage', 'Validation error: ' + e.getMessage());
        validationResult.put('validationDetails', new Map<String, Object>{
            'errorType' => e.getTypeName()
        });
    }

    // Return validation result directly (not wrapped in 'result' key)
    return validationResult;
}

customiseGraphQLQuery

This hook modifies the GraphQL query before execution to add fields, filters, or transformations. Returns a map with a nodes key containing a list of QueryTransformationNode objects.

Hook Method

Map<String, Object> customiseGraphQLQuery(Map<String, Object> graphQLAsMap, Map<String, Object> context)

Sample Apex Implementation

public Map<String, Object> customiseGraphQLQuery(
    Map<String, Object> graphQLAsMap,
    Map<String, Object> context
) {
    // Create transformation specification to add custom fields and filters
    List<Map<String, Object>> transformationNodes = new List<Map<String, Object>>();

    // Node 1: Add custom fields to the Case node
    // Path: troubleTicket (the Case alias in the GraphQL query)
    Map<String, Object> addFieldsNode = new Map<String, Object>{
        'path' => 'troubleTicket',
        'addFields' => new List<String>{'IsClosed', 'SlaStartDate', 'SlaExitDate'}
    };
    transformationNodes.add(addFieldsNode);

    // Node 2: Add filters to the Case query
    // Only retrieve open trouble tickets
    Map<String, Object> filterNode = new Map<String, Object>{
        'path' => 'troubleTicket',
        'addFilters' => new Map<String, Object>{
            'clearExistingFilters' => false,
            'filterGraphQlSnippet' => '{ Status: { eq: "Open" } }'
        }
    };
    transformationNodes.add(filterNode);

    // Return transformation specification
    return new Map<String, Object>{
        'nodes' => transformationNodes
    };
}


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

public Map<String, Object> handlePostOperation(
    Map<String, Object> graphQLQueryResultAsMap,
    Map<String, Object> constructedTMFResponse,
    Map<String, Object> context
) {
    // Input GraphQL result:
    // {
    //   items: [{ Id: '500xx000003ABC', Subject: 'Network Issue', Status: 'Open', CreatedDate: '2024-01-15T10:00:00Z' }],
    //   count: 1
    // }

    // Input TMF response:
    // {
    //   recordList: [{ id: '00001027', name: 'Network Issue', status: 'Open', type: 'TroubleTicket' }]
    // }

    List<Object> troubleTicketList = (List<Object>) constructedTMFResponse.get('recordList');

    if (troubleTicketList != null && !troubleTicketList.isEmpty()) {
        for (Object ticketObj : troubleTicketList) {
            Map<String, Object> ticket = (Map<String, Object>) ticketObj;
            String ticketId = (String) ticket.get('id');

            // Fetch additional data for enrichment
            Case caseRecord = [SELECT Id, CaseNumber, CreatedDate, ClosedDate, Status FROM Case WHERE CaseNumber = :ticketId LIMIT 1];

            // Add calculated fields
            if (caseRecord.CreatedDate != null) {
                DateTime createdDate = caseRecord.CreatedDate;
                DateTime now = DateTime.now();
                Long hoursOpen = (now.getTime() - createdDate.getTime()) / (1000 * 60 * 60);
                ticket.put('hoursOpen', hoursOpen);
            }

            if (caseRecord.ClosedDate != null && caseRecord.CreatedDate != null) {
                Long resolutionTime = (caseRecord.ClosedDate.getTime() - caseRecord.CreatedDate.getTime()) / (1000 * 60 * 60);
                ticket.put('resolutionTimeHours', resolutionTime);
            }

            // Add SLA status
            if ('Open'.equals(caseRecord.Status)) {
                ticket.put('slaStatus', 'In Progress');
            } else if ('Closed'.equals(caseRecord.Status)) {
                ticket.put('slaStatus', 'Resolved');
            }

            // Add audit information
            ticket.put('lastRetrievedAt', DateTime.now().format('yyyy-MM-dd\'T\'HH:mm:ss\'Z\''));
            ticket.put('retrievedBy', UserInfo.getName());
        }
    }

    // Return enriched response
    return constructedTMFResponse;

    // Output response:
    // {
    //   recordList: [{
    //     id: '00001027',
    //     name: 'Network Issue',
    //     status: 'Open',
    //     type: 'TroubleTicket',
    //     hoursOpen: 48,
    //     slaStatus: 'In Progress',
    //     lastRetrievedAt: '2024-01-17T10:30:00Z',
    //     retrievedBy: 'John Smith'
    //   }]
    // }
}


Full Implementation Example

Here's a complete sample Apex implementation using all supported hooks.

/**
 * Complete Trouble Ticket Management GET API Extension
 * Demonstrates all applicable lifecycle hooks with business logic
 */
public class TroubleTicketManagementGETExtension implements comms_apex_ext.ITroubleTicketManagementGET {

    /**
     * Hook 1: Transform request
     */
    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) {
            String idValue = (String)idObj;

            // If not a Salesforce ID, resolve CaseNumber to Id
            if (idValue.length() != 15 && idValue.length() != 18) {
                List<Case> cases = [
                    SELECT Id, CaseNumber
                    FROM Case
                    WHERE CaseNumber = :idValue
                    LIMIT 1
                ];

                if (!cases.isEmpty()) {
                    context.put('id', cases[0].Id);
                }
            }
        }

        return context;
    }

    /**
     * Hook 2: Apply custom validations
     */
    public Map<String, Object> applyCustomValidations(Map<String, Object> context) {
        String troubleTicketId = (String) context.get('id');
        String userId = (String) context.get('userId');

        Map<String, Object> validationResult = new Map<String, Object>();

        try {
            User currentUser = [SELECT Id, Profile.Name FROM User WHERE Id = :userId LIMIT 1];
            Case troubleTicket = [SELECT Id, Status, CaseNumber FROM Case WHERE CaseNumber = :troubleTicketId LIMIT 1];

            if ('Customer Service Rep'.equals(currentUser.Profile.Name)) {
                if ('Closed'.equals(troubleTicket.Status)) {
                    validationResult.put('validationStatus', 'FAIL');
                    validationResult.put('validationMessage', 'Access denied - closed tickets not accessible');
                    validationResult.put('validationDetails', new Map<String, Object>{
                        'denialReason' => 'STATUS_RESTRICTION'
                    });
                } else {
                    validationResult.put('validationStatus', 'PASS');
                    validationResult.put('validationMessage', 'User has access to this trouble ticket');
                }
            } else {
                validationResult.put('validationStatus', 'PASS');
                validationResult.put('validationMessage', 'User profile has unrestricted access');
            }
        } catch (Exception e) {
            validationResult.put('validationStatus', 'FAIL');
            validationResult.put('validationMessage', 'Validation error: ' + e.getMessage());
        }

        return validationResult;
    }

    /**
     * Hook 3: Customize GraphQL query
     * Uses transformation syntax from GRAPHQL_QUERY_TRANSFORMATION.md
     */
    public Map<String, Object> customiseGraphQLQuery(
        Map<String, Object> graphQLAsMap,
        Map<String, Object> context
    ) {
        List<Map<String, Object>> transformationNodes = new List<Map<String, Object>>();

        // Add custom fields to the Case node
        // Path: troubleTicket (the Case alias in the GraphQL query)
        transformationNodes.add(new Map<String, Object>{
            'path' => 'troubleTicket',
            'addFields' => new List<String>{'IsClosed', 'SlaStartDate', 'SlaExitDate'}
        });

        // Add filters to the Case query
        // Only retrieve open trouble tickets
        transformationNodes.add(new Map<String, Object>{
            'path' => 'troubleTicket',
            'addFilters' => new Map<String, Object>{
                'clearExistingFilters' => false,
                'filterGraphQlSnippet' => '{ Status: { eq: "Open" } }'
            }
        });

        return new Map<String, Object>{ 'nodes' => transformationNodes };
    }

    /**
     * Hook 4: Post-process response
     */
    public Map<String, Object> handlePostOperation(
        Map<String, Object> graphQLQueryResultAsMap,
        Map<String, Object> constructedTMFResponse,
        Map<String, Object> context
    ) {
        List<Object> troubleTicketList = (List<Object>) constructedTMFResponse.get('recordList');

        if (troubleTicketList != null) {
            for (Object ticketObj : troubleTicketList) {
                Map<String, Object> ticket = (Map<String, Object>) ticketObj;
                String ticketId = (String) ticket.get('id');

                try {
                    Case caseRecord = [SELECT Id, CaseNumber, CreatedDate, Status FROM Case WHERE CaseNumber = :ticketId LIMIT 1];

                    if (caseRecord.CreatedDate != null) {
                        DateTime createdDate = caseRecord.CreatedDate;
                        DateTime now = DateTime.now();
                        Long hoursOpen = (now.getTime() - createdDate.getTime()) / (1000 * 60 * 60);
                        ticket.put('hoursOpen', hoursOpen);
                    }

                    if ('Open'.equals(caseRecord.Status)) {
                        ticket.put('slaStatus', 'In Progress');
                    } else if ('Closed'.equals(caseRecord.Status)) {
                        ticket.put('slaStatus', 'Resolved');
                    }
                } catch (Exception e) {
                    System.debug('Error enriching trouble ticket data: ' + e.getMessage());
                }
            }
        }

        return constructedTMFResponse;
    }

    /**
     * NOT APPLICABLE: configureDefaultValidations is not used for Trouble Ticket Management API
     * If implemented, this method will be invoked but return values will be ignored.
     */
    public Map<String, Boolean> configureDefaultValidations(
        Map<String, Boolean> defaultValidationConfiguration,
        Map<String, Object> context
    ) {
        // This hook is not applicable for Trouble Ticket Management API
        // Return the configuration unchanged
        return defaultValidationConfiguration;
    }
}


Response Structure

Single Trouble Ticket Response (GraphQL)

{
  "id": "00001027",
  "name": "Network connectivity issue",
  "type": "TroubleTicket",
  "href": "/services/data/v67.0/connect/comms/troubleticket/v5/troubleticket/00001027",
  "description": "Users reporting intermittent network connectivity",
  "status": "Open",
  "ticketType": "Network",
  "priority": "High",
  "creationDate": "2024-01-15T10:00:00Z",
  "lastUpdate": "2024-01-16T14:30:00Z",
  "resolutionDate": null,
  "note": [
    {
      "id": "00a5f000001ABC",
      "text": "Initial investigation started",
      "date": "2024-01-15T10:15:00Z"
    }
  ],
  "relatedParty": [
    {
      "id": "001xx000003ABC",
      "name": "Acme Corporation",
      "role": "Customer",
      "href": "/services/data/v67.0/sobjects/Account/001xx000003ABC"
    }
  ]
}


List Response (GraphQL)

{
  "recordList": [
    {
      "id": "00001027",
      "name": "Network connectivity issue",
      "type": "TroubleTicket",
      "href": "/services/data/v67.0/connect/comms/troubleticket/v5/troubleticket/00001027",
      "status": "Open"
    },
    {
      "id": "00001028",
      "name": "Server performance degradation",
      "type": "TroubleTicket",
      "href": "/services/data/v67.0/connect/comms/troubleticket/v5/troubleticket/00001028",
      "status": "Open"
    }
  ],
  "pageInfo": {
    "startCursor": "YXJyYXljb25uZWN0aW9uOjA=",
    "endCursor": "YXJyYXljb25uZWN0aW9uOjE="
  }
}


GraphQL Query Transformations

Query transformations provide the ability to enhance GraphQL queries by adding fields, applying filters, or modifying the sort order, while preserving the original query definition.

note: For the Customer Management API, use the uiapi.query.Account path to reference the primary Account field.

Query Enhancements: Fields and Filters

Original Query (GraphQL):

query {
myAccounts: accounts {
id
name
}
}

Transformation Instructions (JSON):

{
"nodes": [
{
"path": "myAccounts",
"addFields": ["email", "phone"],
"addFilters": {
"clearExistingFilters": true,
"filterGraphQlSnippet": "{ status: { eq: \"ACTIVE\" } }"
}
}
]
}

Result (GraphQL):

query {
myAccounts: accounts(where: { status: { eq: "ACTIVE" } }) {
id
name
email
phone
}
}

Query Enhancements: Fields, Filters, and Ordering

Original Query (GraphQL):

query {
myAccounts: accounts {
id
name
}
}

Transformation Instructions (JSON):

{
"nodes": [
{
"path": "myAccounts",
"addFields": ["revenue", "industry"],
"addFilters": {
"clearExistingFilters": true,
"filterGraphQlSnippet": "{
and: [
{ status: { eq: \"ACTIVE\" } },
{ revenue: { gte: 1000000 } }
]
}"
},
"orderBy": {
"clearExistingOrderBy": true,
"orderGraphQlSnippet": "{ revenue: { order: DESC } }"
}
}
]
}

Result (GraphQL):

query {
myAccounts: accounts(
where: {
and: [
{ status: { eq: "ACTIVE" } },
{ revenue: { gte: 1000000 } }
]
},
orderBy: { revenue: { order: DESC } }
) {
id
name
revenue
industry
}
}

Query Enhancements: Multiple Query Paths with Different Transformations

Original Query (GraphQL):

query {
myAccounts: accounts {
id
name
myContacts: contacts {
id
name
}
}
}

Transformation Instructions (JSON):

{
"nodes": [
{
"path": "myAccounts",
"addFields": ["email"],
"addFilters": {
"clearExistingFilters": true,
"filterGraphQlSnippet": "{ status: { eq: \"ACTIVE\" } }"
}
},
{
"path": "myAccounts.myContacts",
"addFields": ["email", "phone"],
"orderBy": {
"clearExistingOrderBy": true,
"orderGraphQlSnippet": "{ name: { order: ASC } }"
}
}
]
}

Result (GraphQL):

query {
myAccounts: accounts(where: { status: { eq: "ACTIVE" } }) {
id
name
email
myContacts: contacts(orderBy: { name: { order: ASC } }) {
id
name
email
phone
}
}
}

Query Enhancements: Fields and Snippets

Original Query (GraphQL):

query {
myAccounts: accounts {
id
name
}
}

Transformation Instructions (JSON):

{
"nodes": [
{
"path": "myAccounts",
"addFields": ["email"]
},
{
"path": "metrics",
"insertGraphQlSnippet": "{
metrics: systemMetrics {
totalCount
activeCount
lastSyncTime
}
}"
}
]
}

Result (GraphQL):

query {
myAccounts: accounts {
id
name
email
}
metrics: systemMetrics {
totalCount
activeCount
lastSyncTime
}
}