| 1 | # Merch integration |
| 2 | |
| 3 | Source preparation for `/[locale]/merch` and `/[locale]/merch/contributor`. The collection includes an independent merch-interest form; checkout, supplier ordering and live deployment remain inactive. Payment configuration defaults to unavailable. The footer links to the collection. |
| 4 | |
| 5 | ## Merch interest and price feedback |
| 6 | |
| 7 | `GET /api/merch/interest` reports availability; `POST` records an explicit opt-in only when real `CURATED_KV` and `MERCH_INTEREST_LIMITER` bindings are available. This works independently of Stripe, Yoycol and `MERCH_DB`; it neither creates nor prices an order. The native limiter is configured as 5 requests/minute under namespace 913002. A normal Next server without bindings leaves signup disabled. There is no in-memory success path. |
| 8 | |
| 9 | The form offers 12 item choices: four cotton tees, an ombré polyester/spandex tee, a polyester/spandex polo, heavier polyester fleece and lighter scuba hoodies, mousepad, deskmat, stickers and the planned doll. Materials remain candidates until sample verification. CNY/USD price ladders are optional research answers, with shipping/tax extra; they never replace the disabled commerce quote. Contributor merch remains a separate at-cost flow. |
| 10 | |
| 11 | Records are private, unverified email opt-ins under `private:merch-interest:v1:<email-sha256>`. Each submission replaces that email's choices and expires after 180 days. Email, country/region, consent, locale, currency and validated item/budget choices are stored; no address/payment is requested. `GET /api/admin/merch/interest?limit=50` and JSON `DELETE /api/admin/merch/interest` with `{ "id": "email-hash-from-list" }` use the existing maintainer-token/session authentication. Deletion also requires the allowed site Origin. No public list/count or email delivery service is connected; future mail must include working opt-out handling. |
| 12 | |
| 13 | Production origins are `https://codewhale.net` and `https://www.codewhale.net`. `MERCH_INTEREST_SITE_ORIGIN` can explicitly select a reviewed origin, including confined local preview. Do not add public HTTP origins or arbitrary CORS. See [shared research choices](./interest-options.ts), [signup and administrative handlers](./interest.ts), and [validation tests](./interest.test.ts). |
| 14 | |
| 15 | ## What is implemented |
| 16 | |
| 17 | - The storefront presents eight apparel styles: six everyday options plus Mascot Tee and Ocean Wave Mascot Hoodie. Six styles are solid; the lead mascot hoodie has cobalt waves/stars and a White alternative; Tide alone is ombré. Each has two proposed options, sixteen apparel combinations before sizes. The original blue wave-and-stars board leads the visible mascot section. A separate, prominent 20cm plush section follows it, with prototype-planned status; the catalog keeps kind concept and classifies it as prototype, so no doll ordering is enabled. Cotton leads solid tees, stretch polyester the polo/gradient tee, 230gsm scuba the budget wave hoodie, and 320gsm polyester fleece the Everyday/Signature hoodie. All colors and exact print/fabric outcomes remain concepts awaiting supplier quotes and samples. No new variant or sale activation is configured. |
| 18 | - A centimetre-first, optional-inch size guide contains 64 exact rows from seven official supplier charts, defaulting to DJCTX solid cotton DTF. Separate AOP cotton/stretch tees, polo, scuba/polyester-fleece/cotton-loopback hoodie charts remain. Raw supplier Bust labels remain unchanged; its measurement method and flagged anomalies need clarification. Garment measurements do not activate purchase variants or establish body-size recommendations. |
| 19 | - Eight earlier single-front plain-tee candidates remain under an archive disclosure: Signature, Classic, Wordmark, Ocean line, use codewhale pocket, Whale Bro, use codewhale. and Compiling. The earlier core board also includes Field, a two-placement concept excluded from Checkout. The old disabled quote route supports White cotton tees; its prices do not price the current mixed collection. Colored garments remain studies. |
| 20 | - Contributor flow accepts GitHub identity **or** contribution URL; hoodie/deskgear/seasonal/plush concepts remain non-purchasable. Contributor text has 19 language choices; native-speaker print review and a separate saved design are required for each offered language/phrase. |
| 21 | - Apparel uses quiet logo/text branding alongside one self-contained mascot badge. Six styles keep solid bodies, the mascot hoodie adds waves/stars with a White alternative, and Tide remains the one gradient tee. Pattern and gradient routes need their own AOP process and quote. The existing mascot source is 1,254px square, about 159ppi at 20cm before margins, and is not a verified 300ppi print master. DJCTX no-gradient guidance needs print-compatibility review for the shaded badge; its simple-print price cannot quote mascot variants. The wave hoodie uses the cheaper 3PH 230gsm scuba candidate; the Everyday/Signature hoodie remains 3TPH 320gsm fleece. Both supplier candidates require complete AOP panels and list a black inner hood, differing from the White concept lining. These renderings do not quote the finished products. The complex Stage Dive narrative art belongs to desk gear. Retail quotes must retain at least 30% of merchandise revenue after recorded production, postage, modeled processing fees and a 5% merchandise reserve. This is a guard against recorded costs, not a profit guarantee. Contributor prices have neither this margin floor nor a reserve. |
| 22 | - Server-only Yoycol V4 HMAC, fixed HTTPS destination, bounded responses, exact SKU/region/service lookup. Regional first/additional-piece rates are not an exact-address, mixed-cart or tax quote. One design/size per checkout, quantity up to five; no mixed-product cart. |
| 23 | - Fifteen-minute stored quotes bind variant, design, quantity, full address, currency, cost and shipping. Browser prices are ignored. Approved postal prefixes bound the first rollout; unsupported/remote destinations are blocked. Do not enable a prefix without checking all state/city/address exclusions. The merchant must cover any freight difference inside accepted coverage or obtain a separate reviewed quote; never charge an unresolved delivery price. |
| 24 | - A changed supplier mapping or country review, including shipping service, cost, fees, FX, address coverage or tax wording, invalidates outstanding website quotes before opening or reusing Checkout. Sessions already opened in Stripe retain their frozen terms until their session expiry; pausing sales does not cancel those sessions automatically. |
| 25 | - Stripe SDK 23.0.0 hosted Checkout with separate shipping, stable idempotency, a dedicated payment-method configuration and a frozen delivery address. A session expires 30 minutes after its quote expiry, satisfying Stripe's minimum while the website quote remains usable for 15 minutes. Address edits require a new quote. Return URLs confer no payment authority. |
| 26 | - Delivery is a separate ordinary Checkout line item, not `shipping_options`/`shipping_cost`; the server supplies PaymentIntent shipping and the supplier uses the stored address. This avoids relying on unverified shipping-options behavior without Stripe's address collector. Reconcile delivery from the stored quote/line item, not Stripe's `amount_shipping` field. Contributor processing is disclosed on the website and included in the printed item's cost-recovery price at Checkout. |
| 27 | - Raw-body signature verification, retrieved session amount/currency/product reconciliation, mode checks, and durable SQLite/D1 payment receipts. Completed-but-unpaid events do not fulfill; delayed successes can record a paid receipt. Duplicate events cannot create another supplier order. Pausing new sales leaves webhooks active. |
| 28 | - Maintainer-authenticated manual preview, supplier submission, ambiguity reconciliation and raw tracking refresh. A compare-and-set lock precedes the supplier POST. Ambiguous writes are held, never blindly retried. Customer payment, supplier creation, supplier funding, production, shipment and delivery remain separate evidence. |
| 29 | |
| 30 | ## Bindings and secrets to configure after approval |
| 31 | |
| 32 | Use a **separate D1 database**, bound as `MERCH_DB`, with `web/migrations/merch/0001_merch.sql`. Do not apply this schema to a product balance store or community KV. Add a Cloudflare rate-limit binding named `MERCH_RATE_LIMITER` using an unused namespace ID and a reviewed limit. Neither binding is silently replaced by an in-memory production store. |
| 33 | |
| 34 | Configure server secrets `MERCH_STRIPE_KEY`, `MERCH_STRIPE_WEBHOOK_SECRET`, `YOYCOL_ACCESS_KEY`, `YOYCOL_SECRET_KEY`; server configuration `MERCH_PAYMENT_CONFIGURATION` and `MERCH_CONFIG_JSON`. Keep keys out of browser bundles, committed config, logs and screenshots. The example JSON contains **unverified fee/FX planning assumptions**, blank identifiers and disabled gates; it is not production configuration. |
| 35 | |
| 36 | Use an approved Stripe account and a separate sandbox first. Create a dedicated Codewhale merch payment-method configuration with cards and eligible Alipay enabled; this avoids changing other Codewhale purchase flows. Pass its real `pmc_` ID. Dynamic methods display only eligible methods for account/currency/location; the source does not prove Alipay is enabled. Account-specific implementation planning is blocked until the Stripe connector reauthenticates. Register `/api/merch/webhook` for completed, asynchronous success/failure and expired Checkout events, then verify the exact endpoint secret and mode. The SDK uses its current default API version; pin/change versions only after account compatibility review. |
| 37 | |
| 38 | Enable each country only after its saved design, SKU/cost, currency of supplier rates, postcode coverage and tax/import-charge responsibility are verified. Do not copy a China rate to another country or a heavier tee’s rate onto DJCTX. The earlier plain-tee quote UI describes a white 180gsm cotton tee; mappings must use that approved garment. Ombré, polo, wave/scuba and fleece routes need their own product types, variants and pricing before ordering. Doll development is a separate manufacturing route; no apparel quote or payment eligibility extends to it. A different regional blank requires visible material, measurements and origin disclosure before activation. No EU, US import or worldwide DDP assumption is built in. |
| 39 | |
| 40 | The product-country record's scalar `productionCostUsd` must be the accepted final print/variant cost for every size/design mapped in that record. Do not populate several sizes with different costs behind one scalar, particularly for the at-cost edition. Restrict offered mappings to confirmed equal-cost variants or extend the adapter to individual variant/print costs before offering others. |
| 41 | |
| 42 | **Tax limitation:** this first adapter does not calculate/remit retail sales tax, VAT or customs. `taxReviewed` means the merchant has established that the offered fixed merchandise/production amount and disclosed external charges satisfy the actual route; it is not a tax engine. Keep any jurisdiction needing additional collected tax disabled until that calculation, invoice/remittance and contributor cost treatment are implemented and reviewed. Never turn a tax disclosure into an exemption claim. Contributor fees use reviewed planning rates, not knowledge of the customer's final payment rail; reconcile actual expenses and refund any cost recovery excess after settlement under a clear policy. |
| 43 | |
| 44 | ## Manual fulfillment |
| 45 | |
| 46 | `POST /api/admin/merch` reuses the existing maintainer session (`mt_sid`) and requires the exact configured Origin and JSON content type. It does not expand community-agent automation. Supported action bodies: |
| 47 | |
| 48 | ```json |
| 49 | { "action": "list" } |
| 50 | { "action": "preview", "orderId": "stored-quote-uuid" } |
| 51 | { "action": "submit", "orderId": "stored-quote-uuid", "confirmation": "CREATE_UNPAID_SUPPLIER_ORDER" } |
| 52 | { "action": "reconcile", "orderId": "stored-quote-uuid", "supplierOrderId": "actual-yoycol-id" } |
| 53 | { "action": "refresh", "orderId": "stored-quote-uuid" } |
| 54 | ``` |
| 55 | |
| 56 | Preview/list are read-only. Submission additionally requires live mode and `supplierSubmissionEnabled:true`; test Stripe payments cannot create real supplier orders. Inspect the frozen address/name mapping and final saved print in Yoycol before enabling. Submission records `supplier_cost_review`: review actual amount/shipping/taxes and separately fund the supplier in its Dashboard after the concrete order is approved. The public API has no verified supplier-pay operation or automatic-funding contract for this independent API Store. This code never pays Yoycol. |
| 57 | |
| 58 | If submission crashes or times out, use the order's `CW-<uuid>` external reference to inspect Yoycol manually. Do not switch the state back to pending. Reconcile only a verified supplier order ID; the handler retrieves and matches the external reference before recording it. `refresh` records supplier receipt/tracking without guessing undocumented numeric status codes. Refund/customer notification handling and an operator UI remain manual; no emails or partner contacts are sent by these routes. |
| 59 | |
| 60 | Quotes contain delivery PII and contributor context in durable server storage. Limit maintainer access and set a documented retention/deletion policy before launch; no analytics event or supplier request receives contributor identity. Operate backups, refund/reconciliation review and working capital separately from Stripe payout timing. |
| 61 | |
| 62 | ## Proof boundary |
| 63 | |
| 64 | Focused tests use real SQLite constraints/transactions, the actual Stripe SDK with signed fixture webhooks, and mocked network responses. They prove local behavior; they do not prove account authentication, deployed D1, live Stripe/Alipay, Yoycol mapping/order acceptance, tax obligations, physical print quality or international delivery. Browser access was denied by the desktop administration-policy check; no newer storefront screenshot is claimed. |
| 65 | |
| 66 | Primary contracts: [Yoycol API](https://www.yoycol.com/api/2025/redoc), [Stripe Checkout](https://docs.stripe.com/api/checkout/sessions/create), [Stripe fulfillment](https://docs.stripe.com/checkout/fulfillment), [Alipay](https://docs.stripe.com/payments/alipay), [payment-method configurations](https://docs.stripe.com/payments/payment-method-configurations). |
| 67 |