ADP — Flows
How each path actually runs. See architecture for the surface and data model.
Subscription lifecycle
SUBSCRIPTION_ORDER carries the Pivot Company ID from the checkout custom field and the organizationOID. The handler requires the company to exist — it returns 404 otherwise, because provisioning a new tenant is not built.
It then calls credentials.read. If the client has not consented yet, ADP returns 200 with an empty body, the handler stores clientId/clientSecret as null, and RTDB deletes those keys — leaving a record with only organizationOID.
SUBSCRIPTION_CANCEL calls deleteForCompany, which nulls AdpSettings/{companyId}, EmployeeIntegrationIds/{companyId}/adp and the two CompanySettings flags.
Consent recovery
Because consent normally arrives after the order, /get-credentials-after-consent re-runs credentials.read and, on success, stores the credentials and sets hasAcceptedADPConsent: true. It fetches consent, it cannot grant it — without approval in ADP it throws ADP consent has not been granted yet.
This is the only code path that sets hasAcceptedADPConsent true outside the order event.
Employee sync
Writing AdpSettings/{companyId}/payrollGroupCode fires onInitAdpIntegration. The guard is if (before || !after) return, so it runs only on absent → present — changing an existing code does not resync.
The sync pulls /hr/v2/workers, keeps workers with a primary active assignment in the selected payroll group, and calls matchAndStoreEmployees. That matcher compares ${name} ${surname} against the ADP worker's full name, skips employees that already have a mapping, and only ever adds — it never removes or re-verifies.
syncAdpEmployeesCronJob repeats the pull daily at 05:00 America/Toronto.
Two failure modes worth knowing: onInitAdpIntegration catches and logs without rethrowing, so a failed initial sync looks identical to a successful one; and Adding N new integration IDs versus No new integration IDs to add in the logs is the only signal of whether anything was linked.
Single sign-on
ADP redirects to /oauth/callback/adp on the Pivot web host, and the frontend posts the code to /authenticate-sso. The handler exchanges it, calls /core/v1/userinfo, then resolves the person in two hops: organizationOID → company via AdpSettings, and associateOID → employee via EmployeeIntegrationIds.
Access therefore depends on the employee sync, not on ADP assignment. No matching mapping means forbidden, regardless of what the client assigned in the Marketplace.
On success it reuses or creates a Firebase user, links it to the employee, stamps providers: ['adp'] plus the ADP identifiers on Users/{uid}, and mints a custom token.
Payroll export
/export builds a payload from approved attendance, employee rates and payroll settings, then POSTs /events/payroll/v1/pay-data-input.modify. Each employee is identified by associateOID and payrollFileNumber, so a mapping missing the file number drops that employee from the export.
Payroll group codes come from /events/payroll/v1/pay-data-input.modify/meta — the stored value is the codeValue (e.g. A8L) while ADP's own UI generally shows the shortName.
User assignment
USER_ASSIGNMENT and USER_UNASSIGNMENT are recorded under AdpSettings/{companyId}/assignedUsers/{associateOID} and change nothing else. They neither grant nor revoke access, so an assigned admin who is not a synced payroll worker still cannot sign in, and unassigning someone does not remove their access.