Week 3: checkout + order engine (Helcim gateway, pre-charge recheck, order state machine)

Wires up Helcim's free WooCommerce plugin (installed inert until a sandbox
token exists) alongside a synchronous pre-charge supplier/margin recheck
and a fraud-check hook, both firing immediately before payment per the
Appendix B immediate-charge decision. Adds a custom order-status sequence
(bsc-ordered/bsc-shipped/bsc-exception) as the state machine and review
queue, an Action Scheduler-backed supplier-submission job with idempotency
guards, a bsc_order_audit ledger, and two new transactional emails.

Hooks both the classic WooCommerce checkout actions and the real
woocommerce_checkout_validate_order_before_payment hook, since this site's
storefront runs WooCommerce Blocks/Store API rather than the classic
shortcode checkout (confirmed by reading WooCommerce's own source after
the classic hooks turned out not to fire in live testing).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2gmgaDVXcRGCwLfdXQajD
This commit is contained in:
twooeyandClaude Sonnet 5 committed 2026-08-27 20:03:32 -04:00
1 parent 73c64d94c1
commit 4079bfceac
20 files changed
+1224 -15

No files matched your search

+80 -6
View File
@@ -5,10 +5,11 @@ pre-launch plan and the technical design doc (`bookstore-core` schema,
interfaces, order state machine — the design doc is the source of truth for
*why* this repo is laid out the way it is).
This week's scope (Week 1 of the 5-week plan): environments, deploy
pipeline, and the WooCommerce/HPOS plugin scaffold. Catalog schema, pricing,
supplier adapters, and the order state machine are Week 2+ and live under
`wp-content/plugins/bookstore-core/includes/`, currently empty.
Weeks 1–2 of the 5-week plan are done: environments, deploy pipeline, the
WooCommerce/HPOS plugin scaffold, catalog schema, Gutenberg importer,
pricing engine, and membership shipping all live under
`wp-content/plugins/bookstore-core/includes/`. Checkout, the order state
machine, and live supplier integrations are Week 3+.
## Layout
@@ -97,6 +98,31 @@ After that, the starter site's content and Customizer settings persist in
the database like any other WordPress content — a redeploy or a fresh
`deploy.sh` run doesn't touch it or need to repeat it.
### Payments: Helcim
`deploy.sh` installs and activates the free, wordpress.org-hosted **Helcim
for WooCommerce** plugin (`helcim-commerce-for-woocommerce`) automatically
in every environment — nothing to do here for that part.
The plugin needs a Helcim **sandbox API token**, which isn't something a
public API can hand out, so it's a one-time manual step per environment:
1. Sign up for a free Helcim account.
2. Helcim dashboard → **Integrations → API Configuration** → create an API
token.
3. Put it in `HELCIM_API_TOKEN` in `.env.<environment>`, then re-run
`make deploy ENV=<environment>`.
`deploy.sh` writes the token to a gitignored secrets file the same way it
already does for `HARDCOVER_API_TOKEN` (see `secrets/<env>/` above), and
`Payments\HelcimConfig::ensure_configured_from_env()` seeds the gateway's
WooCommerce settings (sandbox mode, disabled by default — a human still
enables it at checkout once ready) the first time it finds a token. Until a
token exists, the plugin is installed and inert; nothing else in this repo
is blocked on it. Go-live (switching the gateway from sandbox to Helcim's
live environment) is a deliberate manual step on the settings screen, not
something this repo automates.
## Deploy pipeline
The Git server (Gitea) and the staging/production Docker hosts are separate
@@ -140,10 +166,58 @@ run a real restore drill:
`restore.sh` only ever targets staging — there's no production argument, so
a restore drill can't accidentally overwrite the live site.
## Catalog pipeline (Week 2)
Staging data is built with `wp bookstore` sub-commands (see `wp bookstore
help` for full options):
```
wp bookstore import-gutenberg --limit=10000 # Work/Edition/ISBN rows from the real PG catalog feed
wp bookstore generate-offers # synthetic SupplierOffer rows: stock, condition, price
wp bookstore sync-products # one WooCommerce product per Work
wp bookstore sync-covers --limit=20 # optional: real cover art via Open Library
wp bookstore sync-hardcover-tags --limit=20 # optional: genre/mood/content-warning tags
```
`import-gutenberg --limit` counts only *new* titles per run, so re-running
with a bigger `--limit` continues forward instead of rescanning. Pricing
(25% target margin) and the $25/$15 free-member shipping thresholds are in
`includes/Pricing/PricingEngine.php` and
`includes/Shipping/BSC_Free_Shipping.php`.
## Checkout + order engine (Week 3)
Helcim's WooCommerce plugin is installed (see "Payments: Helcim" above),
paired with a synchronous pre-charge supplier/margin recheck and a
fraud-check hook — both run right before the payment gateway is called
(`Order\CheckoutRecheck`), never after. A passing order moves through a
custom status sequence beyond WooCommerce's own pending/processing/
completed: `bsc-ordered` (submitted to the supplier via Action
Scheduler, `Order\SupplierOrderJob`) → `bsc-shipped` → `completed`. Any
failure at any stage — no sellable offer, margin no longer holds, fraud
check fails, or the supplier submission itself fails after charging — lands
the order in `bsc-exception` ("Needs Review"), which is the review queue:
just a filterable WooCommerce order status, not a separate table. A
post-charge failure also triggers an automatic refund
(`wc_create_refund(..., refund_payment: true)`), per the design doc's
Appendix B decision that an undo is always a refund, never a void.
`wp bookstore force-offer-status <isbn13> <active|stale|discontinued>` (see
`wp bookstore help`) forces a specific ISBN's synthetic offer into a
specific state — `SyntheticOfferGenerator`'s rolls are random, so this is
how to reliably exercise the recheck's failure paths (price-increase,
out-of-stock, discontinued-after-charge) in a real checkout.
Real BooksRun/Ingram supplier adapters are still Week 4 — `SupplierOrderJob`
runs against the synthetic `gutenberg_test` supplier for now, deliberately
built through the same Action Scheduler/locking path Week 4's real
integration will reuse rather than restructure.
## What's deliberately not here yet
- Catalog tables, pricing engine, supplier adapters, order state machine —
Week 2–4, see the design doc.
- Live BooksRun/Ingram supplier adapters — Week 4, see the design doc.
- Automated tests for the pricing/shipping-threshold/checkout-recheck
logic — called for by the design doc's launch gates, not yet written.
- A real TLS-terminating public domain — Caddy will auto-provision certs
once `SITE_DOMAIN` points at a real host with 80/443 reachable; until
then `docker-compose.dev.yml` is the only one that works from