Professional Services Automation Apex API Developer Reference

pse.TimecardMatchService

global with sharing class TimecardMatchService

Global service for matching unassigned timecards to eligible assignments.
Each timecard is matched to an assignment on the same project. When multiple candidates exist, the one with the greatest date overlap is selected. Each assignment can be matched to multiple timecards.
An assignment is eligible for matching only if:
- Closed_for_Time_Entry__c is unchecked
- Status__c is one of the values configured in the Assignments_load_status_values__c field of the Timecard Entry UI Global custom setting (defaults to Tentative and Scheduled)

Methods

matchTimecards

global static pse.TimecardMatchService.Result matchTimecards(List<Id> projectIds)

Matches unassigned timecards in the given projects to active assignments using default options.

Input Parameters

Name Type Description
projectIds List<Id> IDs of the projects to match timecards and assignments for.

Return Value

Result with matched pairs and unmatched timecard IDs.

matchTimecards

global static pse.TimecardMatchService.Result matchTimecards(List<Id> projectIds, pse.TimecardMatchService.Options matchOptions)

Matches unassigned timecards in the given projects to active assignments.

Input Parameters

Name Type Description
projectIds List<Id> IDs of the projects to match timecards and assignments for.
matchOptions pse.TimecardMatchService.Options Configuration including status exclusions and date extension behavior

Return Value

Result with matched pairs and unmatched timecard IDs

Sample Code

//Note: This sample code is for demonstration purposes only. It is not intended for
//use in a production environment, is not guaranteed against defects or errors, and
//is in no way optimized or streamlined.

// Acme Corp imports timecards each night from an external time-tracking tool.
// Entries arrive without an assignment, so we match them after import.
List<Id> projectIds = new List<Id>();
for (pse__Proj__c project : [
    SELECT Id
    FROM pse__Proj__c
    WHERE pse__Stage__c = 'In Progress'
]) {
    projectIds.add(project.Id);
}

pse.TimecardMatchService.Options options = new pse.TimecardMatchService.Options();
options.shouldExtendAssignmentDates = true;

pse.TimecardMatchService.Result result = pse.TimecardMatchService.matchTimecards(
    projectIds,
    options
);

for (Id timecardId : result.unmatchedTimecardIds) {
    // No active assignment exists for this resource and project combination.
    // Someone will need to create one before the timecard can be linked.
    System.debug(LoggingLevel.WARN, 'No assignment found for timecard ' + timecardId);
}

for (Id timecardId : result.errorByTimecardId.keySet()) {
    // A match was found but the update failed — check whether the running user
    // has timecard entry permission for this resource.
    System.debug(
        LoggingLevel.ERROR,
        'Failed to link timecard ' + timecardId + ': ' + result.errorByTimecardId.get(timecardId)
    );
}

matchTimecardsAsync

global static Id matchTimecardsAsync(List<Id> projectIds)

Enqueues async matching jobs for the given projects using default options. Jobs are grouped under a single fferpcore__AsyncJobGroup__c — one fferpcore__AsyncJobRecord__c per project.

Input Parameters

Name Type Description
projectIds List<Id> IDs of the projects to match timecards and assignments for.

Return Value

ID of the root fferpcore__AsyncJobGroup__c for this operation.

matchTimecardsAsync

global static Id matchTimecardsAsync(List<Id> projectIds, pse.TimecardMatchService.Options matchOptions)

Enqueues async matching jobs for the given projects. One job is created per project. A notification is sent to the calling user when all matching jobs complete.

Input Parameters

Name Type Description
projectIds List<Id> IDs of the projects to match timecards and assignments for.
matchOptions pse.TimecardMatchService.Options Configuration including status exclusions and date extension behavior.

Return Value

ID of the root job group. To monitor individual matching jobs, query fferpcore__AsyncJobRecord__c where fferpcore__AsyncJobGroup__r.fferpcore__RootAsyncJobGroup__c equals this ID and fferpcore__CallableName__c equals TimecardMatchCallable.

Sample Code

//Note: This sample code is for demonstration purposes only. It is not intended for
//use in a production environment, is not guaranteed against defects or errors, and
//is in no way optimized or streamlined.

// Acme Corp runs nightly maintenance to match any timecards that arrived without
// an assignment. The sync version can hit governor limits on large orgs, so they
// use the async overload instead, which enqueues one background job per project.
List<Id> projectIds = new List<Id>();
for (pse__Proj__c project : [
    SELECT Id
    FROM pse__Proj__c
    WHERE pse__Stage__c = 'In Progress'
]) {
    projectIds.add(project.Id);
}

pse.TimecardMatchService.Options options = new pse.TimecardMatchService.Options();
options.shouldExtendAssignmentDates = true;

Id jobGroupId = pse.TimecardMatchService.matchTimecardsAsync(projectIds, options);

// matchTimecardsAsync returns immediately — the jobs run in the background.
// Store jobGroupId (to the caller) so you can check progress later. In practice
// you would poll after a delay.

// To check whether all jobs in the group have finished, query the job records:
List<fferpcore__AsyncJobRecord__c> jobs = [
    SELECT
        fferpcore__Status__c,
        fferpcore__Complete__c,
        fferpcore__Succeeded__c,
        fferpcore__Failed__c,
        fferpcore__Error__c
    FROM fferpcore__AsyncJobRecord__c
    WHERE fferpcore__AsyncJobGroup__c = :jobGroupId
];

Integer pending = 0;
Integer failed = 0;
for (fferpcore__AsyncJobRecord__c job : jobs) {
    if (!job.fferpcore__Complete__c) {
        pending++;
    } else if (job.fferpcore__Failed__c) {
        failed++;
        System.debug(LoggingLevel.ERROR, 'Matching job failed: ' + job.fferpcore__Error__c);
    }
}

if (pending > 0) {
    System.debug(LoggingLevel.INFO, pending + ' job(s) still running — check back later.');
} else if (failed > 0) {
    System.debug(LoggingLevel.WARN, failed + ' job(s) failed. See error details above.');
} else {
    System.debug(LoggingLevel.INFO, 'All matching jobs completed successfully.');
}

pse.TimecardMatchService.Options

global with sharing class Options

Configuration options for a matching run.

Properties

Name Type Description
excludedTimecardStatuses Set<String> Timecard statuses excluded from matching. Defaults to 'Cancelled'.
shouldExtendAssignmentDates Boolean When true, the service extends assignment schedule dates to cover any matched timecard that falls outside the assignment's current date range.

Methods

Options

global Options()

pse.TimecardMatchService.Result

global with sharing class Result

Result returned after a matching run.

Properties

Name Type Description
assignmentIdByTimecardId Map<Id, Id> Timecard ID → matched Assignment ID for each successfully matched timecard.
unmatchedTimecardIds List<Id> IDs of timecards that could not be matched to an assignment.
errorByTimecardId Map<Id, String> Timecard ID → error message for timecards that failed to save during matching.
newStartDateByAssignmentId Map<Id, Date> Assignment ID → new required start date, populated when a matched timecard starts before its assignment and Options.shouldExtendAssignmentDates is true.
newEndDateByAssignmentId Map<Id, Date> Assignment ID → new required end date, populated when a matched timecard ends after its assignment and Options.shouldExtendAssignmentDates is true.
© Copyright 2009–2026 Certinia Inc. All rights reserved. Various trademarks held by their respective owners.