← back to Dw Signup Fulfillment
DEPLOY.md
188 lines
# 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-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; **hard-fails** if no such
> customer exists), tag `trade`, assign the DW House Account, set `custom.assigned_rep`,
> email rep + applicant. (Unchanged.)
---
## 0. Pre-flight (still DRY_RUN)
> ⛔ **NEVER run `npm test` / `npm run selftest` against the PROD checkout.**
> `scripts/selftest.js` snapshots `data/trade-applications.jsonl`, writes its own test rows
> into the REAL file during the run, then restores the start snapshot on exit — on both the
> success and error paths, and it is NOT gated by `DRY_RUN`. Anything that arrives in that
> ~480 ms window (a submitted trade application, an approve/reject decision) is destroyed
> silently. Reproduced 2026-09-10 under TK-11285. Run the suite on a non-prod checkout, or
> against a throwaway data dir. Post-deploy verification on prod is `/healthz` + `pm2 logs`,
> never the test suite.
```sh
cd ~/Projects/dw-signup-fulfillment
npm install
# 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.
```
> **Readiness gate:** `node scripts/golive-preflight.js` — read-only, sends nothing, and
> verifies every dependency (George Basic-auth via an authenticated health GET, send-token,
> verify secret, Shopify token, PUBLIC_URL, DRY_RUN state, service health) as PASS/WARN/FAIL,
> writing `data/latest.json` for the fleet-health rollup. Run it on Kamatera before flipping
> `DRY_RUN=0`; require all hard checks ✅ (only the manual `regios_rule` may stay WARN).
> The full gated go-live sequence lives in `pending-approval/option-c-golive-runbook-20260814.md`.
Store target is the LIVE prod store (legacy misnomer):
`designer-laboratory-sandbox.myshopify.com`, Admin API `2024-10`.
---
## 1. One-time: create the Regios rule (the reward + the roll guardrail) — do FIRST
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`.*
- ⚠️ **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.
No Shopify Function deploy, no Shopify Plus dependency, no coupon code.
---
## 2. Secrets (via the `secrets` skill — fewer scopes than the old path)
```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>
```
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
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)
```
---
## 4. Deploy the service (still DRY_RUN)
```sh
pm2 start ecosystem.config.js # DRY_RUN=1 by env in ecosystem
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 the
webhook AND customers can click the verify link. `/healthz` returns 200 before any auth.
---
## 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
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/$WEBHOOK_URL_TOKEN\",\"format\":\"json\"}}"
```
Record the returned webhook **id** (for rollback).
**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 Regios rule exists and is
sample-variant-scoped):
```sh
# set DRY_RUN=0 in .env or ecosystem.config.js env, then:
pm2 restart dw-signup-fulfillment --update-env
curl -s http://127.0.0.1:9856/healthz # -> "dry_run":false
```
---
## 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 (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
```
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: **disable the `verified-sample` Regios rule** in
the admin. Tagged customers immediately stop getting free samples. (Optional)
`pm2 stop dw-signup-fulfillment`.
---
## Retired alternates (NOT wired — reference only)
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).