Skip to content

TWO-24746/feat: PrestaShop brand config foundation#32

Open
dgjlindsay wants to merge 7 commits into
doug/TWO-24741-remove-gross-snapfrom
doug/TWO-24746-brand-config
Open

TWO-24746/feat: PrestaShop brand config foundation#32
dgjlindsay wants to merge 7 commits into
doug/TWO-24741-remove-gross-snapfrom
doug/TWO-24746-brand-config

Conversation

@dgjlindsay

Copy link
Copy Markdown
Contributor

What

C3 of the plugin parity plan (TWO-24739 → TWO-24746): introduce a brand-config layer in the PrestaShop module so partner editions can rebrand without forking. No behaviour change for the Two brand.

Stacked on #31 (TWO-24741) — merge that first; base retargets when it lands.

How

  • brands/two.php — default brand config: provider, display/product names, payment-option title/subtitle defaults, support email + docs URL, logo path, and payload identity (vendor_name, brand_tag — empty for Two).
  • Twopayment::getTwoBrand() — lazy loader so every entry point (module ctor, AJAX controllers, test harness) gets the config. Constructor reads author/displayName/description from it, per the ticket's "PHP-accessible, not Smarty-only" requirement.
  • Smarty$two_brand assigned at the actionFrontControllerSetMedia hook (checkout pages) and in the payment-option render. Template {l s='...'} literals deliberately untouched: swapping them changes translation keys, which is TWO-24760's scope.
  • applyTwoBrandPayloadIdentity() — injects vendor_name/brand_tag into create, intent and update order bodies (symmetric across the lifecycle — lesson from the WC #320 review where the create/edit asymmetry was a bug). Keys only sent when non-empty, so Two-brand payloads are byte-identical to today.
  • Tests — brand loads + module-accessible; payload identity omitted for Two; applied when a brand sets it. Full harness green.

Test plan

  • php tests/run.php — all green (incl. 3 new)
  • php -l
  • Manual: checkout renders unchanged for Two brand on a dev shop

🤖 Generated with Claude Code

Lay the brand-config foundation so partner editions can rebrand the
module without forking it.

- brands/two.php: default brand config (identity strings, payment
  option defaults, support links, payload identity). Keys land with
  their consumers.
- Twopayment::getTwoBrand(): lazy loader, available from every entry
  point including the test harness (which skips the module
  constructor). Constructor assigns author/displayName/description
  from it.
- Smarty: $two_brand assigned at the front-controller setMedia hook
  and in the payment-option render, so templates can read
  {$two_brand.product_name} etc. Template {l s='...'} literals are
  deliberately untouched until the translations pass (TWO-24760) so
  existing dictionaries keep matching.
- applyTwoBrandPayloadIdentity(): vendor_name/brand_tag added to the
  create, intent AND update order bodies — symmetric across the order
  lifecycle — and only when the brand sets them, so the Two brand's
  payloads are byte-identical to before.
- Tests: brand loads and is accessible from module methods; payload
  identity omitted for the Two brand; applied when a brand sets it.

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!

@dgjlindsay

Copy link
Copy Markdown
Contributor Author

Adversarial review round 1 applied (3 Opus reviewers). Key correction: brand_tag removed from the payload identity — it is not an order-body field anywhere in the ecosystem (Magento uses it only as a checkout-URL query param; WC sends only vendor_name). The ticket's "vendor_name, brand_tag sent in POST /v1/order" premise was half right; payload carries vendor_name only. Also added: the partner substitution seam (PS_TWO_BRAND_CODEbrands/{code}.php merge), mid-deploy missing-file fallback, Tools::safeOutput on brand-derived admin HTML, and a real-builder payload-parity pin in tests.

Review findings (adversarial round 1):

- Drop brand_tag from the order payload: it is not an order-body field
  anywhere in the Two ecosystem — the Magento plugin uses it solely as
  a checkout-URL query param and the WooCommerce plugin sends only
  vendor_name. A partner setting it would have sent an unrecognised
  key on every create/intent/update. The key leaves the brand file too
  (no consumer) and lands with the checkout-URL code that reads it.
- Implement the partner substitution seam the file header promised but
  the code lacked: PS_TWO_BRAND_CODE configuration resolves to
  brands/{code}.php (basename-confined) and merges over the Two
  defaults, mirroring the WooCommerce mechanism.
- getTwoBrand() no longer fatals checkout when brands/two.php is
  briefly absent mid-deploy: is_file guard with inline Two defaults.
- Tools::safeOutput on brand-derived admin HTML (brand files are now
  an external-ish input surface); locals hoisted in the help panel.
- brands/two.php header gains a consumer map (which key feeds what).
- Tests: a REAL built payload is pinned to omit vendor_name/brand_tag
  for the Two brand (the wrapper test alone could not catch a builder
  regression); partner-brand test asserts brand_tag stays out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dgjlindsay and others added 5 commits June 11, 2026 09:53
Standard branding-framework element, now at parity: a brand may
declare ['min_order_amount', 'currency', 'billing_countries'] and the
payment option hides when unmet. Compares the NET basket (the funding
partner's server-side risk rule compares net); baskets in other
currencies convert via PrestaShop's own currency rates (cross rate =
target/source against the shop default), failing closed without a
usable rate — matching the Magento gate's posture. Country mismatch
and below-minimum both log why the option hid. The Two brand sets no
gate, so behaviour is unchanged until a partner edition declares one.

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

- availability_gate is a four-part config: amount + currency + basis
  (net|gross, explicit — ABN's funding-partner rule compares net, the
  platform's defaults are gross) + billing countries. The gate
  compares the cart total on the declared basis (getOrderTotal with
  or without taxes) and the shared compare helper converts via
  PrestaShop rates, failing closed without one.
- Merchant-set Minimum Order Value in Other Settings: the dynamic
  description shows the brand minimum it must exceed; validation
  rejects values at or below that floor; the gate enforces brand AND
  merchant minima (the merchant value rides the brand currency/basis
  when a gate exists, else shop default currency, gross). Configured
  value cleaned up at uninstall.
- No decline hint here yet: no PrestaShop brand carries a minimum and
  the module has no REJECTED-status message path to hang it on —
  recorded in the parity plan for PS-brand onboarding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The merchant-set Minimum Order Value is now interpreted in the shop
default currency rather than the brand minimum's currency:

- Save validation converts the brand floor into the shop default
  currency via PrestaShop's rates before comparing; without a usable
  rate the floor check is skipped on save (checkout gate enforces both
  minima independently and fails closed).
- Settings description shows the converted floor with the native value
  in brackets ('Platform minimum £215.73 (€250.00), excluding tax'),
  via the locale price formatter when available.
- getTwoMerchantMinimumOrder builds the tuple with the shop default
  currency; the gate's shared compare helper already converts
  cross-currency carts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirror of the WooCommerce/Magento decline hint (base-plugin parity):
when a Two decline is attributable to the brand minimum order value,
tell the buyer the minimum instead of a generic failure.

- New getTwoMinimumOrderDeclineHint(): keyed primarily on the API's
  machine-readable reason (decline_reason on intents and rejected
  orders, error_code on order-create 400s) with a strictly-below-
  minimum fallback; the minimum is shown converted into the cart
  currency via PrestaShop's rates, failing soft (no hint) without a
  usable rate.
- Wired at three decline surfaces: the authoritative intent check at
  payment submit, order-create 400 with ORDER_BELOW_MIN_INVOICE_AMOUNT,
  and 201-without-payment_url (REJECTED orders now get a 'not
  available for this order' message rather than implying a technical
  redirect fault).
- Shared convertTwoAmount() helper replaces the inline conversion in
  getTwoPlatformMinimumInDefaultCurrency.

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

Mirrors tonight's Magento and WooCommerce changes so all three plugins
read the same single source of truth:

- getTwoPlatformMinimumOrder() resolves min_order_amount/currency/basis
  from GET /v1/merchant/{id} (the value checkout-api enforces at order
  create/intent), Configuration-cached for 15 minutes; the no-minimum
  outcome is cached too, and a fetch failure resolves to no minimum
  (the 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.
- New 'Minimum Order Value Tax Basis' select (gross/net, default gross)
  feeding the merchant minimum; the value field's label carries the
  shop default currency ('Minimum Order Value, EUR').

(The version display this propagation round added to WooCommerce
already exists here: the config page footer shows plugin + PrestaShop
versions.)
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