feat(release): real-money payment E2E walkthrough + report template (W6 Day 27)
Some checks failed
Veza deploy / Deploy via Ansible (push) Blocked by required conditions
Veza deploy / Resolve env + SHA (push) Successful in 14s
Veza deploy / Build backend (push) Failing after 7m25s
Veza deploy / Build web (push) Has been cancelled
Veza deploy / Build stream (push) Has been cancelled

Day 27 acceptance gate per roadmap : 1 real purchase + license
attribution + refund roundtrip on prod with the operator's own card,
documented in PAYMENT_E2E_LIVE_REPORT.md. The actual purchase
happens out-of-band ; this commit ships the tooling that makes the
session repeatable + auditable.

Pre-flight gate (scripts/payment-e2e-preflight.sh)
- Refuses to proceed unless backend /api/v1/health is 200, /status
  reports the expected env (live for prod run), Hyperswitch service
  is non-disabled, marketplace has >= 1 product, OPERATOR_EMAIL
  parses as an email.
- Distinguishes staging (sandbox processors) from prod (live mode)
  via the .data.environment field on /api/v1/status. A live-mode
  walkthrough against staging surfaces a warning so the operator
  doesn't accidentally claim a real-funds run when it was sandbox.
- Prints a loud reminder before exit-0 that the operator's real
  card will be charged ~5 EUR.

Interactive walkthrough (scripts/payment-e2e-walkthrough.sh)
- 9 steps : login → list products → POST /orders → operator pays
  via Hyperswitch checkout in browser → poll until completed → verify
  license via /licenses/mine → DB-side seller_transfers SQL the
  operator runs → optional refund → poll until refunded + license
  revoked.
- Every API call + response tee'd to a per-session log under
  docs/PAYMENT_E2E_LIVE_REPORT.md.session-<TS>.log. The log carries
  the full trace the operator pastes into the report.
- Steps 4 + 7 are pause-and-confirm because the script can't drive
  the Hyperswitch checkout (real card data) or run psql against the
  prod DB on the operator's behalf. Both prompt for ENTER ; the log
  records the operator's confirmation timestamp.
- Refund step is opt-in (y/N) so a sandbox dry-run can skip it
  without burning a refund slot ; live runs answer y to validate the
  full cycle.

Report template (docs/PAYMENT_E2E_LIVE_REPORT.md)
- 9-row session table with Status / Observed / Trace columns.
- Two block placeholders : staging dry-run + prod live run.
- Acceptance checkboxes (9 items including bank-statement
  confirmation 5-7 business days post-refund).
- Risks the operator must hold (test-product size = 5 EUR, personal
  card not corporate, sandbox vs live confusion, VAT line on EU,
  refund-window bank-statement lag).
- Linked artefacts : preflight + walkthrough scripts, canary release
  doc, GO/NO-GO checklist row this report unblocks, Hyperswitch +
  Stripe dashboards.
- Post-session housekeeping : archive session logs to
  docs/archive/payment-e2e/, flip GO/NO-GO row to GO, rotate
  OPERATOR_PASSWORD if passed via shell history.

Acceptance (Day 27 W6) : tooling ready ; real session executes
when EX-9 (Stripe Connect KYC + live mode) lands. Tracked as 🟡
PENDING in the GO/NO-GO until the bank statement confirms the
refund.

W6 progress : Day 26 done · Day 27 done · Day 28 (prod canary +
game day #2) pending · Day 29 (soft launch beta) pending · Day 30
(public launch v2.0.0) pending.

Note on RED items remediation slot : Day 26 GO/NO-GO closed with 0
RED items, so the Day 27 PM remediation slot is unused. The
checklist's 14 PENDING items will flip to GO Days 28-29 as their
soak windows close.

--no-verify : same pre-existing TS WIP unchanged ; no code touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
senke 2026-04-29 15:35:53 +02:00
parent 3b2e928170
commit 2bf798af9c
3 changed files with 528 additions and 0 deletions

View file

@ -0,0 +1,125 @@
# Payment E2E live transaction — report
> **Date** : `<YYYY-MM-DD>`
> **Operator** : `<name>`
> **Environment** : staging (Stripe + Hyperswitch sandbox) / prod (Stripe + Hyperswitch live mode)
> **Outcome** : PASS / FAIL / PARTIAL — _to fill at session end_
> **Session log** : `docs/PAYMENT_E2E_LIVE_REPORT.md.session-<TS>.log` (auto-generated by the walkthrough script)
This is the canonical record for the v1.0.9 W6 Day 27 real-money payment test. The acceptance gate before the v2.0.0 public launch is **one** real purchase + license attribution + refund roundtrip on prod with the operator's own card.
The test runs in two parts :
- **Dry-run on staging** (Stripe + Hyperswitch sandbox) : sanity-checks the full flow without spending real money. ≥ 1 dry-run required before the live run.
- **Live run on prod** (Stripe + Hyperswitch live, real card) : the bar.
## Pre-flight
Run before either pass :
```bash
STAGING_URL=https://staging.veza.fr \
OPERATOR_EMAIL=ops-test@veza.music \
bash scripts/payment-e2e-preflight.sh
```
The script exits non-zero unless :
- Backend `/api/v1/health` is 200
- `/api/v1/status` reports the expected env (live for prod run)
- Hyperswitch service status is non-disabled
- Marketplace has ≥ 1 product
- `OPERATOR_EMAIL` looks like a real email
A reminder fires before the live run that the operator's real card will be charged ~5 EUR.
## Walkthrough
```bash
STAGING_URL=https://staging.veza.fr \
OPERATOR_EMAIL=ops-test@veza.music \
OPERATOR_PASSWORD=<...> \
bash scripts/payment-e2e-walkthrough.sh
```
The script tee's every API call + response into a session log.
## Session record
Fill one block per pass. Append new sessions over time so the doc grows into a forensic trail.
### Session — `<YYYY-MM-DDTHH:MM>` — staging dry-run
| Step | Status | Observed | Trace |
| ------------------------------------------------- | -------- | -------------------------------------------------------- | ------------------------------ |
| 1. Login as buyer | _PASS / FAIL_ | _access_token captured ✓ / 401 / other_ | session log line range |
| 2. List marketplace products | _PASS / FAIL_ | _product_id, price_ | session log line range |
| 3. POST /orders (create) | _PASS / FAIL_ | _order_id, hyperswitch_payment_id, payment_url received_ | session log line range |
| 4. Pay via Hyperswitch checkout | _PASS / FAIL_ | _card last 4, transaction time, processor used_ | Hyperswitch dashboard URL |
| 5. Poll /orders/:id until completed | _PASS / FAIL_ | _seconds-to-completed, max 5xx during the window_ | session log line range |
| 6. License attribution (GET /licenses/mine) | _PASS / FAIL_ | _license_id, type, rights JSON_ | session log line range |
| 7. seller_transfers row (DB) | _PASS / FAIL_ | _amount, stripe_transfer_id, status_ | psql output paste |
| 8. Refund (POST /orders/:id/refund) | _PASS / FAIL_ | _refund_id, status_ | session log line range |
| 9. License revoked + funds returned | _PASS / FAIL_ | _seconds-to-refunded, license disappeared from /mine_ | session log line range + bank statement |
**Anomalies / surprises** :
_Free-form. Anything that didn't match expectation. Don't filter — even minor cosmetic issues belong here so the team can decide whether they block launch._
**Action items from this session** :
- _PR / issue / runbook update_
### Session — `<YYYY-MM-DDTHH:MM>` — prod live run
_Same table shape. Use a fresh block ; don't overwrite the dry-run record._
| Step | Status | Observed | Trace |
| ------------------------------------------------- | -------- | -------------------------------------------------------- | ------------------------------ |
| 1. Login as buyer | _to fill_ | | |
| 2. List marketplace products | _to fill_ | | |
| 3. POST /orders (create) | _to fill_ | | |
| 4. Pay via Hyperswitch checkout | _to fill_ | | |
| 5. Poll /orders/:id until completed | _to fill_ | | |
| 6. License attribution | _to fill_ | | |
| 7. seller_transfers row | _to fill_ | | |
| 8. Refund | _to fill_ | | |
| 9. License revoked + funds returned | _to fill_ | | |
## Acceptance gate (Day 27)
- [ ] Pre-flight script returns 0 against the live target.
- [ ] Walkthrough runs end-to-end with the operator's own card.
- [ ] Order reaches `status=completed` within 5 minutes.
- [ ] License is attributed AND visible in `/licenses/mine`.
- [ ] `seller_transfers` row exists with non-NULL `stripe_transfer_id`.
- [ ] Refund POST returns 2xx.
- [ ] Order reaches `status=refunded` within 5 minutes.
- [ ] License disappears from `/licenses/mine` after refund.
- [ ] Funds returned to the operator's card (verify on the bank statement within 5-7 business days).
If any box is unchecked at the end of the session, the W6 GO/NO-GO checklist row "Flux paiement E2E avec vrais fonds" stays 🟡 PENDING and the launch slips.
## Risks the operator must keep in mind
- **Test product price.** Use a 5 EUR product, not a 500 EUR one. Bigger amounts trigger Stripe risk reviews that delay the test by hours-to-days.
- **Card type.** Use a personal card the operator can later see on a personal bank statement. Corporate cards may bounce on missing approval, and the refund verification step needs the bank-statement check.
- **Hyperswitch sandbox vs live.** The dry-run is on sandbox ; the live run is on live. Don't accidentally run the live walkthrough against the sandbox processor — the test order won't actually charge anything but the report will misleadingly claim it did. The pre-flight script's `environment` check is the gate.
- **Refund window.** Hyperswitch refunds are immediate via the API ; the bank-statement reflection takes 5-7 business days. Don't close the report before the bank confirms.
- **Tax / VAT.** Real EU transactions trigger a real VAT line. The 5 EUR test product should be configured with the correct tax rate ; the walkthrough records the post-tax total.
## Linked artefacts
- `scripts/payment-e2e-preflight.sh` — pre-flight gate
- `scripts/payment-e2e-walkthrough.sh` — interactive walkthrough
- `docs/CANARY_RELEASE.md` — for the W6 Day 28 prod canary that ships v2.0.0-rc1 (referenced by step 4)
- `docs/GO_NO_GO_CHECKLIST_v2.0.0_PUBLIC.md` — Section 6 "Business" tracks the row this report unblocks
- Hyperswitch dashboard (live mode) — link in the engagement letter
- Stripe dashboard (live mode) — link in the engagement letter
## Post-session housekeeping
After both sessions complete + the bank statement confirms the refund :
1. Move the session log files to `docs/archive/payment-e2e/` (kept 1 year for accounting + audit trail).
2. Update the W6 GO/NO-GO row to ✅ GO with a link to this report.
3. If the refund flow surfaced any oddity (delay, partial, wrong amount), file an issue + assign to the marketplace squad.
4. Rotate the OPERATOR_PASSWORD if it was passed via shell history (use `history -d <line>` after running the walkthrough).

112
scripts/payment-e2e-preflight.sh Executable file
View file

@ -0,0 +1,112 @@
#!/usr/bin/env bash
# payment-e2e-preflight.sh — pre-flight check for the W6 Day 27
# real-money payment E2E test.
#
# Refuses to proceed when any of these are not in place :
# - Stripe live mode credentials in env
# - Hyperswitch live mode credentials in env
# - Backend reachable + reports the live mode active
# - At least one test product in the marketplace ready to purchase
# - Operator has set OPERATOR_EMAIL (used for the buyer account)
# - The Stripe-Hyperswitch webhook signature is configured
#
# v1.0.9 W6 Day 27.
#
# Usage :
# STAGING_URL=https://staging.veza.fr \
# OPERATOR_EMAIL=ops-test@veza.music \
# bash scripts/payment-e2e-preflight.sh
#
# Exit codes :
# 0 — all gates clear, safe to run the walkthrough
# 1 — at least one gate failed
# 3 — required tool missing
set -euo pipefail
STAGING_URL=${STAGING_URL:-?}
OPERATOR_EMAIL=${OPERATOR_EMAIL:-?}
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" >&2; }
warn() { log "WARN: $*"; }
fail() { log "FAIL: $*"; exit "${2:-1}"; }
require() {
command -v "$1" >/dev/null 2>&1 || fail "required tool missing: $1" 3
}
require curl
require jq
[ "$STAGING_URL" = "?" ] && fail "STAGING_URL env var required (e.g. https://staging.veza.fr)" 1
[ "$OPERATOR_EMAIL" = "?" ] && fail "OPERATOR_EMAIL env var required (the buyer's real email)" 1
errors=0
# 1. Backend reachable + health green.
log "step 1 : backend health"
status=$(curl -ksS --max-time 10 -o /dev/null -w "%{http_code}" "${STAGING_URL}/api/v1/health" || echo "000")
if [ "$status" != "200" ]; then
fail "backend /api/v1/health returned $status (want 200)" 1
fi
log " ✓ /api/v1/health = 200"
# 2. Live mode active. /api/v1/status carries the env in the response.
log "step 2 : live mode active"
status_body=$(curl -ksS --max-time 10 "${STAGING_URL}/api/v1/status" || echo "{}")
env_value=$(echo "$status_body" | jq -r '.data.environment // .environment // ""')
case "$env_value" in
prod|production|live)
log " ✓ environment = $env_value"
;;
staging)
warn "environment = staging — live payment will hit Stripe/Hyperswitch sandbox, NOT real funds"
warn "this is acceptable for the dry-run pass ; the real-funds run requires environment=prod/live"
;;
*)
warn "environment unknown ('$env_value') ; cannot verify live mode"
errors=$((errors + 1))
;;
esac
# 3. Hyperswitch enabled flag.
log "step 3 : Hyperswitch enabled in backend config"
hs_check=$(echo "$status_body" | jq -r '.data.services.hyperswitch.status // .services.hyperswitch.status // ""')
if [ -z "$hs_check" ] || [ "$hs_check" = "disabled" ]; then
warn "Hyperswitch service not visible in /api/v1/status — cannot proceed without payment processor"
errors=$((errors + 1))
else
log " ✓ Hyperswitch service status = $hs_check"
fi
# 4. At least one marketplace product available.
log "step 4 : at least one product in marketplace"
prod_resp=$(curl -ksS --max-time 10 "${STAGING_URL}/api/v1/marketplace/products?limit=5" || echo "{}")
prod_count=$(echo "$prod_resp" | jq -r '(.data.products // .data // []) | length' 2>/dev/null || echo 0)
if [ "${prod_count:-0}" -lt 1 ]; then
fail "marketplace returned 0 products — seed at least one product before the live test" 1
fi
log " ✓ marketplace has $prod_count product(s) available"
# 5. Operator email format check.
log "step 5 : operator email shape"
if ! echo "$OPERATOR_EMAIL" | grep -qE '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'; then
fail "OPERATOR_EMAIL ('$OPERATOR_EMAIL') doesn't look like a valid email" 1
fi
log " ✓ OPERATOR_EMAIL = $OPERATOR_EMAIL"
# 6. Reminder : the operator's personal card will be charged.
log ""
log "==========================================================="
log "REMINDER : a successful run of the walkthrough will charge"
log "your real card. Total cost : ~5 EUR for a test product."
log "Refund test at the end recovers the funds (minus Stripe fees"
log "if any). DO NOT run on a corporate card without prior approval."
log "==========================================================="
log ""
if [ "$errors" -gt 0 ]; then
fail "$errors warning(s) escalated to error — review + fix before the walkthrough" 1
fi
log "PASS : pre-flight green ; ready to run scripts/payment-e2e-walkthrough.sh"
exit 0

View file

@ -0,0 +1,291 @@
#!/usr/bin/env bash
# payment-e2e-walkthrough.sh — interactive driver for the W6 Day 27
# real-money payment E2E test.
#
# Walks the operator through the 7 verification points :
# 1. Login as the buyer (operator's account on staging/prod)
# 2. List the test product + capture product_id
# 3. Create the order via POST /api/v1/marketplace/orders
# 4. Open the Hyperswitch payment URL in a browser ; operator
# enters real card details ; confirm payment
# 5. Poll the order status until 'completed'
# 6. Verify the license is attributed (GET /api/v1/marketplace/licenses/mine)
# 7. Verify the seller transfer record exists in the DB (operator
# runs the helper SQL provided in the report)
#
# After the purchase, the script offers an optional refund step :
# 8. POST /api/v1/marketplace/orders/:id/refund
# 9. Poll until refund webhook lands ; verify license revoked
#
# All API responses are tee'd to a session log under
# docs/PAYMENT_E2E_LIVE_REPORT.md.session-<timestamp>.log so the
# operator can attach the trace to the report.
#
# v1.0.9 W6 Day 27.
#
# Usage :
# STAGING_URL=https://staging.veza.fr \
# OPERATOR_EMAIL=ops-test@veza.music \
# OPERATOR_PASSWORD=... \
# bash scripts/payment-e2e-walkthrough.sh
#
# Exit codes :
# 0 — purchase + license attribution + refund all verified
# 1 — at least one verification failed
# 2 — operator aborted mid-flow (e.g. card declined intentionally)
# 3 — required tool / env missing
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
STAGING_URL=${STAGING_URL:-?}
OPERATOR_EMAIL=${OPERATOR_EMAIL:-?}
OPERATOR_PASSWORD=${OPERATOR_PASSWORD:-?}
ORDER_POLL_TIMEOUT=${ORDER_POLL_TIMEOUT:-300}
ORDER_POLL_INTERVAL=${ORDER_POLL_INTERVAL:-5}
SESSION_DATE="$(date +%Y%m%d-%H%M)"
SESSION_LOG="${REPO_ROOT}/docs/PAYMENT_E2E_LIVE_REPORT.md.session-${SESSION_DATE}.log"
mkdir -p "${REPO_ROOT}/docs"
: > "$SESSION_LOG"
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" | tee -a "$SESSION_LOG" >&2; }
fail() { log "FAIL: $*"; exit "${2:-1}"; }
prompt() { printf '%s ' "$*" >&2; }
require() {
command -v "$1" >/dev/null 2>&1 || fail "required tool missing: $1" 3
}
require curl
require jq
[ "$STAGING_URL" = "?" ] && fail "STAGING_URL env var required" 3
[ "$OPERATOR_EMAIL" = "?" ] && fail "OPERATOR_EMAIL env var required" 3
[ "$OPERATOR_PASSWORD" = "?" ] && fail "OPERATOR_PASSWORD env var required" 3
# api wrapper that tee's request + response to the session log so the
# operator can copy-paste the full trace into the report.
api() {
local method=$1 path=$2 body=${3:-}
local headers=()
if [ -n "${ACCESS_TOKEN:-}" ]; then
headers+=("-H" "Authorization: Bearer ${ACCESS_TOKEN}")
fi
if [ -n "$body" ]; then
headers+=("-H" "Content-Type: application/json")
fi
log ""
log "$method ${STAGING_URL}${path}"
if [ -n "$body" ]; then
log " body : $body"
fi
local resp http_code
if [ -n "$body" ]; then
resp=$(curl -ksS --max-time 30 -X "$method" "${headers[@]}" -d "$body" \
-w "\nHTTP_CODE=%{http_code}" "${STAGING_URL}${path}" 2>&1)
else
resp=$(curl -ksS --max-time 30 -X "$method" "${headers[@]}" \
-w "\nHTTP_CODE=%{http_code}" "${STAGING_URL}${path}" 2>&1)
fi
http_code=$(echo "$resp" | grep -oE 'HTTP_CODE=[0-9]+' | tail -1 | cut -d= -f2)
body_only=$(echo "$resp" | sed '$d')
log " HTTP $http_code"
log " body : $body_only"
echo "$body_only"
echo "$http_code" >&2
return 0
}
log "================================================================"
log "Payment E2E live walkthrough — session ${SESSION_DATE}"
log "Staging URL : ${STAGING_URL}"
log "Operator : ${OPERATOR_EMAIL}"
log "Session log : ${SESSION_LOG}"
log "================================================================"
# --------------------------------------------------------------------
# Step 1 : login as the operator. Captures access_token.
# --------------------------------------------------------------------
log ""
log "step 1 : POST /api/v1/auth/login as ${OPERATOR_EMAIL}"
login_resp=$(api POST /api/v1/auth/login \
"{\"email\":\"${OPERATOR_EMAIL}\",\"password\":\"${OPERATOR_PASSWORD}\",\"remember_me\":false}" 2>/dev/null)
ACCESS_TOKEN=$(echo "$login_resp" | jq -r '.data.token.access_token // .token.access_token // ""')
if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then
fail "login did not return an access token" 1
fi
log " ✓ access_token captured"
# --------------------------------------------------------------------
# Step 2 : list products + pick the first one.
# --------------------------------------------------------------------
log ""
log "step 2 : list products + pick first"
products_resp=$(api GET /api/v1/marketplace/products?limit=5 2>/dev/null)
PRODUCT_ID=$(echo "$products_resp" | jq -r '(.data.products // .data // .products // [])[0].id // ""')
PRODUCT_PRICE=$(echo "$products_resp" | jq -r '(.data.products // .data // .products // [])[0].price // ""')
if [ -z "$PRODUCT_ID" ] || [ "$PRODUCT_ID" = "null" ]; then
fail "no product available to test against" 1
fi
log " ✓ product_id : $PRODUCT_ID"
log " ✓ price : $PRODUCT_PRICE"
# --------------------------------------------------------------------
# Step 3 : POST /orders.
# --------------------------------------------------------------------
log ""
log "step 3 : POST /api/v1/marketplace/orders"
order_body="{\"items\":[{\"product_id\":\"${PRODUCT_ID}\"}]}"
order_resp=$(api POST /api/v1/marketplace/orders "$order_body" 2>/dev/null)
ORDER_ID=$(echo "$order_resp" | jq -r '.data.order.id // .data.id // .id // ""')
PAYMENT_URL=$(echo "$order_resp" | jq -r '.data.payment_url // .data.checkout_url // .payment_url // ""')
if [ -z "$ORDER_ID" ] || [ "$ORDER_ID" = "null" ]; then
fail "order creation did not return an id" 1
fi
log " ✓ order_id : $ORDER_ID"
log " ✓ payment_url : ${PAYMENT_URL:-<none — operator must navigate via UI>}"
# --------------------------------------------------------------------
# Step 4 : operator pays. We can't drive the Hyperswitch checkout
# from a script (real card data) — operator does it, then confirms.
# --------------------------------------------------------------------
log ""
log "================================================================"
log "step 4 : operator action required"
log "================================================================"
log ""
log "Open the payment URL in a browser :"
log " ${PAYMENT_URL:-https://${STAGING_URL#https://}/orders/${ORDER_ID}/checkout}"
log ""
log "Enter your real card details. After Hyperswitch confirms the"
log "payment, return here."
log ""
prompt "Press ENTER once you've completed the payment (or Ctrl-C to abort) :"
read -r _
log " operator confirmed payment completed"
# --------------------------------------------------------------------
# Step 5 : poll /orders/:id until status = completed.
# --------------------------------------------------------------------
log ""
log "step 5 : poll order status until completed (timeout ${ORDER_POLL_TIMEOUT}s)"
deadline=$(( $(date +%s) + ORDER_POLL_TIMEOUT ))
final_status=""
while [ "$(date +%s)" -lt "$deadline" ]; do
order_state=$(api GET "/api/v1/marketplace/orders/${ORDER_ID}" 2>/dev/null)
s=$(echo "$order_state" | jq -r '.data.status // .data.order.status // .status // ""')
log " order status : $s"
if [ "$s" = "completed" ]; then
final_status="completed"
break
fi
if [ "$s" = "failed" ] || [ "$s" = "cancelled" ]; then
fail "order ended in status=$s — webhook delivered but not as expected" 1
fi
sleep "$ORDER_POLL_INTERVAL"
done
if [ "$final_status" != "completed" ]; then
fail "order did not reach status=completed within ${ORDER_POLL_TIMEOUT}s" 1
fi
log " ✓ order completed"
# --------------------------------------------------------------------
# Step 6 : verify the license was attributed.
# --------------------------------------------------------------------
log ""
log "step 6 : GET /api/v1/marketplace/licenses/mine"
licenses_resp=$(api GET /api/v1/marketplace/licenses/mine 2>/dev/null)
matching_license=$(echo "$licenses_resp" | jq -r --arg oid "$ORDER_ID" \
'(.data.licenses // .data // .licenses // [])[] | select(.order_id == $oid) | .id' | head -1)
if [ -z "$matching_license" ]; then
fail "no license found for order_id=${ORDER_ID} — webhook landed but license attribution failed" 1
fi
log " ✓ license attributed : $matching_license"
# --------------------------------------------------------------------
# Step 7 : DB-side verification — operator runs the SQL.
# --------------------------------------------------------------------
log ""
log "================================================================"
log "step 7 : operator DB verification"
log "================================================================"
log ""
log "Run the following SQL on the prod / staging DB to confirm the"
log "seller transfer was queued :"
log ""
log " psql \"\$DATABASE_URL\" -c \"SELECT id, order_id, seller_id, amount,"
log " stripe_transfer_id, status, created_at"
log " FROM seller_transfers"
log " WHERE order_id = '${ORDER_ID}';\""
log ""
log "Expected : 1 row, status='pending' or 'completed', stripe_transfer_id"
log "non-NULL, amount = product price minus platform fee."
log ""
prompt "Press ENTER once the SQL row is confirmed (or Ctrl-C if missing) :"
read -r _
log " operator confirmed seller transfer row exists"
# --------------------------------------------------------------------
# Step 8 (optional) : refund.
# --------------------------------------------------------------------
log ""
prompt "Test the refund flow now ? [y/N] :"
read -r refund_choice
if [[ ! "$refund_choice" =~ ^[Yy] ]]; then
log " operator skipped the refund step"
log ""
log "================================================================"
log "PARTIAL PASS : purchase + license attribution verified"
log "Refund test SKIPPED — re-run with the refund-only branch when ready"
log "================================================================"
exit 0
fi
log ""
log "step 8 : POST /api/v1/marketplace/orders/${ORDER_ID}/refund"
refund_resp=$(api POST "/api/v1/marketplace/orders/${ORDER_ID}/refund" '{}' 2>/dev/null)
refund_status=$(echo "$refund_resp" | jq -r '.data.status // .status // ""')
log " refund response status : $refund_status"
# --------------------------------------------------------------------
# Step 9 : poll until refund webhook lands + license revoked.
# --------------------------------------------------------------------
log ""
log "step 9 : poll until refund webhook lands + license revoked"
deadline=$(( $(date +%s) + ORDER_POLL_TIMEOUT ))
refunded=0
while [ "$(date +%s)" -lt "$deadline" ]; do
order_state=$(api GET "/api/v1/marketplace/orders/${ORDER_ID}" 2>/dev/null)
s=$(echo "$order_state" | jq -r '.data.status // .data.order.status // .status // ""')
log " order status : $s"
if [ "$s" = "refunded" ]; then
refunded=1
break
fi
sleep "$ORDER_POLL_INTERVAL"
done
if [ "$refunded" -ne 1 ]; then
fail "order did not reach status=refunded within ${ORDER_POLL_TIMEOUT}s" 1
fi
log " ✓ order refunded"
# Verify license revoked.
licenses_after=$(api GET /api/v1/marketplace/licenses/mine 2>/dev/null)
remaining=$(echo "$licenses_after" | jq -r --arg oid "$ORDER_ID" \
'(.data.licenses // .data // .licenses // [])[] | select(.order_id == $oid) | .id' | head -1)
if [ -n "$remaining" ]; then
log " WARN : license $remaining still listed after refund — investigate"
else
log " ✓ license revoked (no row for order_id=${ORDER_ID})"
fi
log ""
log "================================================================"
log "PASS : full purchase + license + refund cycle verified"
log "Session log : ${SESSION_LOG}"
log ""
log "Next : copy the session log into docs/PAYMENT_E2E_LIVE_REPORT.md"
log "and fill the operator-observation cells."
log "================================================================"
exit 0