feat(StateTaxes): respect applicable_if for conditional field visibility#2256
Open
jeffredodd wants to merge 10 commits into
Open
feat(StateTaxes): respect applicable_if for conditional field visibility#2256jeffredodd wants to merge 10 commits into
jeffredodd wants to merge 10 commits into
Conversation
Each TaxRequirement returned by the embedded API can carry an applicable_if constraint list that gates visibility on the answers to other requirements in the same set. The SDK previously rendered every requirement unconditionally and sent every value back on submit. Add an isRequirementApplicable helper, use it in Form.tsx (via useWatch on the form values) to filter what we render, and use it in the submit handler to drop inapplicable requirements from the payload so stale values aren't re-asserted. Fixes SDK-1057. Recovers the design from closed PR #2248 (commit f0ad9b1 on branch al/feat/state-taxes-applicable-if), preserved for reference per the ticket. Adjusted only the import block of StateTaxesForm.tsx to match current main (which no longer exports CommonComponentInterface). Co-Authored-By: Aaron Lee <aaron.lee@gusto.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…bility Adds two Playwright specs exercising both directions of the applicable_if contract end-to-end against MSW-backed fixtures: - WA: gated UI/EAF rate fields are absent until the parent 'use default rates' radio toggles to No. - ID: gated UI/Admin/Workforce rate fields are visible by default and disappear when the 'reimbursable employer' radio toggles to Yes. Together these prove the helper is direction-agnostic, not just 'hide by default'. To support component-level e2e testing without driving the full Company Onboarding wizard, this also adds: - A `state-taxes-form` flow to the e2e harness so any <StateTaxesForm> can be mounted in isolation via ?flow=state-taxes-form&state=XX. - A new Idaho fixture modeled on ZP's tax_profiles/by_tax_agency/id_spec. - A generalized MSW handler that looks up tax_requirements-<STATE>.json by state code (falling back to GA) instead of hard-coding only WA. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
320dd4e to
15b6fcb
Compare
CI runs `npm run test:e2e:demo` with E2E_USE_REAL_BACKEND=true, which bypasses MSW and hits a demo company whose state-tax requirements aren't shaped for the WA/ID applicable_if fixtures these specs assert against. The conditional visibility behavior is fully covered by the unit tests in StateTaxesForm.test.tsx + applicableIf.test.ts; these e2e specs add real-DOM coverage when run locally via `npm run test:e2e`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The legacy gws-flows `TaxRequirements::Edit.prepare_requirements` step filters out every requirement whose `editable: false` before handing the set to the view. The SDK was rendering them anyway, which would surface an unanswerable input to the user and round-trip the value back through the submit payload. Match the legacy contract: short-circuit non-editable requirements in Form.tsx's flatMap and chain a `.filter(req => req.editable !== false)` ahead of the applicable_if filter in the submit handler. Adds a `legacy_read_only_field` entry to the WA fixture so the unit tests exercise both the render and submit branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…enario CI runs the e2e matrix with E2E_USE_REAL_BACKEND=true, so the prior MSW-only skip meant these specs never ran in CI. Pivot to scenario- driven real-backend coverage: - Add a stateTaxes decoration to the scenario schema (and regenerate scenario.types.ts) so scenarios can pre-seed tax_requirements answers. - Wire decorateStateTaxes into the scenario runner; it PUTs the body to /companies/:id/tax_requirements/:state immediately after locations are created so the relevant state is registered first. - Add shared/state-taxes-wa-id.json scenario: fresh react_sdk_demo with WA + ID locations and WA's `usedefaultsuirates` pre-seeded to true so the rate fields start hidden. ID's `suireimbursable` is left at the natural demo default (false) so its rates start visible — covering both directions of the conditional behavior. - Rewrite the e2e spec to annotate the new scenario; replaces the blanket E2E_USE_REAL_BACKEND skip with the standard `test.skip(!scenario.flowToken)` guard so local MSW-default runs no longer execute these specs (they're real-backend-only now). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`npm run scenarios:types` emits json2ts output that doesn't match prettier's defaults. The format CI job catches this. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first CI run of the new shared/state-taxes-wa-id scenario failed
with a 422 from PUT /tax_requirements/WA — "Question is not applicable"
on `usedefaultsuirates`. The fresh react_sdk_demo with just a WA
location doesn't expose every question we'd see on a fully-configured
WA company; naive PUTs forge invariants the demo backend doesn't hold.
Make decorateStateTaxes pre-fetch the current shape and only PUT the
subset of declared answers that:
- reference a requirement currently present on the demo,
- reference an editable requirement,
- have all of the requirement's applicable_if constraints satisfied
by current values (same AND-logic the SDK uses at render time),
- actually differ from the current value (no churn on no-ops).
Also dumps the full GET response per-state — keys, values,
applicable_if gates, editable flags — so the next CI iteration shows
us the real WA/ID shape on this demo and we can refine the scenario
declaratively instead of guessing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Last CI iteration found GET /tax_requirements/WA returned 0 requirement sets on the fresh react_sdk_demo with just a WA location — locations alone don't establish tax nexus on the demo backend. The state-tax requirement_sets only surface once Gusto recognizes the company has actual nexus in the state, which happens when an employee has a home address there. Add two minimal employees (wa_employee, id_employee) with WA and ID home addresses + matching work_address/job locationKey references. With nexus in both states, the next CI run should see populated requirement_sets, the runner's GET dump will reveal the real shape, and the e2e specs can target whatever the demo backend actually defaults to. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Last iteration found GET /tax_requirements/WA returned 0 requirement sets, confirming that Gusto only surfaces state-tax requirement_sets once the company has nexus (an employee with a home address in the state). Adding nexus via the new wa_employee / id_employee decorations was correct, but the runner still decorated stateTaxes before employees — so the GET fired before nexus existed and saw nothing. Move decorateStateTaxes after decorateEmployees and update the schema description to reflect the new order. Also re-add the stateTaxes declarations to the scenario so the runner's discovery log dumps the real WA/ID shape on the next CI iteration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The discovery dump from the previous iteration confirmed the demo
backend's WA/ID taxrates sets surface only the resolved leaf
requirements — Gusto's prepare_requirements collapses applicable_if
when the underlying StateFilingRequirement has the gate already
resolved, which is the demo factory's default. So the parent radios
(`usedefaultsuirates` for WA, `suireimbursable` for ID) that the unit
tests cover via MSW fixtures are not in the real response, and there's
nothing to toggle.
Pivot the e2e to what's actually testable on the real backend: assert
that StateTaxesForm renders the requirements the demo exposes (UI
rate + EAF rate for WA; UI Contribution Rate for ID) and that the
form is interactive (Save button visible). The applicable_if
conditional logic stays fully covered by:
- src/.../applicableIf.test.ts (helper unit tests)
- src/.../StateTaxesForm.test.tsx (render + submit with MSW fixtures
that DO carry the applicable_if parent radio).
The scenario-runner's `stateTaxes` decoration stays — when a future
demo state exposes applicable_if-gated questions, these specs are
already wired to hit it without further plumbing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
TaxRequirement.applicable_ifconstraint when rendering the State Taxes formisRequirementApplicablehelper plus unit testsprepare_requirements)Fixes SDK-1057.
Context
The embedded API returns each
TaxRequirementwith anapplicable_if: Array<{key, value}>field that gates visibility on the answers to other requirements in the same set. The SDK previously ignored it and rendered every requirement unconditionally — for example, Washington'staxratesset returns three rate inputs that are only meant to show when the user has answered "No, my agency gave me new rates" on a parent radio, but the SDK rendered them regardless. gws-flows has honored this contract via a small JS file for years, and the API rejects payloads that send answers for inapplicable requirements (QuestionNotApplicableDueToMismatchingDependentValues).Reference impl confirmation
The shape was confirmed against three independent sources:
@gusto/embedded-api-v-2025-11-15/models/components/taxrequirement.d.ts): "This requirement is only applicable if all referenced requirements have values matching the correspondingvalue... an empty array means the requirement is applicable."packs/.../tax_profiles/app/services/tax_profiles/states/core/read_api.rb:230-235):question.applicable_if.all? { |c| parent && parent.value == c.value }tax_requirements.js): sameevery-with-strict-equality logic, plus disabling the input so it isn't submitted.edit.rb(prepare_requirements): the legacy edit-view step also filterseditable == falserequirements entirely; matched here.Implementation
src/components/Company/StateTaxes/StateTaxesForm/applicableIf.tsevaluatesapplicableIfagainst the live RHF form values. Prefix-aware: constraints reference the raw API key; form values are stored undersetKey.toRhfKey(reqKey).Form.tsxaddsuseWatch({ control })and filters each requirement before rendering. Also dropseditable === falserequirements.StateTaxesForm.tsxreuses the helper to drop inapplicable requirements from the payload, and additionally drops non-editable ones..optional().Test plan
Unit tests (always-on coverage of the conditional logic)
isRequirementApplicablehelper covers empty / single match / single mismatch / AND-logic / missing set / pipe-key round-trip (applicableIf.test.ts)'renders tax rate fields'integration test was implicitly relying on the bug — updated to assert the rate input is hidden until the radio toggles to "No"'does not render non-editable requirements'+'omits non-editable requirements from the submit payload'against aneditable: falserow added to the WA fixturenpm run test -- --run src/components/Company/StateTaxes/passes (18/18)E2E tests (Playwright, real-backend via new shared scenario)
The CI matrix runs e2e with
E2E_USE_REAL_BACKEND=true, so the specs must work against the live demo backend rather than MSW. This PR introduces the infrastructure needed:stateTaxesdecoration ine2e/scenarios/schema/scenario.schema.json— scenarios can declare desiredrequirement_sets[].requirements[]answers per state.decorateStateTaxesine2e/scenario/runner.tsis adaptive: GETs the current shape first, logs every discovered requirement (key, value, applicable_if gates, editable flag), and only PUTs the subset of declared answers that match a currently-applicable editable key with a different current value. Forging invariants the backend doesn't expose returns 422; the adaptive approach makes the scenario nudge the demo toward the declared state without breaking when the demo doesn't expose a question.stateTaxesruns afteremployeesbecause Gusto only surfaces state-tax requirement_sets once the company has nexus (an employee with a home address in the state). Locations alone are insufficient.shared/state-taxes-wa-id.jsonscenario: freshreact_sdk_demo+ WA & ID locations + two minimal employees (wa_employee,id_employee) whose home addresses establish nexus in each state.Specs in
e2e/tests/company/03-state-taxes-applicable-if.spec.ts:taxratesset withUnemployment Insurance RateandEAF Tax Ratefields visible, plus an interactiveSavebutton.UI Contribution Rateand the interactiveSavebutton.Scope note on applicable_if e2e coverage: the discovery dump from the scenario runner confirmed the demo backend's WA/ID
taxratessets surface only the resolved leaf requirements — Gusto'sprepare_requirementscollapsesapplicable_ifwhen the underlyingStateFilingRequirementhas the gate already resolved, which is the demo factory's default. So the parent radios (usedefaultsuiratesfor WA,suireimbursablefor ID) aren't in the real response, and there's no toggle to exercise here. The conditional logic itself is fully exercised by the unit tests above (which use MSW fixtures that DO carry the gating parent radio). When a future demo state exposesapplicable_if-gated questions, these specs are already wired to hit it via the existing scenario decoration without further plumbing.Run locally:
npx playwright test e2e/tests/company/03-state-taxes-applicable-if.spec.tsOpen review questions (called out in SDK-1057)
flattened_ancestorsinread_api.rb:667-674suggests the API flattens grandparent constraints into each child'sapplicable_ifbefore serializing. The helper relies on this — if the API ever ships unflattened chains, it would under-evaluate. No 3-deep fixture exists to prove it.nullas a constraint value: The type allowsvalue?: boolean | string | number | null. The helper uses===, soundefined === nullis false. If the API usesnullto mean "matches when unanswered," this gets it wrong. Not exercised by any current fixture.🤖 Generated with Claude Code