[object Object]

← back to Dw Signup Fulfillment

DEPLOY.md: rewrite runbook for Option C (verify→tag→Regios), retire old paths

9196a62cd382b1f1774e1f60a370ff8b61c59a66 · 2026-08-14 09:48:20 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 9196a62cd382b1f1774e1f60a370ff8b61c59a66
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 14 09:48:20 2026 -0700

    DEPLOY.md: rewrite runbook for Option C (verify→tag→Regios), retire old paths
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 DEPLOY.md | 171 ++++++++++++++++++++++++++++++--------------------------------
 1 file changed, 82 insertions(+), 89 deletions(-)

diff --git a/DEPLOY.md b/DEPLOY.md
index 4ee21f9..85f5e84 100644
--- a/DEPLOY.md
+++ b/DEPLOY.md
@@ -1,24 +1,22 @@
-# DEPLOY — dw-signup-fulfillment go-live runbook (path B: sample-locked shared code)
+# DEPLOY — dw-signup-fulfillment go-live runbook (Option C: verify → tag → Regios)
 
 This service is **build-to-ready**. It ships in `DRY_RUN=1` and does nothing
 irreversible until you deliberately flip it. Follow these steps IN ORDER. Do not
 flip `DRY_RUN=0` until every prior step is green.
 
-> **Wired behavior** (DTD 2026-07-28, 5/5 → path B):
-> - **Retail** — on `customers/create`, the service **emails the new customer ONE
->   shared code** (`RETAIL_SHARED_CODE`, e.g. `DWSAMPLES3`). That code is a **code
->   discount you create ONCE in the Shopify admin**, pointing at the deployed
->   **"DW Free Samples" Discount Function**, limited to **one use per customer**.
->   The service makes **no Shopify discount write** — it only sends the email.
->   Safety comes from the FUNCTION: signed-in non-trade → first 3 **Sample** units
->   free; it **never discounts the `$94.42` roll variant**. (This is why the function
->   deploy in Step 1 is REQUIRED, not optional — the code without the function behind
->   it is just a plain discount with no roll protection.)
+> **Wired behavior** (DTD 2026-08-14, Option C — replaces the gift-card / shared-code /
+> function paths, which are retired to reference alternates):
+> - **Retail** — a new/claiming customer gets a branded **"confirm your email to unlock
+>   N free samples"** letter (via `POST /claim` or the `customers/create` webhook). The
+>   `GET /verify?token=…` click appends the **`verified-sample`** customer tag. The
+>   store's **Regios tag-gated sample discount** (the SAME mechanism that already makes
+>   `trade` memos free — scoped to the **Sample** variant, so the `$80+` roll variant is
+>   never touched) then shows their samples free at checkout. The service's only Shopify
+>   write is the **tag** — no gift card, no discount object, no coupon code, no liability.
 > - **Trade** — `POST /trade/apply` → `pending` → reviewed at `/admin/trade` → on
->   **approve**: resolve the Shopify customer id (by email if the form didn't supply
->   one; **hard-fails** if no such customer exists — never writes a placeholder), tag
->   `trade` (unlocks free memos), assign the DW House Account, set `custom.assigned_rep`,
->   email rep + applicant.
+>   **approve**: resolve the Shopify customer id (by email; **hard-fails** if no such
+>   customer exists), tag `trade`, assign the DW House Account, set `custom.assigned_rep`,
+>   email rep + applicant. (Unchanged.)
 
 ---
 
@@ -27,62 +25,62 @@ flip `DRY_RUN=0` until every prior step is green.
 ```sh
 cd ~/Projects/dw-signup-fulfillment
 npm install
-node scripts/selftest.js      # must print ALL CHECKS PASSED and exit 0
+# Boot a throwaway instance and exercise the flow (does not touch the live :9856):
+PORT=9899 DRY_RUN=1 node server.js &   # then:
+curl -s localhost:9899/healthz
+curl -s -X POST localhost:9899/claim -H 'Content-Type: application/json' -d '{"email":"you@example.com"}'
+# mint a token and hit /verify — success page should say "unlocked"; logs show a single
+# PUT customers/<id> {tags:"verified-sample"} and NO discount/gift-card write.
 ```
 
+> `scripts/selftest.js` predates Option C and exercises the retired gift-card path —
+> treat the boot+curl above as the smoke test until selftest is updated.
+
 Store target is the LIVE prod store (legacy misnomer):
 `designer-laboratory-sandbox.myshopify.com`, Admin API `2024-10`.
 
 ---
 
-## 1. One-time: deploy the "DW Free Samples" Discount Function (REQUIRED — the safety layer)
+## 1. One-time: create the Regios rule (the reward + the roll guardrail) — do FIRST
 
-This is the roll-protection. The shared code you create in Step 2 must point at it,
-or the code offers no roll safety. Deploy it ONCE, interactively (Steve-only — Partner
-login):
+In the Shopify admin, open the **Regios** discount app and **clone the existing
+`trade`-memo rule** → new rule: *samples free for customers tagged `verified-sample`.*
 
-```sh
-cd ~/Projects/Designer-Wallcoverings/shopify/staged/free-samples-function
-shopify app config link     # DW Partner org + designer-laboratory-sandbox
-shopify app deploy          # uploads the function; NO discount created yet → zero customer impact
-```
+- ⚠️ **Confirm the entitlement scopes to the "Sample" VARIANT only** (the same scope the
+  trade-memo rule uses), so the `$80+` roll variant on the same product is never
+  discounted. This is the single money-risk to eyeball.
+- Cap: Regios likely won't hard-cap at exactly N. The chosen design is a **soft ~3**
+  (fine at `$4.25` × ~40 signups/day). A hard-N cap would require the retired Shopify
+  Function — not recommended.
 
-> If `shopify app config link` refuses over scope negotiation, confirm
-> `shopify.app.toml` `access_scopes` includes `write_discounts` before retrying.
-
-(Optional, informational: read the function id for reference —
-`query { shopifyFunctions(first:25){ nodes{ id title apiType } } }` → set
-`DISCOUNT_FUNCTION_ID`. The service does not use it on the wired path; the admin
-discount is what references the function.)
+No Shopify Function deploy, no Shopify Plus dependency, no coupon code.
 
 ---
 
-## 2. One-time: create the shared code discount in the Shopify admin
+## 2. Secrets (via the `secrets` skill — fewer scopes than the old path)
 
-Shopify admin → **Discounts** → create a **code** discount:
-- code: **`DWSAMPLES3`** (must match `RETAIL_SHARED_CODE` exactly),
-- discount type: the **"DW Free Samples"** app/function discount from Step 1,
-- usage limit: **one use per customer**.
+```sh
+DW_SIGNUP_VERIFY_SECRET=<openssl rand -hex 32>   # signs the stateless verify token
+WEBHOOK_URL_TOKEN=<openssl rand -hex 24>         # only if using the webhook entry (Step 5)
+SHOPIFY_FULFILLMENT_TOKEN=<a token with read_customers + write_customers + read/write webhooks>
+```
 
-The service emails this exact code to every new customer; it never mints codes.
+Option C needs **only** `read_customers` + `write_customers` (to add the tag) + webhook
+scopes. It **no longer needs** `write_gift_cards` / `write_discounts` /
+`DISCOUNT_FUNCTION_ID` / `RETAIL_SHARED_CODE` / `SAMPLES_COLLECTION_ID`.
+Confirm George `GEORGE_EXTERNAL_SEND_TOKEN` is set and `info@designerwallcoverings.com`
+is a verified send-as alias on the steve-office Gmail.
 
 ---
 
 ## 3. Set env (`.env` — gitignored, never commit/echo real tokens)
 
 ```sh
-SHOPIFY_FULFILLMENT_TOKEN=<value of SHOPIFY_DRAFT_TOKEN from ~/Projects/secrets-manager/.env, ends a43b>
-SHOPIFY_WEBHOOK_SECRET=<signing secret from the webhook registration in Step 5>
-RETAIL_SHARED_CODE=DWSAMPLES3            # MUST equal the admin code from Step 2 (else WARN + skip send)
-HOUSE_ACCOUNT_EMAIL=info@designerwallcoverings.com
-# DISCOUNT_FUNCTION_ID — informational only on the wired path; safe to leave blank.
+PUBLIC_URL=https://signup.designerwallcoverings.com   # so verify links + the webhook resolve
+VERIFIED_TAG=verified-sample                          # MUST equal the Regios rule's tag
+# FREE_SAMPLE_COUNT=3   VERIFY_TTL_HOURS=168  (defaults; override only if needed)
 ```
 
-The `…a43b` token carries `write_customers, read_customers, write_discounts,
-write_price_rules, write_gift_cards` (verified 2026-07-28 via
-`/admin/oauth/access_scopes.json`) — sufficient for tagging, metafields, and the
-email-lookup at approve.
-
 ---
 
 ## 4. Deploy the service (still DRY_RUN)
@@ -93,34 +91,34 @@ pm2 save
 curl -s http://127.0.0.1:9856/healthz # -> {"ok":true,...,"dry_run":true}
 ```
 
-Expose it at a public HTTPS URL (Kamatera vhost / Cloudflare) so Shopify can POST.
-Call that `$PUBLIC_URL`. `/healthz` returns 200 before any auth (fleet rule).
+Expose it at a public HTTPS URL (Kamatera vhost / Cloudflare) so Shopify can POST the
+webhook AND customers can click the verify link. `/healthz` returns 200 before any auth.
 
 ---
 
-## 5. Register the customers/create webhook (Admin API)
-
-DRY_RUN-gated in `lib/shopify.js`; do it explicitly at go-live.
+## 5. Pick the entry trigger (either / both)
 
+**Webhook (automatic on every signup):**
 ```sh
 SHOP=designer-laboratory-sandbox.myshopify.com
 TOKEN=<SHOPIFY_FULFILLMENT_TOKEN>
-PUBLIC_URL=https://signup.designerwallcoverings.com   # <-- your real URL
+PUBLIC_URL=https://signup.designerwallcoverings.com
+WEBHOOK_URL_TOKEN=<the token from Step 2>
 curl -s -X POST "https://$SHOP/admin/api/2024-10/webhooks.json" \
   -H "X-Shopify-Access-Token: $TOKEN" -H "Content-Type: application/json" \
-  -d "{\"webhook\":{\"topic\":\"customers/create\",\"address\":\"$PUBLIC_URL/webhooks/customers/create\",\"format\":\"json\"}}"
+  -d "{\"webhook\":{\"topic\":\"customers/create\",\"address\":\"$PUBLIC_URL/webhooks/customers/create/$WEBHOOK_URL_TOKEN\",\"format\":\"json\"}}"
 ```
+Record the returned webhook **id** (for rollback).
 
-Record the returned webhook **id** (rollback). Put its signing secret into
-`SHOPIFY_WEBHOOK_SECRET` (Step 3).
+**Claim form (pure opt-in, no webhook):** embed `/claim` (or POST to it) on the
+storefront. Good as a standalone landing even if you also run the webhook.
 
 ---
 
 ## 6. Flip DRY_RUN off
 
-Only after Steps 1–5 are green (in particular the function is deployed and the shared
-code exists and equals `RETAIL_SHARED_CODE` — the selftest / logs WARN loudly if the
-code is unset):
+Only after Steps 1–5 are green (in particular the Regios rule exists and is
+sample-variant-scoped):
 
 ```sh
 # set DRY_RUN=0 in .env or ecosystem.config.js env, then:
@@ -130,52 +128,47 @@ curl -s http://127.0.0.1:9856/healthz   # -> "dry_run":false
 
 ---
 
-## 7. Smoke-test LIVE (carefully)
-
-- Trigger a real signup with a throwaway email you control → confirm the email
-  arrives carrying **`DWSAMPLES3`** (NOT a `DWSAMP-…` unique code — the service does
-  not mint one), and confirm **no new discount record was created by the service**
-  in the admin. Then, signed in as that customer, add **4 sample swatches + 1 full
-  roll** and redeem the code → **first 3 samples free, 4th sample paid, roll
-  unaffected** (this is the live proof of the roll-safety guarantee).
-- `POST /trade/apply` a test application (email only, no `shopify_customer_id`, as the
-  public form sends) for an email that HAS a store account → open `/admin/trade` →
-  **Approve** → confirm the approve **resolved the customer by email**, the customer
-  now carries the `trade` tag + `custom.assigned_rep` metafield, and rep + applicant
-  emails went out. Then approve an application whose email has **no** store account →
-  confirm it **hard-fails** with `cannot_resolve_customer` and writes nothing.
+## 7. Smoke-test LIVE (the money check)
+
+- Hit `/claim` with a throwaway email you control (or trigger a real signup) → the
+  **"confirm your email"** letter arrives → click it → confirm the customer now carries
+  the **`verified-sample`** tag in the admin. Then, signed in as that customer, add a
+  **sample** (shows `$0`) **and a full roll** to the cart → **confirm the roll is still
+  full price.** If the roll is `$0`, the Regios scope is wrong (Step 1) — STOP.
+- `POST /trade/apply` a test application (email only) for an email that HAS a store
+  account → `/admin/trade` → **Approve** → confirm the `trade` tag + rep emails. Then
+  approve one whose email has **no** account → confirm it hard-fails and writes nothing.
 
 Watch `pm2 logs dw-signup-fulfillment` throughout.
 
 ---
 
-## ROLLBACK
+## ROLLBACK (instant, reversible)
 
 1. **Flip DRY_RUN back on** (stops all live writes immediately):
    ```sh
    # set DRY_RUN=1 in .env or ecosystem.config.js env
    pm2 restart dw-signup-fulfillment --update-env
-   curl -s http://127.0.0.1:9856/healthz   # -> "dry_run":true
    ```
-2. **Delete the webhook**:
+2. **Delete the webhook** (if registered):
    ```sh
    SHOP=designer-laboratory-sandbox.myshopify.com; TOKEN=<SHOPIFY_FULFILLMENT_TOKEN>
    curl -s "https://$SHOP/admin/api/2024-10/webhooks.json" -H "X-Shopify-Access-Token: $TOKEN"   # find the id
    curl -s -X DELETE "https://$SHOP/admin/api/2024-10/webhooks/<id>.json" -H "X-Shopify-Access-Token: $TOKEN"
    ```
-3. To stop the retail offer entirely: **deactivate the `DWSAMPLES3` discount** in the
-   admin (Discounts). Already-emailed codes are one-use-per-customer and stop working
-   the moment the discount is deactivated. (Optional) `pm2 stop dw-signup-fulfillment`.
+3. To stop the retail offer entirely: **disable the `verified-sample` Regios rule** in
+   the admin. Tagged customers immediately stop getting free samples. (Optional)
+   `pm2 stop dw-signup-fulfillment`.
 
 ---
 
-## Alternates (NOT wired — reference only)
-
-- `lib/giftcard.js` — stored-value gift card. Reachable via
-  `POST /admin/retail/issue?mode=giftcard`. **Not safe as default**: the balance is
-  spendable on any line item, including full rolls (bounded to `$12.75`).
-- `lib/giftcode-discount.js` — price-rule + Samples-collection-scoped 100%-off code.
-  Reachable via `POST /admin/retail/issue?mode=discount` (needs `SAMPLES_COLLECTION_ID`).
+## Retired alternates (NOT wired — reference only)
 
-The wired default is `lib/retail-code.js` (sample-locked shared function code).
-Switching away is a deliberate `server.js` change; do not do it without cause.
+Superseded by Option C on 2026-08-14. Still reachable via `POST /admin/retail/issue?mode=…`:
+- `lib/giftcard.js` — stored-value gift card (`mode=giftcard`). **Not safe as default**:
+  balance spendable on any line item, and it books `$12.75` liability per signup.
+- `lib/retail-code.js` — shared function-backed code (`mode=sharedcode`). Needs the
+  "DW Free Samples" Shopify Function (Plus + cross-app ownership issues) + an admin code.
+- `lib/giftcode-discount.js` — collection-scoped 100%-off code (`mode=discount`).
+  **Unsafe** — DW samples share a product with the roll variant, so collection scoping
+  can discount the roll (the exact reason Option C uses variant-scoped Regios instead).

← 75d27f5 Retail free-samples: pivot to verify→tag (Option C), drop gi  ·  back to Dw Signup Fulfillment  ·  selftest: cover Option C (verify letter, token security, tag f2bce23 →