fix(TWO-25554): answer "is billing distinct" from the checkbox and the quote - #413
fix(TWO-25554): answer "is billing distinct" from the checkbox and the quote#413dgjlindsay wants to merge 1 commit into
Conversation
…e quote The single authority behind both the resolver and billingRoleIdentity(): the buyer's own "same as shipping" box on the ACTIVE payment method, and the quote's cache-key comparison. A third-party re-render detaches the billing fieldset for an instant, and a visibility read hands the other panel's field the company the buyer just picked. The billing panel's MOUNT predicate is unchanged — it is still the visibility of the billing company field. On a saved-shipping-address, non-virtual, search-enabled checkout that leaves the billing panel mounted as the only control on the page while the quote has not yet diverged, so a pick made there resolves to shipping's empty identity and is discarded. Awaiting a product ruling; this commit is self-contained so it can be dropped or amended on its own. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the full diff (production JS in company-capture.js, company-capture-component.js, company-identity.js, company-search.js, company-search-panel.js, company-source-resolver.js, address-autocomplete.js, gateway_method.js/template, plus the extensive Test/Js and LayoutProcessorPluginTest.php updates). No security, data-corruption, crash, or breaking-API issues found.
Notable, non-blocking observations:
quoteHoldsDistinctBillingAddress()compares cache keys with!=rather than!==; both sides are always strings fromgetCacheKey()so this is harmless today, but worth tightening if that ever changes.writeAddressInto/revertAddressFormFieldsnow key the write record on the panel's identity object viaWeakMap, and every call site guards on!identitybefore writing — this looks correctly threaded through call sites incompany-capture.js,address-autocomplete.js, and the tests exercising the "no owning identity" refusal path.renderAddressNotice()usestextContent, notinnerHTML, so no XSS surface from the notice copy.
The PR description itself flags that the tip commit (f980c94) still needs a ruling on which panel is authoritative for billingRoleIdentity()/mount timing, and that translations for the new "could not fill in this company's address" string are still English placeholders — both already called out as open items by the author, not new findings from this review.
No inline comments to add given the above.
Ready for human approval ✅
Draft: needs a product ruling, not a review pass. One commit, stacked on #414.
Changes how "is billing a distinct address" is answered, and that question has now been answered four different ways in four review rounds, each fix trading one error class for another. The next answer should be a decision rather than another iteration.
What it does
billingIsDistinct()becomes the active "same as shipping" checkbox unchecked, AND the quote holding a billing address whose cache key differs from the shipping address's. Today it is the billing company field's visibility.That fixes a real false positive: a third-party re-render (Fire Checkout, Amasty) detaches the billing fieldset for an instant, so a visibility answer goes transiently false while neither the buyer's intent nor the quote has changed. With the seed routed on that answer, the quote's billing company gets written into the shipping identity — one panel's event changing the other panel's field.
billingRoleIdentity()takes no argument, so the resolver's choice and the seed's destination read one predicate and cannot disagree. The resolver itself is untouched: code-identical tostaging, one JSDoc line aside.Why it is not mergeable as it stands
It introduces a false negative of the same class. The billing panel's mount predicate remains the fieldset's visibility, so the two are still different questions.
On a saved-shipping-address, non-virtual, search-enabled checkout: the billing panel mounts, which blocks the tile, so the shipping panel has no mount at all — the billing panel is the only company control on the page. A buyer picks a company and billing captures it. The quote has not yet diverged from shipping, so
billingIsDistinct()is false, and the resolver returns shipping's empty identity. The only capture on the page is discarded. Reproduced, not inferred.The two candidate rulings
Arbitrate the mount on
billingIsDistinct(), so the billing panel does not appear until the quote diverges. Coherent, but it hides a panel the buyer has explicitly asked for and puts a DOM-visibility dependency back in the other direction.Or make
billingIsDistinct()true when the active box is unchecked and the billing field is a live mount, with the quote as fallback only where no box exists. This treats an unchecked box plus a rendered billing field as the buyer stating they have a separate billing address, and the lagging quote as simply behind. This is the recommendation.A note on the asymmetry either ruling inherits: re-checking the box is authoritative immediately, ahead of the quote, because that is the buyer's stated intent; unchecking becomes distinct only once the quote actually holds a second address. During that window billing's identity is empty, so the resolver falls back to shipping and nothing is lost — claiming "distinct" before a second address exists would simply be false.
Tests
53 suites / 942 tests on top of #414's 929, all green. The commit carries its own tests for the transient-fieldset case and for re-resolution on a quote notification with the checkbox untouched.