← back to Wpb Sales Dashboard
README.md
86 lines
# WPB Sales & Products Dashboard
Tracks the **Wallpaper's Back design-marketplace selling operation** in one place:
```
Designs (inventory) -> Listings (design on a marketplace) -> Sales (orders)
```
- **Port:** 9812 · Basic auth `admin:DW2024!` (the 401 is the healthy gate) · `/healthz` is auth-exempt.
- **Zero dependencies** — plain Node HTTP.
- **Cost:** $0 (local) — reads pattern-vault data files + a local append-only sales ledger, no paid API.
## Data sources (read live per request)
| Data | Source |
|------|--------|
| Designs (pattern inventory) | `~/Projects/pattern-vault/data/catalog.json` |
| Spoonflower listings (LIVE, currently private) | `~/Projects/pattern-vault/data/spoonflower-uploads.json` |
| Etsy listings (PLANNED) | `~/Projects/pattern-vault/data/etsy-listings-plan.json` |
| Marketplace accounts | `config/marketplaces.json` (this repo) |
| Sales | `data/sales-ledger.jsonl` (this repo, append-only) |
## Sales ledger
Starts **empty on purpose** — everything is pre-launch / PRIVATE. Record the first
real order via the dashboard's "Record a sale" form, or POST directly:
```sh
curl -s -u admin:DW2024! -X POST http://127.0.0.1:9812/api/sales \
-H 'Content-Type: application/json' \
-d '{"marketplace":"spoonflower","title":"Fern Wink — Rainbows","units":1,"gross":8.50,"fee":2.10}'
```
A marketplace webhook can POST the same shape later — the manual form and the
webhook write identical rows. Fields: `marketplace` (key from marketplaces.json),
`title`/`design`, `units`, `gross`, `fee`, optional `net`/`date`/`orderId`.
## Inbound sales webhook `POST /webhook/sale`
A **secured, idempotent receiver** for automated sales. Token-gated (NOT basic-auth,
so external senders work) via the `WEBHOOK_TOKEN` in `.env` (gitignored). Send it in
the `X-Webhook-Token` header or `?token=` query param.
```sh
curl -s -X POST http://127.0.0.1:9812/webhook/sale \
-H "X-Webhook-Token: $WEBHOOK_TOKEN" -H 'Content-Type: application/json' \
-d '{"marketplace":"etsy","listing":"Greige Carouse No.57710","order_total":149,"commission":9.8,"receipt_id":"R-88231"}'
```
- **Field aliases** are normalized, so you can forward a source's native field names:
`gross|total|amount|order_total|price`, `fee|commission|marketplace_fee`,
`title|listing|product|pattern`, `orderId|order_id|receipt_id|transaction_id`, etc.
- **`marketplace` must be a known key** (`spoonflower|etsy|creativemarket|redbubble|patternbank|society6`) — a webhook can't invent a shop.
- **Idempotent:** a repeat with the same `marketplace` + `orderId` returns `{duplicate:true}` and is NOT double-counted (safe for webhook retries).
### Wiring the last hop (marketplace → this receiver)
**None of these POD marketplaces push a native sales webhook to third parties** —
so the last hop is one of:
1. **Zapier / Make** — trigger "new sale confirmation email in Gmail" (or the
platform's own trigger where one exists) → action "Webhook POST" to
`https://<host>/webhook/sale?token=…` mapping the email fields to the aliases above. *(Recommended — works for every platform via the order-confirmation email.)*
2. **Etsy poller** — Etsy has a real API but it's **poll-based** (pull `receipts`,
no push). A small cron that pulls new receipts for shop `SteveAbramsStudios` and
POSTs each here would make Etsy fully automatic — needs Etsy OAuth for that shop.
3. **Manual / CSV** — paste a sale via the dashboard form, or `curl` a row (above).
The receiver is real, secured infrastructure; the platforms simply give no
push option, so a forwarder (Zapier/email/poller) bridges the last hop.
## Run
```sh
node server.js # http://localhost:9812
```
## Standing-rule compliance
- **Sort + density slider** on the listings grid (persisted in `localStorage`).
- **Created date + time chip** on every listing card (admin surface).
- **Brand identity per shop** encoded in `config/marketplaces.json` — WPB everywhere
except Spoonflower + Patternbank (permanent `info@dw` exceptions).
Deploy is **Steve-gated** — local-only until then.