feat(onboarding): wire onBlur events to job title eligibility check - #1406
gabrielseco wants to merge 9 commits into
Conversation
Splits the flag declaration out of #1395 so it can land on its own: adds 'job_title_eligibility' to the OnboardingFeatures union (with its JSDoc) and enables it in the example app. No behavior wired up yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the useJobTitleEligibilityCheck hook and jobTitleEligibilityCheckOptions query, and wires OnboardingForm's onBlur to call the flowBag's new checkJobTitleEligibility(values). The check only runs on the contract_details step, when 'job_title_eligibility' is enabled, and when the role fields (role_description, role_is_onsite, role_requires_license) are filled and valid. Dedup: params are compared against the previous check (fast-deep-equal) before firing a request, and jobTitleEligibilityCheckOptions sets staleTime: Infinity since the query key already encodes employmentId + params. Without both, react-query's built-in dedup only covers truly concurrent calls with an identical key — an imperative queryClient.query() call is not a mounted observer, so it treats every call as a fresh mount and refetches under the default staleTime: 0. The result isn't consumed yet (no UI reacts to eligibility yet) - follow-up. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
📦 Bundle Size Report
Size Limits
Largest Files (Top 5)
View All Files (283 total)
✅ Bundle size check passed |
|
Deploy preview for adp-cost-calculator ready!
Deployed with vercel-action |
|
Deploy preview for remote-flows ready!
Deployed with vercel-action |
📊 Coverage Report
|
| Metric | Current | Previous | Change | Status |
|---|---|---|---|---|
| Lines | 86.01% | 86.40% | -0.39% | 🔴 |
| Statements | 85.59% | 85.97% | -0.38% | 🔴 |
| Functions | 84.49% | 84.92% | -0.44% | 🔴 |
| Branches | 77.21% | 77.93% | -0.72% | 🔴 |
Detailed Breakdown
Lines Coverage
- Covered: 4880 / 5674
- Coverage: 86.01%
- Change: -0.39% (27 lines)
Statements Coverage
- Covered: 4964 / 5800
- Coverage: 85.59%
- Change: -0.38% (27 statements)
Functions Coverage
- Covered: 1296 / 1534
- Coverage: 84.49%
- Change: -0.44% (6 functions)
Branches Coverage
- Covered: 3016 / 3906
- Coverage: 77.21%
- Change: -0.72% (7 branches)
✅ Coverage check passed
Removes the console.log statements left over from development. Also fixes a real bug surfaced while writing tests for this: paramsChanged was read immediately after calling setParams(updaterFn), assuming the functional updater runs synchronously. It doesn't reliably do that, so paramsChanged came back false almost every time - meaning the eligibility check never actually fired a request, for any input. Fixed by comparing against the params value already in the closure before calling setParams, instead of depending on a side effect inside the updater. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
.claude/settings.json is a personal/local permission-allowlist file, not meant for this PR - it got swept into the previous commit because it was already staged from earlier local work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The JSDoc claimed the check also runs on entering the contract details step and before submitting it, but nothing calls it that way - only OnboardingForm's onBlur does. Since useOnboarding is reachable through the public ./flows/* entry point, this overstated the contract for anyone building a custom UI around the headless hook. Reworded to describe only what's actually wired up, and pointed custom UIs at calling checkJobTitleEligibility themselves for other trigger points instead of implying it happens automatically. Also drops stepValues, initialContractDetailsValues, and fieldValues from useJobTitleEligibilityCheck's params - they were threaded through from hooks.tsx but never used inside the hook, leftover scaffolding for the same unimplemented triggers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
check() compared nextParams against params captured from the render closure after two awaits (handleValidation, parseFormValues). If a second blur's check() started before the first one's setParams had committed a re-render, both calls would compare against the same stale params - depending on timing, that can either fire a duplicate request or wrongly treat a later complete result as unchanged and skip it. Flagged by Bugbot on PR #1406, independently of the same race I'd already called out in review. Fixed by tracking params in a ref alongside the state (state still drives the reactive useQuery's enabled/key). Ref writes are synchronous and immediately visible to any concurrently-resolving check() call, so the comparison always sees the latest known params regardless of render timing, closing the window entirely rather than narrowing it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 38681bb. Configure here.
| .catch(() => | ||
| console.error('Failed to fetch job title eligibility check'), | ||
| ); | ||
| } |
There was a problem hiding this comment.
Failed checks skip later retries
Medium Severity
setParams runs before the request finishes, and check() only calls queryClient.query when params changed. After a failed first attempt, later blurs with the same role values skip the request, so the server never records a check unless the user edits a role field again.
Reviewed by Cursor Bugbot for commit 38681bb. Configure here.


Summary
When filling in the contract details step, we now call the job title eligibility check as fields lose focus, needs job_title_eligibility FF
Why
Partners need to know as early as possible if a job title isn't eligible for a given role setup, not just at submit time. Hooking it to blur gives feedback while the form is still open, without waiting for the step transition.
What changed
Toggle details
OnboardingForm.tsx: added anonBluron the form element that calls the newcheckJobTitleEligibility(values)from the flow bag.hooks/useJobTitleEligibilityCheck.tsx(new): owns the check's params state and the underlyinguseQuery; exposescheck(), wired up inuseOnboardingascheckJobTitleEligibility.api.ts: addsjobTitleEligibilityCheckOptions(queryOptions factory) and the mutation-error normalization for the endpoint. SetsstaleTime: Infinity— the query key fully encodesemploymentId+ params, so an identical request should never be considered stale within a session.utils.ts: addsgetJobTitleEligibilityParams, which derives the check's params from the contract details JSF fields (only when the role fields —role_description,role_is_onsite,role_requires_license— are visible, filled, and valid) and returnsnullotherwise.check()only fires the request when the derived params actually changed since the last check (fast-deep-equal, compared inside thesetParamsupdater for freshness). This matters becausequeryClient.query()is an imperative call, not a mounted observer — react-query's automatic dedup only covers truly concurrent calls with an identical key, so without this the same blur-triggered params would refetch every time under the defaultstaleTime: 0.contract_detailsstep, and only when thejob_title_eligibilityfeature flag is enabled.Screenshots
N/A — no rendered UI changes yet; the eligibility result isn't surfaced to the user in this PR.
Related Resources
Testing
example/app in a browserjob_title_eligibilityNote
Medium Risk
Adds feature-flagged calls to the job title eligibility API during contract details editing; behavior is gated but touches employment eligibility logic before results are shown in UI.
Overview
With the
job_title_eligibilityfeature flag, contract details onboarding now triggers a job title eligibility check while the user is still on the step—not only at submit.OnboardingFormcallscheckJobTitleEligibilityon form blur.useOnboardingexposes that from a newuseJobTitleEligibilityCheckhook, which validates/parses values, builds request params viagetJobTitleEligibilityParams(only when the eligibility JSF slug is present and role fields are visible, filled, and valid), and imperatively fetches throughjobTitleEligibilityCheckOptions(postV2+ react-query,staleTime: Infinity, deduped when params are unchanged). The check runs only oncontract_detailswith an employment id.This PR does not surface the API result in the UI; it only fires the request.
.gitignorekeepsexample/tsconfig.e2e.tsbuildinfotracked with a newline fix.Reviewed by Cursor Bugbot for commit 6b6034b. Bugbot is set up for automated code reviews on this repo. Configure here.