Skip to content

TWO-24745/feat: Base brand seams for the ABN overlay migration#321

Open
dgjlindsay wants to merge 10 commits into
doug/TWO-24744-brand-configfrom
doug/TWO-24745-base-brand-seams
Open

TWO-24745/feat: Base brand seams for the ABN overlay migration#321
dgjlindsay wants to merge 10 commits into
doug/TWO-24744-brand-configfrom
doug/TWO-24745-base-brand-seams

Conversation

@dgjlindsay

Copy link
Copy Markdown

What

C1.5 of the plugin parity plan: the base-plugin seams the ABN overlay (TWO-24745 / C2) needs but the C1 brand layer could not express. Driven by an exhaustive fork-vs-base divergence inventory of woocommerce-abn-plugin.

Stacked on #320 — merge that first; base retargets when it lands.

Why these seams

The inventory found the four C1 hooks + brand keys cover identity and payload shape, but five divergences are hardcoded in base code paths:

Seam Why the overlay needs it
meta_prefix brand key routed through WC_Twoinc_Brand::meta_key() / prefixed_name() (order meta, user meta, confirmation params, nonce action) Live ABN stores hold _abn_* order meta. Without the prefix following the brand, every existing ABN order's reference/state/hash becomes unreadable after migration. The confirmation return-side (abn_order_reference/abn_nonce params, abn_confirm_ nonce) is the same prefix family — the outbound twoinc_confirmation_url filter alone could not cover the read side.
availability_gate brand config + woocommerce_available_payment_gateways filter The fork's €250/EUR/NL gate (front-end only, inclusive minimum). Config, not code, so the overlay declares ['min_order_amount' => 250, 'currency' => 'EUR', 'billing_countries' => ['NL']].
twoinc_payment_validation_error filter in process_payment The fork's required terms-acceptance checkbox validation — without a veto seam the overlay must duplicate process_payment().
twoinc_payment_description filter The fork ships wholly different payment-box copy (bullet list, "Enabled by ABN AMRO").
supported_buyer_countries brand key Fork pins the checkout JS to ["NL"].

Two brand values reproduce today's behaviour exactly (meta_prefix=twoinc, no gate, default countries); _tillit_* legacy fallbacks and HTML form input names stay literal (not data-bearing).

Test plan

  • 18/18 unit tests green on PHP 7.4 + 8.2 (new: prefix derivation under overlay brand, confirmation-URL params follow prefix, gate absent/unmet/exact-minimum boundary, validation veto)
  • php -l all changed files
  • e2e suite on this PR (Two brand, full checkout) — verifies the zero-behaviour-change claim

🤖 Generated with Claude Code

The fork-divergence inventory found five things the brand layer (C1)
cannot yet express that the ABN overlay needs. Add them to the base so
the overlay stays thin:

- Brand meta prefix: order meta (_twoinc_order_reference, _twoinc_
  merchant_id, _twoinc_order_state, _twoinc_req_body_hash,
  twoinc_order_id), user meta (twoinc_company_id etc.), confirmation
  request params and the confirmation nonce action all derive from the
  brand's meta_prefix via WC_Twoinc_Brand::meta_key()/prefixed_name().
  Live ABN stores hold data under _abn_* — the prefix is load-bearing
  for existing orders, so the overlay sets meta_prefix=abn and inherits
  its installed base's data. Two default 'twoinc': zero behaviour
  change. The _tillit_* legacy fallback reads stay literal (Two-only
  history). HTML form input names and $_POST keys stay literal
  (transient, not data-bearing).
- Brand availability gate: config-driven woocommerce_available_payment_
  gateways filter (front-end only, inclusive minimum, currency +
  billing-country match) mirroring the ABN fork's EUR 250/EUR/NL gate
  semantics. Two brand sets no gate.
- twoinc_payment_validation_error filter: a brand overlay vetoes
  process_payment with a buyer-facing error (ABN's required terms
  checkbox) without overriding the method.
- twoinc_payment_description filter: overlays replace the payment-box
  copy wholesale (ABN ships its own bullet list).
- supported_buyer_countries brand key feeding the checkout JS config
  (ABN is NL-only).

Tests: prefix derivation under an overlay brand, confirmation-URL
params following the prefix, gate absent/unmet/exact-minimum, and the
validation veto. 18/18 green on PHP 7.4 + 8.2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown

🖌 Pre-commit success 🏆

Details
Downloading virtualenv (7.3MiB)
 Downloaded virtualenv
Installed 11 packages in 12ms
prettier.................................................................Passed

Exit code: 0

Author ✍️@dgjlindsay

dgjlindsay and others added 3 commits June 11, 2026 00:05
Review findings (adversarial round 1):

- Log (WC logger, once per request) when the availability gate removes
  the gateway, with the failing basket and the gate criteria — removing
  a payment method is invisible to the merchant and a config typo would
  otherwise read as the gateway vanishing (same lesson as the Magento
  MinimumOrderGate review).
- Guard the gate against a truthy-but-partial availability_gate shape:
  missing criteria means config bug, not a basket decision — leave the
  gateway available rather than judging with absent keys.
- Document the registration deadline on twoinc_payment_description
  (applied once at gateway construction; register by plugins_loaded,
  same contract as twoinc_brand_file).
- Test the confirmation READ side: is_confirmation_page() must detect
  params under the brand's prefix and ignore another brand's — the
  read side is the half that strands in-flight orders if it drifts
  from the write side.

Reviewer-verified (no change needed): fork-created ABN orders fulfil
end-to-end under base+overlay; the prefix sweep is complete repo-wide;
the Two-brand path is byte-identical; rollback is safe. Known C2 note:
an overlay terms-line filter that doesn't reproduce the fork body
byte-for-byte costs one benign self-healing re-sync PUT per
pre-cutover order on first edit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… about-block seam

Two more divergences the overlay inventory surfaced once the gateway
id became brand-driven:

- twoinc.js and admin.js hardcoded 'woocommerce-gateway-tillit' in 17
  selectors (payment-method radio, payment box, settings field ids) —
  under an overlay's gateway id none of the checkout or settings JS
  would bind. Both now read the id from their localized config objects
  (window.twoinc.gateway_id / twoinc_admin.gateway_id).
- twoinc_about_html filter: the about block inside the payment-box
  subtitle is the piece of the description the ABN edition actually
  replaces (its own bullet list, no link). Narrower seam than
  rebuilding the whole description; twoinc_payment_description also
  now passes the gateway instance for get_option reads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The overlay review's deleted-functionality audit found one regression:
init_form_fields hardcoded the Two title default, so a FRESH install of
a brand overlay would show 'Business invoice - %s days' instead of the
brand's phrasing (the ABN fork defaulted to 'Achteraf betalen - Bestel
op factuur'). Existing stores are unaffected (saved titles win). The
default now comes from the title_default brand key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dgjlindsay and others added 6 commits June 11, 2026 08:55
The funding partner's server-side risk rule (risk-engine
funding_partner_rules) compares NET order value against the EUR 250
threshold; the gate compared the gross cart total, so a ~EUR 250-gross
basket (net ~EUR 207 at 21% VAT) showed the method and was then
declined at credit check. The gate now subtracts the cart tax. The
fork's gross compare was itself misaligned with the risk rule, so this
deliberately goes beyond fork parity (confirmed by Doug; risk rule is
the source of truth — its missing FX conversion is ABN-446). Gate log
line reports the net value; brands/two.php documents the semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…declines

Mirror of the Magento change: when a rejected order's net value is
below the brand's minimum or within 5% above it, append 'Note: <brand>
requires a minimum order value of <amount> <currency> excluding tax.'
to the decline message (checkout banner + merchant order note).
Same-currency baskets only — WooCommerce has no FX rate source, and
the gate already restricts the method to the gate currency anyway.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirror of the Magento changes (Doug's minimum-order review):

- availability_gate is a four-part config: amount + currency + basis
  (net|gross, explicit) + billing countries. The gate compares the
  basket on the declared basis.
- Decline hint keyed primarily on the API's machine-readable
  decline_reason (ORDER_BELOW_MIN_INVOICE_AMOUNT), strict
  below-minimum fallback — the 5% band is gone (Doug: accept the
  corner case). Wording: 'Minimum order value is <symbol><amount>
  excluding|including tax.' in the order currency.
- Merchant-set Minimum Order Value setting: dynamic description shows
  the platform minimum it must exceed; validate_..._field rejects
  values at or below the floor; the gate enforces platform AND
  merchant minima (merchant minimum rides the platform currency/basis
  when one exists, else store currency, gross; foreign-currency
  baskets fail open on the merchant's own bar — no FX source in WC).
- Fixed three sprintf formats I'd written as '%1\$s' in single-quoted
  strings — \$ is only an escape in double quotes, so the literal
  backslash reached sprintf (ValueError on PHP 8.2; 7.4 was silently
  lenient).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The merchant-set Minimum Order Value is now interpreted in the STORE
currency (the saved woocommerce_currency option, not the active
multicurrency display currency) rather than the platform minimum's
currency:

- Settings description shows the platform floor with its currency
  symbol; when the store currency differs the floor is shown in its
  native currency only — WooCommerce has no FX source until TWO-24776,
  so no converted figure can honestly be displayed.
- Validation compares against the floor only when the store currency
  matches the platform minimum's; otherwise the numeric floor check is
  skipped on save and both minima are enforced independently at
  checkout (gate fails open on the merchant's own bar cross-currency,
  platform gate still applies).
- New unit test covers the cross-currency skip; bootstrap gains
  get_option/get_woocommerce_currency_symbol stubs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ncy label, version line

Mirrors tonight's Magento changes so the WooCommerce plugin reads the
same single source of truth:

- get_platform_minimum_order() resolves min_order_amount/currency/basis
  from GET /v1/merchant/{id} (the value checkout-api enforces at order
  create/intent), option-cached for 15 minutes following the
  get_days_on_invoice() pattern; the no-minimum outcome is cached too,
  and a fetch failure resolves to no minimum (server still enforces).
- The availability gate, settings description, save-time floor
  validation and decline hint all read the API tuple; the brand
  config's availability_gate shrinks to a billing-country restriction.
  Each minimum is compared on its own basis.
- New 'Minimum Order Value Tax Basis' select (gross/net, default gross)
  feeding the merchant minimum; the value field's label carries the
  store currency ('Minimum Order Value, EUR').
- Settings page footer shows plugin/WooCommerce/WordPress versions,
  mirroring the Magento and PrestaShop version panels.
The availability gate judged the session cart unconditionally, so on
the pay-for-order page (where the cart is empty or unrelated to the
order being paid) any configured minimum removed the gateway from the
payment-link flow. Skip the value checks when there is no live basket
to judge; the billing-country gate still applies and the API still
enforces the platform minimum at order creation.
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