Skip to content

[Due for payment 2026-05-26] [HR Import] Add MERGE_HR consts, types, and provider map [F01] #90594

Description

@francoisl

Note

Updated 2026-05-18 — the merge.dev naming convention was pivoted from per-provider (merge_hr_workday, merge_hr_hibob, …) to per-category (merge_hris), with the specific provider stored as config.integration on the connection. The agreed shape is mirrored in the matching backend PRs: Auth#21676 and WE#52929. The design doc (Detailed Implementation > Release 1 > Frontend changes > Consts & Type definitions) is out of date on this point; the spec below supersedes it.

Task

Lay the foundation for all other frontend work: connection name constant, the shared config type, and the local provider map.

The agreed policy JSON shape:

{
  "connections": {
    "merge_hris": {
      "config": {
        "integration": "workday",
        "approvalMode": "APPROVAL_ADVANCED",
        "finalApprover": "alice@acme.com"
      }
    }
  }
}

One connection per category, regardless of how many merge.dev providers we eventually ship — matches the existing "one integration per category" constraint we already enforce for accounting integrations.

Files:

  • src/CONST/index.ts — add a single MERGE_HRIS entry to CONST.POLICY.CONNECTIONS.NAME and to HR_CONNECTION_NAMES. Add one entry to CONST.POLICY.CONNECTIONS.ROUTE and to NAME_USER_FRIENDLY ('Merge HR' is fine — the per-provider display name comes from config.integration via MERGE_HR_PROVIDERS, not from this map). Also add a separate CONST.MERGE_HR.APPROVAL_MODE namespace mirroring CONST.GUSTO.APPROVAL_MODE (basic, manager, custom) — kept separate from Gusto's so each integration can diverge if/when its supported modes change.

  • src/types/onyx/Policy.ts — add the shared MergeHRConnectionConfig type and extend Connections:

    /** Merge HR connection data — empty (no data lives outside config today). */
    type MergeHRConnectionData = Record<string, never>;
    
    /** Shared base for HR integrations (Gusto, Merge HR). Pull `finalApprover` + offline-feedback keys
     *  out of the existing GustoConnectionConfig so both extend a common base. */
    type HRConnectionConfigBase = OnyxCommon.OnyxValueWithOfflineFeedback<
        {
            finalApprover: string | null;
            // ... other shared fields ...
        },
        'approvalMode' | 'finalApprover'
    >;
    
    type MergeHRConnectionConfig = HRConnectionConfigBase & {
        /** Approval mode — uses its own enum, kept separate from Gusto's to allow divergence. */
        approvalMode: ValueOf<typeof CONST.MERGE_HR.APPROVAL_MODE> | null;
    
        /** Merge integration slug identifying the connected HR system. Strongly typed to the keys
         *  of MERGE_HR_PROVIDERS so adding a provider lights up the union automatically. */
        integration: keyof typeof MERGE_HR_PROVIDERS;
    };
    
    type Connections = {
        // ... existing entries (gusto, zenefits, qbo, etc.) ...
        [CONST.POLICY.CONNECTIONS.NAME.MERGE_HRIS]: Connection<MergeHRConnectionData, MergeHRConnectionConfig>;
    };

    Refactor GustoConnectionConfig to extend the same HRConnectionConfigBase (small cleanup that drops duplication and makes the parallel shape explicit).

  • src/CONST/MERGE_HR_PROVIDERS.ts — the single source of truth for display info about each supported Merge HRIS provider (slug → { displayName, iconUrl }). The connection name itself is no longer per-provider, so this map is purely a display lookup:

    const MERGE_HR_PROVIDERS = {
        workday:  { displayName: 'Workday',  iconUrl: '<from Merge Metadata API>' },
        bamboohr: { displayName: 'BambooHR', iconUrl: '<from Merge Metadata API>' },
        hibob:    { displayName: 'HiBob',    iconUrl: '<from Merge Metadata API>' },
    } as const satisfies Record<string, { displayName: string; iconUrl: string }>;
    
    export default MERGE_HR_PROVIDERS;

    Resolving a connected policy to its provider becomes a one-liner: MERGE_HR_PROVIDERS[policy.connections.merge_hris?.config?.integration].

iconUrl values are fetched once at design time from Merge's Integration Metadata API and committed here. The images themselves load from Merge's CDN at render time — we don't bundle them. The CheckMergeHRIntegrations Bedrock job (B13) will catch any drift from Merge's published metadata.

What's intentionally out of scope

  • Multiple Merge connections per policy. One merge_hris per workspace at a time, parallel to the existing accounting-integration constraint. If a workspace ever wants e.g. Workday for US employees + HiBob for UK employees, that's a future enhancement.
  • Other Merge categories (ATS, accounting). When they arrive, they get their own connection name (merge_ats, merge_accounting) and a parallel MERGE_ATS_PROVIDERS.ts map. The convention this issue establishes scales by adding sibling files, not by extending this one.

Open question for the implementer

Per-provider routes (/workspaces/$id/hr/workday, /hr/hibob, …) vs. a single category-level route (/hr/merge-hris)? Better deeplinking + back-button UX in the per-provider case, but more routes for one underlying connection. Worth confirming with @dannymcclain / @twisterdotcom before implementing. The existing Gusto pattern uses the connection name as the route slug, which would translate to a single /hr/merge-hris here — the simplest answer.

Dependencies

None. Blocks most other frontend issues (F02, F03, F05, F06, F08), so should land first.

Issue OwnerCurrent Issue Owner: @mhawryluk

Metadata

Metadata

Labels

DailyKSv2InternalRequires API changes or must be handled by Expensify staff

Type

Fields

No fields configured for Task.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions