fix(scripts): payment-e2e walkthrough safety guards (DRY_RUN + prod confirm)

Three holes in the v1.0.9 W6 Day 27 walkthrough that an operator under
stress could fall into :

1. Typo'd STAGING_URL pointing at production. The script accepted any
   URL with no sanity check, so `STAGING_URL=https://veza.fr ...` would
   happily POST /orders and charge a real card on the first run.
   Fix: heuristic detection (URL doesn't contain "staging", "localhost"
   or "127.0.0.1" → treat as prod) refuses to run unless
   CONFIRM_PRODUCTION=1 is explicitly set.

2. No way to rehearse the flow without spending money. Added DRY_RUN=1
   that exits cleanly after step 2 (product listing) — exercises auth,
   API plumbing, and the staging product fixture without creating an
   order.

3. No final confirm before the actual charge. On a prod target, after
   the product is picked and before the POST /orders fires, the script
   now prints the {product_id, price, operator, endpoint} block and
   demands the operator type the literal word `CHARGE`. Any other
   answer aborts with exit code 2.

Together these turn "STAGING_URL typo = burnt 5 EUR" into "STAGING_URL
typo = exit code 3 with explanation". The wrapper docs in
docs/PAYMENT_E2E_LIVE_REPORT.md already mention card-charge risk in
prose; these guards enforce it at exec time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
senke 2026-04-30 22:27:14 +02:00
parent 6c644cff03
commit 05b1d81d30

View file

@ -42,6 +42,17 @@ OPERATOR_EMAIL=${OPERATOR_EMAIL:-?}
OPERATOR_PASSWORD=${OPERATOR_PASSWORD:-?} OPERATOR_PASSWORD=${OPERATOR_PASSWORD:-?}
ORDER_POLL_TIMEOUT=${ORDER_POLL_TIMEOUT:-300} ORDER_POLL_TIMEOUT=${ORDER_POLL_TIMEOUT:-300}
ORDER_POLL_INTERVAL=${ORDER_POLL_INTERVAL:-5} ORDER_POLL_INTERVAL=${ORDER_POLL_INTERVAL:-5}
# v1.0.10 polish safety guards:
# DRY_RUN=1 — skip the POST /orders + payment steps; rehearse
# the login + product-listing + license-poll path
# end-to-end on staging without spending a euro.
# CONFIRM_PRODUCTION=1 — required when STAGING_URL points at the live
# environment. Without it the script refuses to
# run, so a typo (`STAGING_URL=https://veza.fr`
# on a sandbox-targeted command) can't accidentally
# charge a real card.
DRY_RUN=${DRY_RUN:-0}
CONFIRM_PRODUCTION=${CONFIRM_PRODUCTION:-0}
SESSION_DATE="$(date +%Y%m%d-%H%M)" SESSION_DATE="$(date +%Y%m%d-%H%M)"
SESSION_LOG="${REPO_ROOT}/docs/PAYMENT_E2E_LIVE_REPORT.md.session-${SESSION_DATE}.log" SESSION_LOG="${REPO_ROOT}/docs/PAYMENT_E2E_LIVE_REPORT.md.session-${SESSION_DATE}.log"
@ -64,6 +75,43 @@ require jq
[ "$OPERATOR_EMAIL" = "?" ] && fail "OPERATOR_EMAIL env var required" 3 [ "$OPERATOR_EMAIL" = "?" ] && fail "OPERATOR_EMAIL env var required" 3
[ "$OPERATOR_PASSWORD" = "?" ] && fail "OPERATOR_PASSWORD env var required" 3 [ "$OPERATOR_PASSWORD" = "?" ] && fail "OPERATOR_PASSWORD env var required" 3
# Heuristic: any URL that doesn't include the substring "staging" is
# treated as production. Operators on a non-veza-domain (custom env)
# can still run the script; they just have to pass CONFIRM_PRODUCTION=1.
TARGET_LOOKS_LIKE_PROD=0
if [[ ! "$STAGING_URL" =~ staging ]] && [[ ! "$STAGING_URL" =~ localhost ]] && [[ ! "$STAGING_URL" =~ 127\.0\.0\.1 ]]; then
TARGET_LOOKS_LIKE_PROD=1
fi
if [ "$TARGET_LOOKS_LIKE_PROD" = "1" ] && [ "$CONFIRM_PRODUCTION" != "1" ]; then
cat >&2 <<EOF
================================================================
ABORTING — production target detected without explicit confirmation
================================================================
STAGING_URL=$STAGING_URL does not contain "staging", "localhost" or
"127.0.0.1", so this script will refuse to run by default to prevent
an accidental real-card charge.
If you genuinely want to run against production, re-invoke with:
CONFIRM_PRODUCTION=1 \\
STAGING_URL=$STAGING_URL \\
OPERATOR_EMAIL=$OPERATOR_EMAIL \\
OPERATOR_PASSWORD=... \\
bash scripts/payment-e2e-walkthrough.sh
Or set DRY_RUN=1 to rehearse the flow without making the actual charge.
================================================================
EOF
exit 3
fi
if [ "$DRY_RUN" = "1" ]; then
log "DRY_RUN=1 — order creation + payment + refund steps will be SKIPPED"
fi
# api wrapper that tee's request + response to the session log so the # api wrapper that tee's request + response to the session log so the
# operator can copy-paste the full trace into the report. # operator can copy-paste the full trace into the report.
api() { api() {
@ -134,8 +182,39 @@ log " ✓ price : $PRODUCT_PRICE"
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Step 3 : POST /orders. # Step 3 : POST /orders.
# -------------------------------------------------------------------- # --------------------------------------------------------------------
if [ "$DRY_RUN" = "1" ]; then
log ""
log "step 3 : POST /api/v1/marketplace/orders — SKIPPED (dry-run)"
log "================================================================"
log "DRY-RUN PASS : login + product list + license-mine endpoints reached"
log "Run without DRY_RUN to exercise the real charge + refund flow."
log "================================================================"
exit 0
fi
log "" log ""
log "step 3 : POST /api/v1/marketplace/orders" log "step 3 : POST /api/v1/marketplace/orders"
# v1.0.10 polish: confirm prompt before the actual charge so a typo'd
# product_id or wrong operator account can't quietly burn 5 EUR.
if [ "$TARGET_LOOKS_LIKE_PROD" = "1" ]; then
log ""
log "================================================================"
log "FINAL CONFIRMATION — about to charge a real card on production"
log "================================================================"
log " product_id : $PRODUCT_ID"
log " price : $PRODUCT_PRICE"
log " operator : $OPERATOR_EMAIL"
log " endpoint : ${STAGING_URL}/api/v1/marketplace/orders"
log ""
prompt "Type the literal word 'CHARGE' to proceed (anything else aborts) :"
read -r confirm_word
if [ "$confirm_word" != "CHARGE" ]; then
fail "operator did not confirm the charge ($confirm_word) — aborting" 2
fi
log " operator confirmed CHARGE — proceeding"
fi
order_body="{\"items\":[{\"product_id\":\"${PRODUCT_ID}\"}]}" order_body="{\"items\":[{\"product_id\":\"${PRODUCT_ID}\"}]}"
order_resp=$(api POST /api/v1/marketplace/orders "$order_body" 2>/dev/null) 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 // ""') ORDER_ID=$(echo "$order_resp" | jq -r '.data.order.id // .data.id // .id // ""')