feat(gp): Global payroll demo flow#1077
Conversation
…app (PBYR-4044) - Add api.ts with useGPFormSchema, useGPCreateEmployment, useGPUpdateContractDetails, useGPUpdateAdministrativeDetails, useGPInviteEmployee (all via mutateAsyncOrThrow) - Expand usePayrollAdminOnboarding hook with step-aware form schemas, mutations, handleValidation, parseFormValues, onSubmit, and sendInvite - Implement SelectCountryStep, ContractDetailsStep, AdministrativeDetailsStep, InvitationStep, SubmitButton, BackButton, and PayrollAdminForm components - Extract useStepSubmitHandler to eliminate duplicated try/catch/navigate logic across form-based step components - Wire real components into PayrollAdminOnboardingFlow (replaces scaffolding stubs) - Add PayrollAdminOnboardingForm demo page to the example app Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses Jordi's review comment on PR #1076 — aligns with the pattern used in the other flow contexts instead of asserting a redefined type. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ede84ee to
a458469
Compare
📦 Bundle Size Report
Size Limits
Largest Files (Top 5)
View All Files (378 total)
✅ Bundle size check passed |
📊 Coverage Report
|
| Metric | Current | Previous | Change | Status |
|---|---|---|---|---|
| Lines | 83.76% | 88.35% | -4.59% | 🔴 |
| Statements | 83.19% | 87.75% | -4.56% | 🔴 |
| Functions | 81.75% | 84.77% | -3.01% | 🔴 |
| Branches | 75.59% | 79.64% | -4.05% | 🔴 |
Detailed Breakdown
Lines Coverage
- Covered: 3863 / 4612
- Coverage: 83.76%
- Change: -4.59% (50 lines)
Statements Coverage
- Covered: 3928 / 4722
- Coverage: 83.19%
- Change: -4.56% (53 statements)
Functions Coverage
- Covered: 1044 / 1277
- Coverage: 81.75%
- Change: -3.01% (9 functions)
Branches Coverage
- Covered: 2403 / 3179
- Coverage: 75.59%
- Change: -4.05% (25 branches)
✅ Coverage check passed
|
Deploy preview for remote-flows ready!
Deployed with vercel-action |
The new flow brings total raw size to ~614 kB, exceeding the prior 600 kB cap. Following the existing pattern of 50 kB bumps when new flows ship. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gp): PayrollEmployeeOnboardingFlow — steps, mutations, and example app (PBYR-4045) - Add security field to putV1EmployeePersonalDetails, putV1EmployeeAddress, putV1EmployeeBankAccount in sdk.gen.ts so hey-api injects the Bearer token - Export getV1EmployeeBankAccount and putV1EmployeeBankAccount from client/index.ts - Extract GPStepCallbacks to src/flows/types.ts; both admin and employee flows re-export as GPAdminStepCallbacks / GPEmployeeStepCallbacks for compat - Add api.ts: useGPEmployeeFormSchema, useGPUpdatePersonalDetails, useGPUpdateHomeAddress, useGPUpdateBankAccount (all via mutateAsyncOrThrow) - Expand usePayrollEmployeeOnboarding with step-aware schemas, mutations, handleValidation, parseFormValues, onSubmit; add required countryCode prop - Implement PersonalDetailsStep, HomeAddressStep, BankAccountStep (self-guards when substep not required), SubmitButton, BackButton, PayrollEmployeeForm - Extract useEmployeeStepSubmitHandler mirroring the admin pattern - Export useGPOnboardingSteps from public SDK surface - Date picker: add captionLayout="dropdown" with year range from field constraints; add CSS for rdp caption_dropdowns in global.css - Example app: PayrollEmployeeOnboardingForm with employment ID input, two-context auth (company manager for step status, employee assertion for mutations) - Example backend: GET /api/fetch-employee-token/:employmentId via JWT assertion grant (sub: urn:remote-api:employee:employment:<id>) - Proxy: pass-through token type for /v1/employee/* preserves employee Bearer token instead of overwriting with company manager token Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(gp): personal_details field fixes and jsfModify stability (PBYR-4045) Strip read-only name field and clarify mobile_number format: - Strip 'name' from PUT personal_details payload (additionalProperties: false on the PUT endpoint rejects it — 422 "is not accepted") - Hide 'name' via jsfModify so it is never rendered in the employee form - Add description to mobile_number: 10 digits only, no + or country code (USA) - Add jsfModify param to useGPEmployeeFormSchema for per-schema field overrides Extract hook calls to variables in PersonalDetailsStep and HomeAddressStep: - Avoids calling useEmployeeStepSubmitHandler inline inside JSX prop expression Hoist PERSONAL_DETAILS_JSF_MODIFY to module constant: - The inline object literal was a new reference every render, causing createHeadlessForm to be called on every keystroke (select closure captures jsfModify; React Query re-runs select when its function reference changes). A stable module constant eliminates the unnecessary re-computation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ing (PBYR-4047) (#1080) * feat(gp): PayrollEmployeeFederalTaxesFlow — W-4 step for USA employees (PBYR-4047) - Add new src/flows/PayrollEmployeeFederalTaxes/ flow: hooks, api, context, types, PayrollEmployeeFederalTaxesFlow render-prop component - usePayrollEmployeeFederalTaxes probes /v1/employments/:id/onboarding-steps to derive isActive (completion.sub_steps[0].status === 'completed') and exposes isAvailable + unavailableReason ('unsupported_country' | 'pending_enrollment') - Schema fetch (global_payroll_federal_taxes) is gated on countryCode === 'USA' && active; mutation hits PUT /v1/employee/federal-taxes (employee assertion token) - Components: FederalTaxesStep (returns null when unavailable), SubmitButton, PayrollEmployeeFederalTaxesForm, useFederalTaxesStepSubmitHandler - Add security: [{ scheme: 'bearer', type: 'http' }] to putV1EmployeeFederalTaxes in sdk.gen.ts so hey-api injects the Bearer token (same fix PBYR-4045 applied to personal-details/address/bank-account) - Export PayrollEmployeeFederalTaxesFlow, usePayrollEmployeeFederalTaxes, and related types from public SDK surface - Example app: PayrollEmployeeFederalTaxes demo with employment ID entry and not-available state for non-USA / pre-active employments Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(gp): integrate federal_taxes + state_taxes into PayrollEmployeeOnboarding (PBYR-4047) Replace the standalone PayrollEmployeeFederalTaxesFlow with two new steps (federal_taxes, state_taxes) inside the existing PayrollEmployeeOnboarding flow, matching the merged-flow shape requested for the demo. - Delete src/flows/PayrollEmployeeFederalTaxes/ and its exports/example demo - Extend EmployeeStepKey with 'federal_taxes' and 'state_taxes' - Add jurisdiction?: string prop (US state code) — required for state_taxes - Add useGPUpdateFederalTaxes, useGPUpdateStateTaxes; extend GPEmployeeSchemaType with global_payroll_federal_taxes / global_payroll_state_taxes - Expose employeeBag.taxStepsAvailability keyed by step: { isAvailable, unavailableReason: 'unsupported_country' | 'pending_enrollment' | 'no_jurisdiction' } - Gating model: USA-only + onboarding completion-step probe + retroactive 404 catch. On submit, if Tiger returns 404 (`Tax task not found...`), flip the step's reason to 'pending_enrollment' so the consumer renders the not-available UI instead of a raw API error - New FederalTaxesStep / StateTaxesStep components that return null when the step is unavailable; consumer drives the not-available UI from the bag - Hand-add putV1EmployeeStateTaxes (with security: bearer) + EmploymentStateTaxesParams to sdk.gen.ts / types.gen.ts (prod gateway doesn't expose this endpoint yet, same hand-edit convention used in PBYR-4045) - Re-export TaxStepUnavailableReason + GPEmployeeStepCallbacks from public SDK - Example app: extend GP Employee Onboarding demo with both new steps + a TaxStepNotAvailable banner; jurisdiction from VITE_GP_STATE_JURISDICTION (default 'CA') Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(gp): surface schema_unavailable on tax steps when backend has no form schema The local Tiger gateway exposes `PUT /v1/employee/state-taxes/{jurisdiction}` but returns 400 from `GET /v1/countries/USA/global_payroll_state_taxes`, so the tax form was rendering with zero fields. Fold the schema query's error state into `taxStepsAvailability` so the bag exposes `unavailableReason: 'schema_unavailable'` and the consumer renders the not-available UI instead of a blank form. - Extend TaxStepUnavailableReason with 'schema_unavailable' - Compute taxStepsAvailability AFTER the schema queries so federalTaxesSchema.isError / stateTaxesSchema.isError can feed in; gate the schema queries on country + jurisdiction + post-enrollment (not on availability itself, to avoid a cycle) - Update example app's TaxStepNotAvailable banner with the new reason text Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(gp): derive countryCode and jurisdiction from the employment in the demo Instead of reading VITE_GP_COUNTRY_CODE / VITE_GP_STATE_JURISDICTION env vars (footgun: a USA env + DEU employment would silently submit to the wrong shape), fetch the employment in the outer (company-manager) context alongside useGPOnboardingSteps and hand country.code + work/home address state down to the inner (employee-token) context as props. - New useEmployeeFlowContext combines useGPOnboardingSteps + useEmploymentQuery, returns substeps/hasBankAccount/countryCode/jurisdiction/isLoading - Jurisdiction prefers work_address_details.state, falls back to address_details.state - EmployeeFlowInner now takes countryCode + jurisdiction as required props - Render an error card when the employment can't be loaded (e.g. bad ID or token without access) instead of running the flow with undefined country Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The PEO components landed without unit tests in #1078/#1080, dropping branch coverage to 74.99% (threshold 75%) and failing CI on the parent PR. Add a single tests/components.test.tsx that covers every render-prop component and the submit handler with mocked context, exercising the key branches: - SubmitButton: forwards form id, disabled-on-isSubmitting, no-button throws - BackButton: goToPreviousStep + consumer onClick chain, no-button throws - PersonalDetailsStep / HomeAddressStep: pass step-keyed initialValues - BankAccountStep: returns null when bank substep not in selfOnboardingSubsteps - FederalTaxesStep / StateTaxesStep: returns null when taxStepsAvailability.*.isAvailable === false - useEmployeeStepSubmitHandler: success path advances, MutationError path routes through onError without advancing, non-MutationError falls back to plain shape Branches: 74.99% → 75.38%. PayrollEmployeeForm is stubbed since its react-hook-form wiring isn't what we're exercising here; behaviour-level tests for the form itself belong in a follow-up flow integration test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #1078 changed the example app's proxy to route GET /v[12]/countries* to the user-token, which broke E2E tests in CI: the user token doesn't have access to the countries list, so the country dropdown never populates and selectOption('#country') times out. The original behaviour (pre-1078) used client_credentials for both GET /v[12]/countries$ and GET /v[12]/countries/:country_code/address_details since neither endpoint requires a user identity — they're public reference data. Restore that, leaving the schema endpoint GET /v[12]/countries/:country_code/:form on user-token where it belongs. Unblocks the CostCalculator E2E specs (add-estimation, annual-gross-salary, edit-estimation, hiring-budget) on the PR #1077 stack. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
gabrielseco
left a comment
There was a problem hiding this comment.
there are too many comments
| onSuccess={clearErrors} | ||
| /> | ||
| <AlertError errors={errors} /> | ||
| {adminBag.countryCode && adminBag.fields.length > 0 && ( |
There was a problem hiding this comment.
these ifs are strange, we don't have these for other demos why?
There was a problem hiding this comment.
These guards exist because select_country rolls two actions into one step: (1) pick a country, (2) fill the country-specific basic-information form and submit to create the employment. Until both countryCode is set AND the schema's fields have loaded, there's nothing to submit. In aa456a3 I extracted it as const isFormReady = !!adminBag.countryCode && adminBag.fields.length > 0 so the intent reads better. Splitting select_country into two steps (like Onboarding does) would let us drop the guard entirely — happy to do that in a follow-up MR if you'd prefer that shape.
PR #1078 regenerated src/client/sdk.gen.ts against the local gateway OpenAPI spec, which is missing `security:` declarations on most endpoints. Hey-api's client gates auth injection on that field (src/client/client/client.gen.ts: `if (opts.security) { setAuthParams }`), so 234 SDK functions silently dropped their Bearer token — including getV1CostCalculatorCountries. The CostCalculator country dropdown never populated, timing out the four cost-calc E2E specs (add-estimation, annual-gross-salary, edit-estimation, hiring-budget). Regenerate from the production gateway (npm run openapi-ts), which restores security on every protected endpoint (263 declarations vs 20 on the broken branch). One naming ripple: production exposes the state-taxes mutation as putV1EmployeeStateTaxesJurisdiction (same URL, /v1/employee/state-taxes/{jurisdiction}); update the import + call in PayrollEmployeeOnboarding/api.ts to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Deploy preview for adp-cost-calculator ready!
Deployed with vercel-action |
- Drop PBYR codes from demo descriptions - Convert if-chain to switch in PayrollAdminOnboarding demo, factor a shared onStepError helper - Reuse useStepState's fieldValues/setFieldValues instead of declaring a duplicate state in admin + employee hooks - Convert step→schema lookups to object maps (admin + employee) - Flatten nested ternaries in taxStepsAvailability to early-return helpers - Rename goToNextStep/goToPreviousStep to next/back to match other flows' hook surface - Inline the invite mutation directly in InvitationStep, drop the sendInvite wrapper from the admin hook - Generic-ize GPStepCallbacks<TSuccess = unknown> so each step can narrow its onSuccess payload - Skip federal_taxes / state_taxes / bank_account steps at the navigation layer when the prereqs aren't met (visible: false on the step), instead of relying solely on the component returning null after the user has already landed there Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related changes so the FE no longer holds a Bearer token for the Global Payroll demos: 1. Gate the JWT auth endpoints to non-production. /api/fetch-company-manager and /api/fetch-employee-token/:id now 403 when NODE_ENV=production, matching the existing gate on /api/fetch-refresh-token. 2. Mint employee assertion tokens in the proxy. The proxy now picks `employee-assertion` for /v1/employee/* and signs the JWT-bearer token using the x-rf-employment-id header the FE forwards via the SDK's `proxy.headers`. The FE never sees the token. The GP Admin and GP Employee demos now both pass `authType='none'` to skip the FE-side auth callback entirely — the proxy is the only thing that ever holds a Bearer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The personal_details schema's mobile_number is shaped differently per country: - USA: a flat 10-digit string with `inputType: 'text'`. - Everywhere else: an `anyOf` of per-country dial-code patterns. PR #1078 hardcoded a USA-flavoured override on every render ("Enter 10 digits, no country code…") which silently nudges non-USA users toward a value that fails the schema's anyOf validation, so submits 422 server-side with no client-side hint. Split the jsfModify into two variants and pick by countryCode: - USA keeps the helpful description. - Everything else forces `inputType: 'tel'` so TelField renders a country picker + national-number input. Verified against the AUS schema in the local gateway — the picker hydrates with 244 countries and the form submits a properly-prefixed `+61…` value. Also surface the actual `warnings` array returned by jsf-kit's `modify()` and pass `muteLogging: true` so we don't spam the console with the generic "we recommend handling warnings" notice on every form mount. Net result: no warnings on a clean render. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The OpenAPI spec marks the `employment_id` query param on
GET /v1/countries/{code}/{form} as required for contract_amendment
and global_payroll_state_taxes, but the gateway uses it more broadly:
the schema engine's `restrict_fields` component branches on
user_role, and resolving that role depends on having an employment
context. Without employment_id, restrict_fields can resolve to nil
and the gateway returns 500 for some country/schema combos (locally
confirmed for BRA and DEU contract_details).
Thread internalEmploymentId from usePayrollAdminOnboarding into the
contract_details and administrative_details schema fetches. Forward-
compatible — gateway accepts and ignores the param for schemas that
don't need it.
(N.B. the immediate `restrict_fields` 500 on the local Tiger is a
backend bug — even with employment_id the controller doesn't set
user_role for company_manager tokens. AUS/USA schemas don't use
restrict_fields so they work; DEU/BRA do. Reported upstream.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rolladminonboardingflow
CI started failing on three fronts after the SDK regen + GP changes landed: - Lint/Format: oxfmt was upset about three files where the back-to-back edits left long-line wraps that didn't match the formatter. Ran the formatter; no logic changes. - Example App Checks: `npm run build` (tsup) OOMs on the runner with the larger SDK. Bumped to `NODE_OPTIONS=--max-old-space-size=8192` in the package.json `build` script so CI and local match (`dev` already had this). - Deploy Cost Calculator / Remote Flows previews: Vercel uses the merge commit, which pulled in main's #1073 ("preview employment agreement step"). That commit references components added in main's SDK regen — the regen on this branch (from production gateway) had to be re-merged. Merged origin/main, re-resolved `src/client/types.gen.ts`, kept main's added Onboarding components and SDK exports. Local example build now succeeds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When stashing+merging origin/main in the previous CI fix, I resolved the example/package-lock.json conflict with `--theirs`, which took the stashed (pre-merge) lockfile instead of the merge result. That left example/package.json on dompurify@3.4.9 / vite@8.0.16 (from main's security bumps) but the lockfile still pinned 3.4.5 / 8.0.13, so `npm ci` in the Example App Checks job failed with EUSAGE. Regenerated the lockfile so it matches the manifest. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
PayrollAdminOnboardingFlowfor Global Payroll admin onboardingPOST /v1/employmentswithtype: global_payroll_employeePUT /v2/employments/:id/contract_detailsandPUT /v2/employments/:id/administrative_detailsmutateAsyncOrThrow(no deprecatedmutateAsync)useStepSubmitHandlerhook extracted to eliminate duplicated try/catch/navigate logic across form step componentsCloses PBYR-4044 · Part of PBYR-3935
Test plan
npm run type-checkpassesnpm test -- --runpasses (781 tests)npm run check-formatclean🤖 Generated with Claude Code