Replaced mollie
All checks were successful
Deploy / build-deploy (push) Successful in 3m47s

This commit is contained in:
Jorijn van der Graaf 2026-08-20 20:15:47 +02:00
commit df91762271
29 changed files with 3079 additions and 838 deletions

57
tools/dev-credit.sh Executable file
View file

@ -0,0 +1,57 @@
#!/bin/sh
# Pretend the bank reported an incoming transfer, so a dev order can be paid
# without a bank.
#
# The transfer rail settles an order when a credit quoting its reference shows
# up in the credits file. In production that file is filled by
# `catcrafts-server --pull-credits` reading the real account; here it is filled
# by hand. Same file, same format, same matching code — which is the point:
# this exercises the real settlement path rather than a test double.
#
# tools/dev-credit.sh <orders-file> <reference> <amount-in-cents> [method]
#
# Example, paying order CC-2B6457 the €570.43 it is waiting for:
# tools/dev-credit.sh /tmp/dev/orders.jsonl CC-2B6457 57043
#
# The reference is matched forgivingly (case, spacing and punctuation are
# ignored, and the RF… form works too), so it is worth deliberately mangling it
# to watch that hold:
# tools/dev-credit.sh /tmp/dev/orders.jsonl "betaling cc 2b6457 bedankt" 57043
set -eu
if [ "$#" -lt 3 ]; then
echo "usage: $0 <orders-file> <reference> <amount-in-cents> [method]" >&2
echo " method defaults to sepa; try 'ideal' or 'card' to see the ledger's" >&2
echo " via column change, which is what decides if an order is safe to ship." >&2
exit 2
fi
ORDERS="$1"
REFERENCE="$2"
CENTS="$3"
METHOD="${4:-sepa}"
CREDITS="$ORDERS.transfer-credits.jsonl"
case "$CENTS" in
''|*[!0-9]*) echo "$0: amount must be whole cents, got '$CENTS'" >&2; exit 2 ;;
esac
# A unique id per line, because the rail deduplicates on it: appending the same
# id twice is deliberately a no-op, which is what stops a repeated pull from
# double-crediting an order. Using a counter keeps each hand-made credit
# distinct without needing a clock.
n=1
if [ -f "$CREDITS" ]; then
n=$(( $(wc -l < "$CREDITS") + 1 ))
fi
# Escape the two characters that would break the line-per-record format. The
# reference is free text on purpose so it can be mangled realistically.
escaped=$(printf '%s' "$REFERENCE" | sed 's/\\/\\\\/g; s/"/\\"/g')
printf '{"id":"dev-%s","reference":"%s","amount_minor":%s,"method":"%s"}\n' \
"$n" "$escaped" "$CENTS" "$METHOD" >> "$CREDITS"
echo "credited $CENTS cents quoting '$REFERENCE' (method $METHOD)"
echo "wrote $CREDITS"
echo "the reconciler sweeps about once a minute; watch the dev log for 'paid'"

View file

@ -133,30 +133,41 @@ cat > "$WORK/Caddyfile" <<EOF
EOF
# Rail selection for dev:
# * a repo-root .env (gitignored, never committed) is sourced if present —
# put MOLLIE_API_KEY=test_… there to point dev at Mollie's real test mode;
# * DEV_RAIL=fake|mollie overrides the automatic choice;
# * default with no key is the fake rail: full order lifecycle, no network.
# "Pay" an order with: touch $WORK/orders.jsonl.fake-paid
# * a repo-root .env (gitignored, never committed) is sourced if present;
# * DEV_RAIL=transfer|fake overrides the automatic choice;
# * default is TRANSFER, because that is what production serves. Dev used to
# default to the fake rail, which meant the bank rail's actual page — the
# IBAN, the beneficiary name, the reference, the whole thing a buyer reads
# — could not be looked at locally at all.
#
# A live_ key is refused outright. Dev creates throwaway orders; pointing them
# at real money collection is never what anyone meant.
# The transfer rail needs no credential, so dev can run the REAL rail rather
# than a stand-in: it only needs an account to name. If .env does not name one,
# obviously-fake values are used, and they are labelled as such on purpose —
# never make fixture data look real (a sample financials file was once mistaken
# for the actual books).
#
# Nothing here can collect real money: the transfer rail only RENDERS account
# details, and settlement comes from a credits file this machine writes.
if [ -f .env ]; then
set -a; . ./.env; set +a
fi
RAIL="${DEV_RAIL:-}"
if [ -z "$RAIL" ]; then
RAIL=fake
[ -n "${MOLLIE_API_KEY:-}" ] && RAIL=mollie
fi
if [ "$RAIL" = mollie ]; then
case "${MOLLIE_API_KEY:-}" in
test_*) echo "dev: payments via Mollie TEST mode" ;;
live_*) echo "dev: refusing to run dev against a LIVE Mollie key." >&2
echo "dev: live keys belong in /etc/catcrafts/payments.env on the server." >&2
exit 1 ;;
*) echo "dev: MOLLIE_API_KEY is not a test_ or live_ key" >&2; exit 1 ;;
esac
RAIL="${DEV_RAIL:-transfer}"
if [ "$RAIL" = transfer ]; then
# Refuse a real-looking IBAN that is not yours to be paid into by mistake?
# No — the opposite risk matters here. These values are only ever RENDERED
# in dev; nothing can be paid. What must not happen is dev quietly showing
# the PRODUCTION account while someone screenshots the page, so when .env
# supplies nothing the placeholders say plainly that they are placeholders.
: "${TRANSFER_IBAN:=NL00DEVB0000000000}"
: "${TRANSFER_BENEFICIARY:=DEV PLACEHOLDER, not a real account}"
: "${TRANSFER_BIC:=DEVBNL2A}"
export TRANSFER_IBAN TRANSFER_BENEFICIARY TRANSFER_BIC
if [ "$TRANSFER_IBAN" = NL00DEVB0000000000 ]; then
echo "dev: bank transfer rail with PLACEHOLDER account details"
echo "dev: put TRANSFER_IBAN / TRANSFER_BENEFICIARY / TRANSFER_BIC in .env to preview the real ones"
else
echo "dev: bank transfer rail, account $TRANSFER_IBAN ($TRANSFER_BENEFICIARY)"
fi
fi
# /financials has two inputs and a fresh dev run has neither: sales fold out of
@ -247,7 +258,16 @@ cat <<EOF
Edit the aggregates and just refresh — they are re-read every request:
\$EDITOR $WORK/orders.jsonl.financials.json")
Payment rail: $RAIL$([ "$RAIL" = fake ] && printf '%s' " — simulate a customer paying with:
touch $WORK/orders.jsonl.fake-paid")
touch $WORK/orders.jsonl.fake-paid")$([ "$RAIL" = transfer ] && printf '%s' " — the order page shows
the account details a buyer would transfer to. To simulate the money arriving,
append the credit the bank would have reported, using the order's own
reference (the CC-… code on the order page):
tools/dev-credit.sh $WORK/orders.jsonl CC-XXXXXX 57043
The reconciler picks it up within a minute and the order flips to paid.
Reference matching is deliberately forgiving, so try lower case, spaces, or
the RF… form to see that they all still settle the same order.")
Ctrl-C to stop.
EOF

View file

@ -157,7 +157,7 @@ ORDERS=$(systemctl cat "$UNIT" | sed -n 's/^[[:space:]]*--orders=\([^ \\]*\).*/\
[ -n "$ORDERS" ] || ORDERS=/var/lib/catcrafts/orders.jsonl
ENVF=$(systemctl cat "$UNIT" | sed -n 's/^EnvironmentFile=-\{0,1\}\(.*\)/\1/p' | head -1)
[ -n "$ENVF" ] || ENVF=/etc/catcrafts/payments.env
[ -e "$ENVF" ] || { echo "apply: $ENVF does not exist — is the Mollie side even configured?" >&2; exit 1; }
[ -e "$ENVF" ] || { echo "apply: $ENVF does not exist — is the bank rail even configured?" >&2; exit 1; }
# Respect an explicit EURC_POOL override if one is already configured;
# otherwise the server's default: the pool hangs off the orders path.
@ -198,8 +198,8 @@ install -m 0644 "$work/chains.json" "$CHAINS_DEST"
#
# Two bugs lived in the one-liner this replaces. First, payments.env is
# hand-maintained, so its last line may have no trailing newline — and then a
# bare >> concatenated onto it, turning MOLLIE_API_KEY=live_abc into
# MOLLIE_API_KEY=live_abcEURC_CHAINS=/etc/... : both rails broken, and the
# bare >> concatenated onto it, turning TRANSFER_IBAN=NL00 into
# TRANSFER_IBAN=NL00EURC_CHAINS=/etc/... : both rails broken, and the
# rollback below could not even see it because the line no longer started with
# EURC_CHAINS. Second, the rollback deleted EVERY EURC_CHAINS= line, including
# one the operator had set themselves pointing at a different chains file — so