feat(gp): Global payroll demo flow#1077
Conversation
ede84ee to
a458469
Compare
📦 Bundle Size Report
Size Limits
Largest Files (Top 5)
View All Files (384 total)
✅ Bundle size check passed |
📊 Coverage Report
|
| Metric | Current | Previous | Change | Status |
|---|---|---|---|---|
| Lines | 83.95% | 88.45% | -4.50% | 🔴 |
| Statements | 83.38% | 87.86% | -4.48% | 🔴 |
| Functions | 82.01% | 84.83% | -2.82% | 🔴 |
| Branches | 75.80% | 79.83% | -4.03% | 🔴 |
Detailed Breakdown
Lines Coverage
- Covered: 3929 / 4680
- Coverage: 83.95%
- Change: -4.50% (47 lines)
Statements Coverage
- Covered: 3994 / 4790
- Coverage: 83.38%
- Change: -4.48% (50 statements)
Functions Coverage
- Covered: 1062 / 1295
- Coverage: 82.01%
- Change: -2.82% (11 functions)
Branches Coverage
- Covered: 2452 / 3235
- Coverage: 75.80%
- Change: -4.03% (14 branches)
✅ Coverage check passed
|
Deploy preview for remote-flows ready!
Deployed with vercel-action |
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.
|
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>
|
This PR bundles 6 independent features (44 files, 3,044 lines). Each needs domain-specific review:
Why split? One giant PR = 60+ unfocused comments, reviewer fatigue, rubber-stamp approvals. Split PRs = focused 5-10 comment discussions that improve code quality. ❌ Major Issues: PayrollEmployeeOnboarding Example
Issue 1:
|
…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>
* 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>
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>
- 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>
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>
852d369 to
9235602
Compare
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dingFlow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace three duplicated isMutationError try/catch blocks in PayrollAdminOnboarding (useStepSubmitHandler, SelectCountryStep, InvitationStep) with handleStepError, matching ContractorOnboarding and CreateCompany - Apply the same fix to useEmployeeStepSubmitHandler in PayrollEmployeeOnboarding - Align GPStepCallbacks.onError.fieldErrors to NormalizedFieldError[] (was FieldError[]) so the public API is consistent with other flows - Remove hardcoded <select> HTML from SelectCountryStep; the component now renders PayrollAdminForm through the schema/field pipeline like ContractDetailsStep and AdministrativeDetailsStep - Fix skipCountry to require both initialCountryCode and initialEmploymentId so providing only a countryCode no longer skips employment creation - Make onSubmit guards throw instead of silently returning undefined, preventing next() from being called when preconditions aren't met Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…guard bugs" This reverts commit cb30d66.
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
Note
Medium Risk
Touches OAuth/JWT token minting and proxy routing for employee APIs (dev-only guards on token routes), plus large new payroll onboarding surfaces that call employment and tax endpoints.
Overview
This PR replaces the GP onboarding scaffolds with working admin and employee flows: JSON-schema–driven steps, per-step API mutations, shared submit/error handling, and typed step callbacks via
GPStepCallbacks.Payroll admin adds country selection, contract/administrative forms, employment creation (
POST /v1/employments), updates, and an invite step wired through new API hooks and step components (no more null placeholders).Payroll employee adds personal details, address, optional bank account, and USA federal/state tax steps with
taxStepsAvailability(pending enrollment, jurisdiction, schema errors) andcountryCode/jurisdictionon the flow props.The example app registers GP Admin and GP Employee demos and extends the dev proxy to mint employment-scoped JWT-bearer tokens for
/v1/employee/*usingx-rf-employment-id, plusauthType: 'none'when the proxy holds auth server-side. Bundle size limit is bumped slightly; lockfile has routine rollup-related updates.Reviewed by Cursor Bugbot for commit 1ca1e3a. Bugbot is set up for automated code reviews on this repo. Configure here.