# Blueprint: `commerce`

> Audience: AI coding agents first. A blueprint is a **worked example** in `apps/consumer`, not a shipped
> product (delivery-stack §4.1). It proves the preset's modules **compose**; it does not implement a
> store. If you find yourself adding a cart UI, checkout routes, a products table, or a chart of
> accounts here — stop, that is the generic-platform trap. Those are host-owned.

## Preset

`registry.json → presets.commerce` =
`util · db · billing · tax · ledger · uploads · search`

The preset is import-closed (see `registry.json → presets._closure`). All seven are already in
`apps/consumer/package.json` devDependencies (shared with saas-admin/community), so this blueprint also
proves the **preset installs**.

## The worked flow

The **product lifecycle — list, then sell**:

```
uploads (is this product image real?)  +  search (index it)
  →  tax (split the inclusive price PRE-CHARGE — Pattern C)
    →  billing.settleCharge (inject the ledger seam — Pattern B)
      →  ledger (record +gross in the append-only journal)
```

The composition proof lives in `apps/consumer/tests/blueprint-commerce.test.ts` — one flow, three
cases (valid list+sell · money-idempotent re-settle · forged image). Two headline properties:

1. **Money idempotency (hard floor):** settling the **same** `chargeKey` twice records exactly **one**
   ledger entry. billing keys the ledger write `idempotencyKey(['charge', chargeKey])` and ledger
   `onConflictDoNothing`s it — a provider redelivery or double-submit can never double-credit. The
   idempotency proven here is **ledger-level**: the fake provider records two `charge` calls (its own
   naivety, not a real double-charge), but the **money lands in the journal once**. Real *provider*
   idempotency is the host's job via the `chargeKey` it passes the provider — a separate guarantee
   from the ledger-seam dedup this blueprint exercises.
2. **Catalog fail-closed (trust boundary):** a forged product image (a script-bearing SVG posing as
   `image/png`) is rejected at the upload gate **before** the product is listed or charged — nothing
   indexed, nothing in the ledger.

## The two patterns this blueprint exists to demonstrate

- **Pattern B (ledger ABOVE billing).** billing never imports ledger internals; it receives a
  `LedgerSeam` = `{ appendEntry }` bag and funnels settlement/refund through it. `wiring/ledger.ts`
  supplies that bag from ledger's real `appendEntry`. **Seam-friction finding:** billing's `LedgerSeam`
  is generic (`S extends Schema`) while ledger's `appendEntry` is narrowed (`S extends LedgerSchema`),
  so they do not unify directly — the host bridges with the cast `appendEntry as LedgerSeam['appendEntry']`.
  This is the **module author's own** pattern (`asLedgerSeam()` in `packages/billing/src/index.test.ts`),
  sound because `appendEntry` only inserts into `ledger_entries`. Reproduced, not invented.
- **Pattern C (tax is a shared primitive, resolved pre-charge).** For **inclusive** pricing (IL/EU) the
  rate is resolvable before the charge, so the host splits net/vat in `wiring/tax.ts` and passes only
  the final amount to billing — billing imports **zero** tax (verified). The exclusive (US, at-charge)
  path is the one that could hide in billing; this blueprint shows the inclusive path that *motivates*
  the standalone module.
- **Pattern A (invoice-on-charge = config, not a module).** The provider's `emitsInvoiceOnCharge` flag +
  the `documentUrls` invoice on the settled result ARE the resolution — there is no
  `@platform-modules/invoicing`.

## Module roster — wired-and-exercised vs deferred

| Preset module | Role in this blueprint | Wiring file |
|---|---|---|
| `uploads` | **trust boundary** — `magic-bytes` sniff + `size-limit` cap on the product image (subpath-only) | `wiring/uploads.ts` |
| `search` | index the product; make it findable through `searchEntities` (one 'product' provider) | `wiring/search.ts` |
| `tax` | resolve the VAT rate for the order date + split the inclusive gross into net/vat (Pattern C; subpath-only) | `wiring/tax.ts` |
| `billing` | `settleCharge` funnels a provider charge into the injected ledger seam; provider = the variation axis | `wiring/billing.ts` |
| `ledger` | the `{ appendEntry }` seam billing injects (Pattern B); the append-only journal is the source of truth | `wiring/ledger.ts` |
| `util` | transitive L0 leaf (`search` imports `util/fts`; `billing` uses `idempotencyKey`); not directly wired | — |
| `db` | substrate — the `Querier` ledger writes through; stood up in `wiring/ledger.ts` | (in ledger wiring) |

Everything in the preset has a natural role in the list→sell flow, so unlike saas-admin/community there
is no "deferred" module here — but the WEIGHT is on the settlement (the headline). The catalog steps
(uploads+search) are the minimal listing preamble, re-using the same trust-boundary + index seams the
community blueprint proved (a deliberate convergence, not redundancy).

## Refund / webhook — NAMED, not exercised

billing's `refundCharge` (mirror of settle through the same seam, negative delta) and `ingestWebhook`
(the M5 atomic single-winner claim + module-owned post + host dispatch) are billing's own tested surface.
The blueprint proves the **settle→ledger** compose + its idempotency; re-running the full webhook state
machine here would be the generic-platform trap. A real host mounts `ingestWebhook` at a route and backs
`DedupStore` with a Postgres `ON CONFLICT` claim (recipe in billing's source).

## Blueprint-owned vs host-owned boundary

- **Blueprint-owned (here):** the five per-module wiring seams + the composition test. The wiring files
  instantiate each module's REAL exports with consumer-side fakes (capture PaymentProvider, in-memory
  pglite ledger + product index, fixed VAT schedule) — just the instantiation seam (adapter-minimalism).
- **Host-owned (NAMED, not implemented):**
  - **Cart / checkout / product UI** = a host-copied shadcn block over Radix (ui-primitives KILLED). No UI ships.
  - **Routes / endpoints** = host web-standard `Request`/`Response` handlers (incl. the billing webhook route).
  - **The product entity, the order record, the refundable-amount authority, the VAT-liability accounts** =
    host domain. The blueprint represents a sale only as an indexed product + a journal entry.
  - **The real PaymentProvider (stripe/sumit), R2/S3 storage, FTS provider, `DedupStore`** = host adapters
    injected where the fakes sit.

## What this blueprint is NOT

A package · a published artifact (blueprints are NEVER in the publish set — only the modules ship) ·
styled UI / routes / entity schemas · a generator.

## Convergence note (delivery-stack §7 — generator readiness)

Third hand-built blueprint. It **reuses** the uploads-trust-boundary + search-index seams from community
verbatim (only the noun changes), confirming that part of the scaffold. It **adds** a new axis the first
two lacked: a multi-module MONEY funnel (tax→billing→ledger) whose seam is a `{ appendEntry }` bag + a
provider, and whose headline property is idempotency rather than a single auth/upload gate. A generator
must therefore template not just "gate → side effects" but also "pre-charge primitive → injected-seam
funnel → append-only journal." Marketplace (commerce+community) is the last convergence check before §7's
generator decision.
