Skip to content

Refetch shipping on coupon application - #1441

Open
pbennett1-godaddy wants to merge 35 commits into
mainfrom
refetch-shipping-on-coupon-application
Open

pbennett1-godaddy wants to merge 35 commits into
mainfrom
refetch-shipping-on-coupon-application

Conversation

@pbennett1-godaddy

@pbennett1-godaddy pbennett1-godaddy commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Ensure discount changes correctly reconcile shipping rates and taxes without duplicate requests or stale totals.

Discounts can change shipping eligibility, including enabling or disabling free shipping. The checkout now refetches shipping methods after a successful discount change when a valid shipping destination is available. It determines whether the returned rates require shipping reconciliation and assigns the final tax calculation to either the discount or shipping workflow, ensuring taxes run exactly once.

This PR also improves shipping-method selection and express checkout discount synchronization.

Key changes

  • Refetch shipping methods after applying or removing a discount when a complete shipping destination is available.
  • Skip shipping-rate and tax requests when no valid shipping or billing destination exists.
  • Separate core discount and shipping mutations from their workflow side effects.
  • Reapply all applied discount codes (order, line-item, and shipping-line) after a shipping-method change, before calculating taxes.
  • Calculate taxes once:
    • From the discount workflow when shipping remains unchanged.
    • From the shipping workflow when a new method must be applied.
  • Shipping-method selection:
    • Keep the saved or customer-chosen method whenever it is still offered, including on page load and after rate repricing.
    • Fall back to the cheapest method only when the current method is no longer offered, was selected automatically and the rates changed, or free shipping newly becomes available.
  • If reapplying shipping fails after a discount change, clear the stale shipping line on the order and surface the error.
  • Show a localized retry action when shipping rates cannot be loaded.
  • Remove the unused experimental_rules.freeShipping client-side filtering and query fields; the shipping API is authoritative for rate eligibility.
  • Express checkout:
    • Synchronize order, line-item, and shipping-line discount codes with the GoDaddy and Stripe shadow orders, using the code worth the most (express accepts a single coupon).
    • Recompute cached coupon adjustments when the applied code or the order subtotal changes, without duplicate PriceAdjustments requests from unrelated draft-order updates.
    • GoDaddy wallet: refresh shipping methods on coupon changes, in preparation for discount-adjusted rates from checkout-api (gdcorp-commerce/checkout-api#207).

Changeset

  • Changeset added (docs)

Test Plan

  • Added coverage for discount application when shipping rates remain unchanged.
  • Added coverage for paid shipping becoming free after a discount.
  • Added coverage for a newly available free method becoming the default.
  • Added coverage ensuring shipping reconciliation completes before the single tax request.
  • Added coverage for applying, reapplying, and removing shipping-line discounts.
  • Added coverage for applying a coupon before an address is available.
  • Added coverage for a saved non-cheapest shipping method being kept on load.
  • Added coverage for a customer-chosen method being kept when a coupon reprices the rates.
  • Added coverage for shipping-method selection rules and reconciliation decisions.
  • Added coverage for collecting discount codes and choosing the highest-value code for express checkout.
  • Added coverage for shipping reapplication failures, error codes, and recovery.
  • Added coverage for clearing a stale shipping line when reapplication fails, then reapplying it.
  • Added coverage for re-applying line-item discount codes when the shipping method changes.
  • Added coverage for an automatic shipping selection staying automatic after a failed customer change.
  • Added coverage for matching shipping-line discounts by id in the discount cache.
  • Added GoDaddy and Stripe express component coverage for the highest-value coupon code, subtotal-driven adjustment refreshes, GoDaddy wallet shipping refresh on coupon removal, and Stripe not refetching shipping on coupon changes.
  • Verified the complete browser flow for:
    • Discount application with unchanged shipping.
    • Discount removal with unchanged shipping.
    • Discount application enabling free shipping.
    • Shipping-specific coupon application and method changes.
    • A fresh checkout without an address.

Validation completed:

  • pnpm --filter @godaddy/react typecheck
  • pnpm --filter @godaddy/react test (79 test files, 840 tests passed)
  • biome check on changed files

🤖 Generated with Claude Code

@pbennett1-godaddy
pbennett1-godaddy requested a review from a team as a code owner August 26, 2026 16:45
@changeset-bot

changeset-bot Bot commented Aug 26, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 92a0191

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

This PR includes changesets to release 2 packages
Name Type
@godaddy/react Patch
@godaddy/localizations 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

@pbennett1-godaddy
pbennett1-godaddy changed the base branch from main to fix-billing-collection-logic August 26, 2026 16:57
Base automatically changed from fix-billing-collection-logic to main August 29, 2026 00:48

@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.

Nice work splitting the core mutations from their workflow side effects. The single tax call is well covered, and the request-id guard in the express coupon sync is a good fix for the duplicate PriceAdjustments calls. CI-equivalent checks pass locally (818 tests, typecheck, biome).

I'm requesting changes for two shipping-selection regressions I reproduced against main, plus two express checkout issues. Details are inline. A few items didn't fit inline:

  • Free-shipping filter removal: Is the free-shipping minimum order total enforced server-side in every environment? If not, removing the experimental_rules.freeShipping filter means free shipping is offered below the threshold. Dropping freeShipping from the queries also removes it from the public CheckoutSession type, which is derived from the query, in a patch release.
  • PR description: The description mentions express visibility changes ("without depending on temporary PURCHASE fulfillment state", plus digital-only/pickup visibility coverage), but they aren't in the diff. Please update the description or add the missing changes.
  • Unrelated schema changes: checkout-env.ts adds orderId and a checkoutSession(id:) arg. These look unrelated, so please split them out or confirm they're intended.

const existingMethod = currentFormMethod || currentServiceCode;
const isInitialSelection = lastShippingMethodsKeyRef.current === null;
const { selectedMethod: methodToApply, methodsKey } =
selectShippingMethod({

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.

Regression: a persisted shipping selection is overwritten on load. On first render lastShippingMethodsKeyRef.current is null, so selectShippingMethod treats the rates as changed and picks the cheapest method. It then mutates the order if the saved line differs.

To reproduce, load a draft order with weight-based ($1) already selected and the default rates. main sends no ApplyCheckoutSessionShippingMethod; this branch applies free-shipping. This affects reloads, returning from redirect payment flows (MercadoPago/CCAvenue), and any remount of this form.

The free-order test was rewritten to expect this behavior, and the new "preserves an existing rate" test only uses the cheapest rate, so it can't catch it.

Suggested fix: on first load, seed the key from the current methods so an existing service code is kept, e.g. previousMethodsKey: lastShippingMethodsKeyRef.current ?? getShippingMethodsKey(shippingMethods) when existingMethod is set.

const availableMethods = sortShippingMethods(shippingMethods);
const methodsKey = getShippingMethodsKey(availableMethods);
const methodsChanged = methodsKey !== previousMethodsKey;
const selectedMethod = methodsChanged

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.

An explicit customer choice is downgraded whenever any rate in the list changes. The key includes cost, so any repricing counts as a change and falls back to availableMethods[0].

To reproduce, the customer picks Express ($20 vs Standard $5), then edits the postal code so the rates become $21 / $6. main keeps Express; this branch switches to Standard. Carrier rates reprice on almost every address edit, so this will be common. The same thing happens when a coupon makes a different method cheaper.

Is that intended? If not, I'd keep the current method whenever it's still available and the customer chose it. Only fall back to the cheapest when the current method is gone, was auto-defaulted, or free shipping newly appears. One way is to track user selection in handleValueChange.

// Start with the base line items
const baseLineItems = [...poyntExpressRequest.lineItems];

// Refetch shipping methods so rates reflect the coupon change (e.g. free-shipping discounts)

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.

I don't think this refetch can reflect the coupon. DraftOrderShippingRatesQuery takes only destination, and a wallet-entered coupon is never written to the draft order. It only feeds the read-only calculatedAdjustments query. So the rates come back the same as before.

Meanwhile this adds a round trip on every coupon change. Combined with the block at ~L847, it replaces the wallet's shippingMethods while the line items and total still use godaddyTotals.shipping.value / shippingMethod. If the wallet resets the selection to the first (cheapest) option, the displayed method and the charged amount diverge. I'd remove this block and the one at ~L847.

const defaultMethod = sortShippingMethods(shippingMethodsData || [])[0];

if (defaultMethod) {
setSelectedShippingRate({

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.

This can mutate selectedShippingRate outside a wallet event. shippingAddress is reset in handleClick and handleCancel, so this branch only runs while the sheet is open. When it does run, it changes the selected rate without resolving anything to the Express Checkout Element. handleConfirm would then submit a shippingTotal and method different from what the customer saw. Since Stripe ECE has no coupon entry, I'd drop the shipping refetch from this effect entirely.

syncPriceAdjustments();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [draftOrder, draftOrderDiscountCodes]);
}, [hasDraftOrder, discountCodesKey, couponSyncRevision]);

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.

Keying only on the discount codes removes the duplicate requests, but calculatedAdjustmentsRef depends on the subtotal and shipping too. If the cart or shipping changes while the codes stay the same, the cached adjustments are stale (e.g. a percentage discount computed on the old subtotal). Consider adding totals.subTotal / shippingTotal values to the key. The same applies to the Stripe effect.

result?.checkoutSession?.draftOrder?.calculatedShippingRates?.rates
)
) {
throw new Error('Shipping rates are unavailable');

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.

Two questions:

  1. Can the API return calculatedShippingRates: null for a legitimate "no rates" case, e.g. a store with no shipping profile? If so, this now shows the failure/retry UI and clears the applied shipping instead of "No shipping methods found."
  2. Since this is an intentional throw, please set retry: false here explicitly. Hosts can pass their own QueryClient, and TanStack's default of 3 retries means ~7s of skeleton, plus useReconcileAfterDiscount awaits the refetch while the coupon button spins.

);
}

const allCodes = new Set<string>();

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.

Pre-existing, but easy to fix now: this re-applies only order and shipping-line codes. The discount mutation replaces the whole list (DiscountStandalone sends the full set, and removal sends []), so an order with both an order-level code and a line-item code loses the line-item code on any shipping change. getDraftOrderDiscountCodes(order) would fix it. DiscountStandalone could use the same helper.

},
},
],
shippingLines: [],

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.

This hook isn't used anywhere. It's still edited here, and it calls useDiscountApply, which now runs useReconcileAfterDiscount. That could re-apply a shipping method right after it's removed. I'd delete it, or switch it to useApplyDiscountCore.

Comment thread .changeset/calm-coupons-ship.md Outdated

Fix billing collection, shipping reconciliation, and discount/coupon sync across checkout flows.

- Align billing fields and validation for paid, free, pickup, shipping, purchase, and digital orders.

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.

The billing bullets (this line through L10) already shipped in #1438 (e366269), and they're in the @godaddy/react CHANGELOG. Please drop them so the release notes don't repeat them.

Comment thread packages/localizations/src/deDe.ts Outdated
'Geben Sie Ihre Adresse ein, um verfügbare Versandmethoden zu sehen.',
noShippingMethods: 'Keine Versandmethoden gefunden.',
failedToLoadMethods:
'Versandarten konnten nicht geladen werden. Bitte versuche es erneut.',

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.

Nit: the rest of deDe uses formal address ("Sie"). Suggest 'Versandarten konnten nicht geladen werden. Bitte versuchen Sie es erneut.'

Keep a saved or customer-chosen shipping method while it is still offered,
including on load, and only fall back to the cheapest rate when it is gone,
was picked automatically and rates changed, or free shipping newly appears.

Re-apply line-item discount codes on shipping changes, stop the Stripe
express sheet from refetching and reselecting shipping on coupon changes,
use the highest-value code for express wallets, and recompute cached coupon
adjustments when the subtotal changes. Keep the unused remove-shipping hook
from triggering reconciliation, disable automatic retries on the shipping
rates query, trim the changeset, and use formal German copy.

Add tests for the selection rules, failed-reapply recovery, discount cache
matching, and the GoDaddy and Stripe express coupon sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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