Skip to content

Add VAT Included line item for inclusive taxes - #1445

Merged
wcole1-godaddy merged 7 commits into
mainfrom
inclusive-taxes-line-item
Sep 15, 2026
Merged

wcole1-godaddy merged 7 commits into
mainfrom
inclusive-taxes-line-item

Conversation

@pbennett1-godaddy

@pbennett1-godaddy pbennett1-godaddy commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a separate VAT included row to checkout totals without changing the amount due. The row sums amount.value from order-level taxes where included === true, uses the order currency, and appears only when enableTaxes is enabled and includedTaxTotal is positive. All supported locales include the label, including GST included for Australia.

The shared totals component and CartTotals accept includedTaxTotal. The built-in storefront cart drawer passes enableTaxes={false}, so it hides both tax rows.

The checkout draft-order query now selects only taxes { included amount { value } }. Both tax rows show loading during tax, shipping, and discount mutations. Tax mutations remain pending until the draft-order refetch settles, keeping totals and tax constituents synchronized; this includes refetch retries and delays rejection of failed tax mutations. Shipping fulfillment synchronization uses the mutation hook's existing refresh instead of issuing another full refetch.

The new checkout.summary.totals.included-taxes.before target lets extensions position content before the included-tax row.

Backend contract

The checkout tax pipeline writes inclusive constituents at the order level with included: true and additional: false. In order-api, updateOrderTotals adds order-level taxes to taxTotal only when additional is true, alongside line-item and shipping tax totals. Therefore these checkout inclusive constituents do not increase taxTotal or the amount due; their value is already represented in the supplied price. For example, a $120 price containing $20 VAT remains $120 due, with $20 disclosed as VAT included.

This frontend calculation uses included; it does not select or filter additional. The backend contract above describes checkout's order-level constituents, not a general exclusion rule for line-item or shipping taxes.

Checkout totals showing VAT included

Changeset

  • Patch changeset for @godaddy/react and @godaddy/localizations.

Test Plan

  • Unit coverage sums inclusive constituents, excludes non-inclusive constituents, and handles missing tax data and amounts.
  • Checkout coverage verifies the amount within the VAT row, multiple constituents, mixed inclusive/non-inclusive taxes, zero amounts, and enableTaxes={false}.
  • Loading coverage verifies both tax rows during shipping/discount changes and holds a tax refetch open to verify that stale VAT stays hidden until the refreshed amount arrives or the row disappears.
  • Fulfillment coverage verifies new physical line items become SHIP with one draft-order refresh through tax recalculation, discount recalculation, and direct-refresh paths.
  • Extension coverage includes the new target.
  • Validation: pnpm --filter @godaddy/react typecheck, pnpm --filter @godaddy/react lint, pnpm --filter @godaddy/react test, and pnpm --filter @godaddy/react build.

@pbennett1-godaddy
pbennett1-godaddy requested a review from a team as a code owner September 2, 2026 16:34
@changeset-bot

changeset-bot Bot commented Sep 2, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fa40597

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@godaddy/localizations Patch
@godaddy/react Patch
nextjs Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@wcole1-godaddy wcole1-godaddy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this — the feature is small and well-scoped, typecheck and the full @godaddy/react vitest suite pass on the branch, and all 21 locales have the new key. Requesting changes for the first two items below; the rest are non-blocking but worth addressing.

Should fix before merge

1. The additional exclusion is dead code on the checkout path, so cart and checkout can disagree.
getIncludedTaxTotal filters out additional === true, but the checkout schema's LineItemTax type has no additional field and DraftOrderQuery cannot select it — only the storefront OrderTax has it. Because TaxAmount.additional is optional, tsc never surfaces the gap. For an order with a tax that is both included and additional, the cart drawer excludes it and the checkout summary counts it. The unit test asserts a shape checkout can never produce.
Suggest either dropping the filter, or typing the helper against the real query result types (CartTax plus a DraftOrder['taxes'][number] alias) so the mismatch is visible, and documenting that the exclusion is storefront-only.
→ packages/react/src/components/checkout/totals/utils/get-included-tax-total.ts:16

2. The 502-line checkout-env.ts regen is unrelated to this PR.
It is insertion-only and adds tips, CheckoutSessionFee, calculateCheckoutSessionFees, FundingSourceType, etc. DraftOrder.taxes and LineItemTax already existed at the merge base, so the new taxes { ... } selection type-checks without it. The introspection is generated from a gitignored schema file, so reviewers can't reproduce it or confirm which endpoint it came from. Please revert that file here and land the schema sync as its own PR.

Should fix

3. The VAT row bypasses the enableTaxes switch.
"Estimated taxes" is gated by enableTaxes (derived in checkout-form from session.enableTaxCollection || taxTotal > 0); the new row is gated only on vatIncluded > 0. Two concrete effects: the library's own storefront Cart passes enableTaxes={false} to CartTotals while vatIncluded arrives via the spread, so the cart drawer now shows "VAT included" with no taxes row above it; and a merchant with tax collection disabled but tax-inclusive pricing gets a row they can't turn off except by passing vatIncluded={0}. If showing it in the cart is intentional, please say so in the description and add a dedicated enable prop.
→ packages/react/src/components/checkout/totals/totals.tsx:160

4. Stale VAT amount after tax/discount/shipping mutations.
isTaxLoading only tracks the pending updateDraftOrderTaxes mutation. Its onSuccess in use-update-taxes.ts patches totals only (and writes it under the key taxesTotal — pre-existing bug), and the discount/shipping mutations don't select taxes. So after an address change from a VAT region to a non-VAT one, the skeleton clears and the old VAT amount renders next to already-updated totals until the onSettled refetch lands. The skeleton also never shows on first appearance because vatIncluded is still 0 when loading starts.

5. Missing extension anchor.
Every sibling row has a <Target id='checkout.summary.totals.<row>.before' />; the new row doesn't, so UI extensions can't position relative to it.

Minor

  • Exempted constituents are counted. An included tax with exempted: true still adds to the VAT row while "Estimated taxes" shows zero. Worth confirming the API never emits that shape, or filtering it.
  • Public prop name is jurisdiction-specific. vatIncluded lands on the exported DraftOrderTotalsProps / CartTotals, yet enAu labels it "GST included" and the API returns per-constituent name. Consider includedTaxes for the prop and keep the VAT wording only in the locale key — renaming later is a breaking change.
  • Tests are weaker than the description claims. The description lists four integration cases; the diff adds one test plus one negative assertion. That negative assertion runs against the fixture default taxes: [], so it would pass if the row rendered whenever any tax exists. getAllByText('$1.23').length > 0 can't fail on length (getAllByText throws on zero matches) and doesn't tie the amount to the VAT row.
  • Over-fetching. The new order-level taxes selection in DraftOrderQuery requests currencyCode, exempted, id, name, ratePercentage, none of which the client reads, and this query refetches on every window focus and draft-order mutation.
  • Nit. The ?? null in useDraftOrderIncludedTaxTotal is redundant (the helper accepts undefined), and the cart call site omits it.

@wcole1-godaddy wcole1-godaddy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Typecheck passes and the checkout suites pass locally (90/90 across the six relevant files). The removed optimistic write in use-update-taxes.ts targeted a taxesTotal key nothing reads, so that deletion is safe, and all 21 locales are covered.

A few non-blocking notes:

  • Description vs. code: the summary says inclusive taxes exclude additional === true, that the storefront query now fetches additional, and that a test covers "included and additional". None of that is in the diff. getIncludedTaxTotal only checks included, and the checkout LineItemTax type has no additional field. Worth correcting the description/test plan so history matches behavior (or adding the filter on the storefront OrderTax, which does expose it).
  • Double-count assumption: the row assumes backend taxTotal excludes inclusive constituents. Not verifiable from this repo. A quick confirmation from the API owners would close that off.
  • Loading asymmetry: the new row skeletons on isShippingLoading || isDiscountLoading, but "Estimated taxes" directly above only watches isTaxLoading, so one tax row pulses while the other shows a stale value during shipping/discount changes. Consider using the same condition for both.
  • Mutation lifecycle: returning the invalidate promise from onSettled means every await updateTaxes.mutateAsync(...) chain now waits through the draft-order refetch and up to 3 retries, and a failed tax call doesn't reject until the refetch settles. Intentional trade-off, but a comment would help the next author. The fulfillment-sync branch in shipping-method.tsx also now does two serialized full refetches.
  • isRemovingShipping is wired to useRemoveShippingMethod, which currently has no production callers.
  • Minor: the new taxes selection fetches exempted/id/name/ratePercentage that nothing reads; the "keeps tax loading" test re-mocks getDraftOrder by hand rather than using setCurrentDraftOrder; the row block is a nested ternary where sibling rows use the flat form.

@wcole1-godaddy
wcole1-godaddy merged commit 60ac1da into main Sep 15, 2026
3 checks passed
@wcole1-godaddy
wcole1-godaddy deleted the inclusive-taxes-line-item branch September 15, 2026 15:07
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.

2 participants