Skip to content

fix(checkout): clear tax ID when country change coalesces with another address edit - #14260

Closed
detail-app[bot] wants to merge 1 commit into
mainfrom
detail/bug-fix/fix-checkout-clear-tax-id-when-country-change-coal-c2c59a
Closed

detail-app[bot] wants to merge 1 commit into
mainfrom
detail/bug-fix/fix-checkout-clear-tax-id-when-country-change-coal-c2c59a

Conversation

@detail-app

@detail-app detail-app Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Detail bug report: View on Detail

Summary

Related Issue: polarsource/feedback#483

The checkout form's debounced address watcher failed to clear the tax ID when a billing country change was coalesced with another address-field edit, causing a misleading "Invalid tax ID" error and a form/server desync for returning customers with a saved tax ID.

What

  • clients/packages/checkout/src/components/CheckoutForm.tsx: in the watcher's else if (name.startsWith('customer_billing_address')) branch, clear the tax ID (payload + form field + error) when the coalesced update carries a country that differs from the last persisted checkout country.
  • clients/packages/checkout/src/components/CheckoutForm.test.tsx: two regression tests covering the coalesced clear and the no-over-fire guard.
  • clients/.changeset/checkout-coalesced-country-tax-id.md: patch changeset for the published @polar-sh/checkout package.

Why

The form syncs address edits to PATCH /v1/checkouts/client/{id} through a single 500 ms-debounced watch callback. The watcher branches on the last field name that changed within the debounce window. Commit 7ece780fb5 ("fix(checkout): clear tax ID when billing country changes") added customer_tax_id: null clearing only to the name === 'customer_billing_address.country' branch.

When a returning customer changes billing country and then edits another address field (e.g. line1) within 500 ms, the debounce fires carrying the later field's name and takes the sibling name.startsWith('customer_billing_address') branch — sending the new country without customer_tax_id: null. The server keeps the stale tax ID, re-validates it against the new country, and rejects the PATCH with 422 "Invalid tax ID". The transaction rolls back server-side, but the form keeps the new country, leaving a transient desync and a field error pointing at the tax-ID field instead of the customer's intended country change.

How

The tax-ID clearing is decoupled from the name-based branching. In the coalesced branch, the guard compares the new country against checkout.customer_billing_address?.country — the last persisted server country — rather than the live watched country value. By the time the coalesced watcher fires, the component has already re-rendered with the new country, so the closed-over country equals newCountry and a naive country !== newCountry guard would be a no-op for the very race being fixed. The persisted checkout country only advances on a successful PATCH, so it is still the old country in the coalesced case and the trip is detected. When the country actually changed and a tax ID is present, the branch sends customer_tax_id: null, clears the field error, and resets the form field — the same side effects the dedicated country-branch already performs.

A comment documents why the guard uses the persisted checkout country, since the correctness of the comparison against checkout.customer_billing_address?.country (rather than the live country) is the non-obvious core of the fix.

Checklist

  • This PR addresses a single concern (one bug fix)
  • The diff is reasonably sized and easy to review
  • New functionality is covered by tests
  • Linting and type checking pass (pnpm --filter @polar-sh/checkout typecheck, oxlint, oxfmt --check)
  • No unrelated changes or drive-by fixes are included

Testing

Unit testspnpm --filter @polar-sh/checkout exec vitest run src/components/CheckoutForm.test.tsx passes 37/37 (35 pre-existing + 2 new):

  • clears the tax ID when a country change is coalesced with a line1 edit within the debounce window — the core regression: a country change followed by a line1 edit within 500 ms produces a single coalesced update carrying both the new country/line1 and customer_tax_id: null, with the form field reset to ''.
  • does not clear the tax ID when only a non-country address field is edited (no country change) — guard: editing line1 alone (country unchanged) must not add customer_tax_id to the payload or reset the form field, ensuring the new clear is gated on a real country change rather than unconditional.

Both new tests were confirmed to fail against the pre-fix source (the coalesced-clear test fails because customer_tax_id is absent from the payload) and pass with the fix.

Full checkout suite + coveragepnpm --filter @polar-sh/checkout test passes 455/455 across 34 files; coverage (Statements 82.64% / Branches 73.21% / Functions 86.1% / Lines 83.24%) stays above the package's 80/80/80/70 thresholds.

Provider error-surfacingCheckoutFormProvider.test.tsx passes 26/26; the PolarRequestValidationError → field-level setError mapping (the path that renders a server "Invalid tax ID" as a field error) is unchanged.

Server tax-ID re-validationpytest tests/checkout/test_service.py::TestUpdate::test_invalid_tax_id passes 3/3. The server still rejects a country update with no customer_tax_id when the stale tax ID is invalid for the new country; the fix is purely client-side (it stops the client sending that rejected shape on a real country change).

Typecheck, lint, format, buildtsc --noEmit, oxlint, oxfmt --check, and the tsup build all pass cleanly.

End-to-end browser verification (not run) — The intended check was to drive the live checkout form with Playwright via the verifier-web skill: open a Stripe-keyed checkout for a returning customer with a saved tax ID, change country to US and edit line1 within the debounce window, then confirm no "Invalid tax ID" error appears, the tax-ID field clears, the country persists server-side (verified in the DB), and the checkout still completes with a Stripe test card. I could not run this in the local sandbox: the dev docker web container (the Next.js checkout UI host) fails to start with a host cgroup v2 limitation (cannot enter cgroupv2 "/sys/fs/cgroup/user/workload" with domain controllers -- it is in domain threaded mode, reproduced on initial bring-up and explicit restart), and the Playwright MCP browser tooling the skill requires is not available in this environment. The API half of the stack is healthy (/healthz returns ok). The aspects this e2e would verify are otherwise covered by the unit suites (client payload shape) and the server test (tax-ID re-validation); the browser e2e should be re-run on a host with a working cgroup setup before release.


Automatic Fixes PRs can be configured here.

Review in cubic

@vercel

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
orbit Ready Ready Preview Sep 8, 2026 3:57pm UTC
polar-test Ready Ready Preview Sep 8, 2026 3:57pm UTC

Request Review

This branch was successfully deployed

2 active deployments
Preview – polar-test 45599fc5 Deployed Sep 8, 2026 by vercel[bot]
Preview – orbit 45599fc5 Deployed Sep 8, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant