Skip to content

TWO-24754/feat: Sole trader checkout for supported countries#326

Open
dgjlindsay wants to merge 3 commits into
doug/TWO-24751-terms-chipsfrom
doug/TWO-24754-sole-trader
Open

TWO-24754/feat: Sole trader checkout for supported countries#326
dgjlindsay wants to merge 3 commits into
doug/TWO-24751-terms-chipsfrom
doug/TWO-24754-sole-trader

Conversation

@dgjlindsay

Copy link
Copy Markdown

What

Sole-trader checkout support (UK + US), stacked on #323. Buyers in countries where Two legally supports sole traders see a Registered company / Sole trader toggle in the payment box. Sole-trader mode suppresses company search, mints delegation + autofill tokens server-side, opens Two's hosted signup popup, and autofills the buyer's TWO:ST… organization number from GET /autofill/v1/buyer/current.

Design

  • Two gates, both server-side (per TWO-24754): the registry endpoint GET /registry/v1/supported-company-types/<ISO> (TWO-24753, must be deployed first) and a new enable_sole_trader admin toggle (default off). Registry answers are session-cached for the endpoint's own Cache-Control max-age and fail soft to registered-business only — checkout never blocks on the registry.
  • Business logic in PHP, JS renders only (TWO-24767 posture): WC_Twoinc_Sole_Trader owns eligibility + token minting behind two wc-ajax endpoints (two_sole_trader_availability, two_sole_trader_tokens); twoincSoleTrader in twoinc.js is a presentation module mirroring the terms-chips split.
  • Order payload unchanged. Per the TWO-24749 spike there is no plugin-side buyer-type field — the TWO:ST org-number prefix carries the semantics and checkout-api enriches company_type itself. The ticket's original "personal name + trading name fields" scope was superseded by the spike: those are collected by Two's hosted signup, exactly as in the Magento reference.
  • Tokens are minted with the merchant API key (never exposed to the browser) and read from the two-delegated-authority-token response header, matching Magento's adapter. Popup completion arrives via postMessage ACCEPTED from the checkout-page origin.
  • twoinc_sole_trader_signup_url filter lets brand overlays adjust the signup URL. WC-ABN itself needs no sole-trader work: the ABN product is NL-only and NL fails the registry gate.

Tests

9 new unit tests: both-gates matrix, registry error/404/malformed-body fail-soft, malformed-country short-circuit, per-request caching, case-insensitive token-header read, fail-closed token pair, signup URL env + filter. Full suite green (make test-unit).

Deploy order

checkout-api TWO-24753 (PR 12277) must deploy before this releases; until then the registry call 404s and the toggle simply never shows (fail-soft path, manually exercised by the 404 unit test).

🤖 Generated with Claude Code

Buyers in countries where Two supports sole traders (per the new
GET /registry/v1/supported-company-types/<ISO> endpoint, TWO-24753) can
switch the checkout to sole-trader mode, register or log in through
Two's hosted signup popup, and have their TWO:ST organization number
autofilled from GET /autofill/v1/buyer/current. The order payload is
unchanged — the backend derives the company type from the org-number
prefix (TWO-24749 spike), mirroring the Magento reference flow.

Two gates decide visibility, both server-side: the registry endpoint's
country answer (session-cached for its Cache-Control max-age, failing
soft to registered-business only) and a new enable_sole_trader admin
toggle (default off). Business logic lives in WC_Twoinc_Sole_Trader
with two wc-ajax endpoints; twoinc.js renders only (TWO-24767 posture).
Delegation + autofill tokens are minted server-side with the merchant
API key and read from the two-delegated-authority-token response header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown

🖌 Pre-commit success 🏆

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

Exit code: 0

Author ✍️@dgjlindsay

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request implements a sole trader checkout feature for the WooCommerce integration, adding a front-end toggle, hosted signup popup integration, and server-side logic to check country eligibility and mint delegated authority tokens. The review feedback highlights two important improvements: safely encoding the signup popup prefill data to prevent errors with non-ASCII characters when using btoa(), and securing the public ajax_tokens endpoint by verifying that the user has an active cart session to prevent token-minting abuse.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread assets/js/twoinc.js
Comment on lines +1232 to +1233
"&autofillData=" +
encodeURIComponent(btoa(JSON.stringify(prefill)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Using btoa() directly on a stringified JSON object can throw an InvalidCharacterError if any of the fields (such as the buyer's name, company name, or address) contain non-ASCII/UTF-8 characters (e.g., accented letters, smart quotes, or non-Latin characters). To safely encode the string to Base64 in a cross-browser compatible way, first encode it using encodeURIComponent and unescape before passing it to btoa().

Suggested change
"&autofillData=" +
encodeURIComponent(btoa(JSON.stringify(prefill)));
"&autofillData=" +
encodeURIComponent(btoa(unescape(encodeURIComponent(JSON.stringify(prefill)))));

Comment on lines +200 to +203
if (!$gateway || !self::is_enabled($gateway)) {
wp_send_json_error('Sole trader checkout is not enabled');
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-medium medium

The ajax_tokens endpoint is a public wc_ajax action that can be queried by any guest or bot. Since it performs external API requests to Two's servers to mint delegated authority tokens using the merchant's API key, it is highly recommended to verify that the user has an active checkout session with a non-empty cart. This prevents potential rate-limiting, key suspension, or resource exhaustion from automated token-minting spam.

            if (!$gateway || !self::is_enabled($gateway)) {
                wp_send_json_error('Sole trader checkout is not enabled');
                return;
            }
            if (function_exists('WC') && (null === WC()->cart || WC()->cart->is_empty())) {
                wp_send_json_error('Cart is empty');
                return;
            }

dgjlindsay and others added 2 commits June 12, 2026 12:31
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The registry endpoint no longer advertises REGISTERED_BUSINESS —
registered businesses need no registry enrollment, so the response now
lists only enrollable types and an empty list is a valid answer meaning
registered-business-only checkout. Empty/error fallbacks therefore
resolve to an empty list instead of coercing to [REGISTERED_BUSINESS],
and the now-unused constant is dropped. Behaviour is unchanged: the
sole trader option shows iff SOLE_TRADER is present.

Co-Authored-By: Claude Fable 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.

1 participant