finacial page
All checks were successful
Deploy / build-deploy (push) Successful in 2m20s

This commit is contained in:
Jorijn van der Graaf 2026-08-14 02:50:58 +02:00
commit e68d2c245c
17 changed files with 1801 additions and 15 deletions

View file

@ -86,6 +86,12 @@ chmod +x "$WORK/sendmail"
export MAIL_COMMAND="$WORK/sendmail"
export MAIL_FROM='Catcrafts <info@catcrafts.net>'
# The bunq mutation callback. The secret IS the last segment of the callback
# URL, and setting it is what brings the endpoint into existence — unset, the
# path is an ordinary 404. Note what is NOT here: a bunq API key. One could
# initiate payments, so no such key ever reaches the server; it only receives.
export BUNQ_CALLBACK_SECRET='e2e-callback-secret-not-a-real-one'
# The shipping rate table. Shipping has no compiled-in fallback any more — the
# carrier table is the only source of prices — so without this file every
# checkout correctly refuses and the whole order suite would be testing the
@ -170,6 +176,7 @@ header_has() {
echo "== status codes =="
for p in / /about /shop /shop/fp6-pmos /projects /posts /demos /demos/raytracer \
/financials \
/legal/privacy /legal/terms /legal/imprint /feed.xml /sitemap.xml /api/healthz; do
status "$p" 200
done
@ -219,10 +226,12 @@ body_has /projects "imsd" "/projects has content in the
body_has /projects "<title>Projects" "/projects has a real title"
body_lacks /projects "<script" "/projects ships no script at all"
body_lacks /legal/privacy "<script" "/legal/privacy ships no script"
body_has /financials "<title>Financials" "/financials has a real title"
body_lacks /financials "<script" "/financials ships no script"
# Placeholders are dev-only markers; one reaching production is a content bug
# (an imprint that says PLACEHOLDER once shipped exactly that way).
for pg in /legal/privacy /legal/terms /legal/imprint /shop/fp6-pmos; do
for pg in /legal/privacy /legal/terms /legal/imprint /shop/fp6-pmos /financials; do
body_lacks "$pg" 'PLACEHOLDER' "$pg ships no placeholder markers"
done
@ -627,6 +636,120 @@ body_has /sitemap.xml "/shop/fp6-pmos" "sitemap lists the product"
body_has /sitemap.xml "/about" "sitemap lists the about page"
body_has /sitemap.xml "/legal/privacy" "sitemap lists the privacy page"
body_has /sitemap.xml "/demos" "sitemap lists the demos page"
body_has /sitemap.xml "/financials" "sitemap lists the financials page"
echo "== the financials page =="
# Aggregate-only by construction: totals and counts, machine-readable for
# this suite via the data-fin-* attributes. Live is the page's promise, so
# it must never sit in a shared cache.
header_has /financials 'cache-control: *no-store' "financials are never cached"
body_has /financials 'data-fin-sales-count="0"' "financials start at zero sales"
# Before the bank-aggregates file exists the page says so, and publishes no
# donation figures at all — an unknowable €0 would be a lie.
body_has /financials 'not been published yet' "unpublished bank figures say so"
body_lacks /financials 'data-fin-donations-count' "no donation figures before the file exists"
# The aggregates file appears, exactly as the owner's tooling will write it,
# and the very next request reflects it: no restart, no cache, no delay —
# this is the liveness the donation counter depends on.
cat >"$ORDERS.financials.json" <<'JSON'
{"as_of":"2026-08-14",
"donations":{"count":3,"total_minor":4500},
"recurring":[{"label":"Hosting","total_minor":1200}],
"single":[{"label":"Inventory","total_minor":230000}]}
JSON
body_has /financials 'data-fin-donations-count="3"' "donation count picked up live"
body_has /financials 'data-fin-expenses-minor="231200"' "expense total picked up live"
body_has /financials 'Hosting' "recurring category renders"
body_has /financials 'Inventory' "one-off category renders"
body_has /financials '2026-08-14' "bank figures carry their as-of date"
echo "== the bunq mutation callback =="
# The live path: bunq PUSHES a mutation, the server classifies it against the
# rules file and folds it into the totals. This is what makes a donation tick
# the public counter while the donor is still looking at the page.
#
# The rules are written here rather than at startup on purpose — they are
# re-read per callback, so a new rule takes effect without a restart.
cat >"$ORDERS.financial-rules.json" <<'JSON'
{"donation_accounts":[9911],
"rules":[{"description_contains":"hetzner","group":"recurring","label":"Hosting"},
{"iban":"NL01OWNSELF0000000","group":"ignore"}]}
JSON
CB="/api/bunq/$BUNQ_CALLBACK_SECRET"
# bunq_post <id> <account> <value> <iban> <description> -> HTTP status
bunq_post() {
curl -s -o /dev/null -w '%{http_code}' -X POST \
-H 'content-type: application/json' \
--data-binary "$(printf '{"NotificationUrl":{"category":"MUTATION","event_type":"MUTATION_CREATED","object":{"Payment":{"id":%s,"created":"2026-08-15 09:31:02.000000","monetary_account_id":%s,"amount":{"currency":"EUR","value":"%s"},"description":"%s","counterparty_alias":{"iban":"%s","display_name":"Someone"}}}}}' \
"$1" "$2" "$3" "$5" "$4")" \
"$BASE$CB"
}
# fin_attr <attribute> -> its value on the live page
fin_attr() { curl -s "$BASE/financials" | grep -o "$1=\"[0-9]*\"" | cut -d'"' -f2; }
# An endpoint guarded by a secret must not confirm its own existence: every
# unauthorised shape is the same 404 an unknown order token gets.
status "/api/bunq/wrong-secret" 404 POST '{}'
status "$CB" 404 # GET on the right URL is still not a callback
status "$CB" 404 HEAD
# A donation arrives on the donation account. No rule names the sender —
# donors are strangers, which is exactly why the account is what classifies.
if [ "$(bunq_post 4823 9911 25.00 NL55BUNQ2025123456 'Thanks for imsd')" = 200 ]; then
ok "the callback accepts a mutation"
else
bad "bunq callback" "a valid notification was not accepted"
fi
if [ "$(fin_attr data-fin-donations-count)" = 4 ] \
&& [ "$(fin_attr data-fin-donations-minor)" = 7000 ]; then
ok "a donation ticks the public counter immediately"
else
bad "donation ingest" "counter did not move to 4 / 7000 cents"
fi
# bunq redelivers a callback it did not see a 2xx for, and can redeliver one
# it did. Counting that twice would publish money that never arrived.
bunq_post 4823 9911 25.00 NL55BUNQ2025123456 'Thanks for imsd' >/dev/null
if [ "$(fin_attr data-fin-donations-count)" = 4 ] \
&& [ "$(fin_attr data-fin-donations-minor)" = 7000 ]; then
ok "a redelivered mutation is not counted twice"
else
bad "callback idempotency" "a duplicate mutation moved the totals"
fi
# Default-deny: money no rule claims is WITHHELD from the page. It is logged
# for classification, never published as a guess.
if [ "$(bunq_post 4824 1234 90.00 NL99UNKNOWN00000000 'unlabelled transfer')" = 200 ]; then
ok "an unclassifiable mutation is still accepted (no redelivery loop)"
else
bad "unclassified mutation" "the callback answered non-2xx and will be retried forever"
fi
if [ "$(fin_attr data-fin-donations-count)" = 4 ] \
&& [ "$(fin_attr data-fin-expenses-minor)" = 231200 ]; then
ok "an unclassified mutation is withheld from every total"
else
bad "default-deny" "an unmatched mutation reached the public figures"
fi
# An outgoing bill matched by description becomes a positive expense.
bunq_post 4825 9911 -12.00 DE00HETZNER00000000 'HETZNER ONLINE GMBH' >/dev/null
if [ "$(fin_attr data-fin-expenses-minor)" = 232400 ]; then
ok "an outgoing bill lands in its expense category"
else
bad "expense ingest" "expenses did not move to 232400 cents"
fi
body_has /financials '2026-08-15' "the as-of date advances with the mutations"
# The page still publishes nothing but aggregates: no counterparty, no
# description, no id, no timestamp. This is the assertion that would catch a
# well-meant future edit adding a "recent activity" list.
for leak in 'NL55BUNQ2025123456' 'Someone' 'Thanks for imsd' '4823' '09:31'; do
body_lacks /financials "$leak" "financials leak no transaction detail ($leak)"
done
# And nothing identifying was written to disk either — the ingest ledger holds
# opaque ids and counters, and no other file learned the donor exists.
if grep -rlF 'NL55BUNQ2025123456' "$WORK" >/dev/null 2>&1; then
bad "callback storage" "a counterparty IBAN was persisted somewhere under $WORK"
else
ok "no counterparty IBAN is persisted anywhere"
fi
echo "== instance-agnostic copy =="
# The account lives on one instance but posts go into communities on others, so
@ -1108,6 +1231,38 @@ else
bad "notified event" "no notified event in $ORDERS"
fi
echo "== financials reflect the ledger =="
# Lifetime sales on /financials must equal the ledger: sum of total_minor over
# orders that have a paid status event. Derived from the ledger rather than
# written as a literal — same rule as the email count above, and for the same
# reason: "the page equals the ledger" is the actual property.
want_minor=0; want_count=0
for pid in $(grep '"type":"status"' "$ORDERS" | grep '"status":"paid"' \
| grep -o '"id":"[0-9a-f]\{32\}"' | grep -o '[0-9a-f]\{32\}' | sort -u); do
t=$(grep '"type":"order"' "$ORDERS" | grep -F "\"id\":\"$pid\"" \
| grep -o '"total_minor":[0-9]*' | head -n1 | cut -d: -f2)
want_minor=$((want_minor + t)); want_count=$((want_count + 1))
done
fin_page=$(curl -s "$BASE/financials")
got_minor=$(printf '%s' "$fin_page" | grep -o 'data-fin-sales-minor="[0-9]*"' | cut -d'"' -f2)
got_count=$(printf '%s' "$fin_page" | grep -o 'data-fin-sales-count="[0-9]*"' | cut -d'"' -f2)
if [ "$want_count" -gt 0 ] && [ "$got_minor" = "$want_minor" ] && [ "$got_count" = "$want_count" ]; then
ok "sales totals equal the ledger ($want_count orders, $want_minor cents)"
else
bad "financials sales" "ledger says $want_count/$want_minor, page says $got_count/$got_minor"
fi
# And the formatted euro figure for that total appears on the page.
if [ $((want_minor % 100)) -eq 0 ]; then
eur=$(printf '€%d' $((want_minor / 100)))
else
eur=$(printf '€%d.%02d' $((want_minor / 100)) $((want_minor % 100)))
fi
if printf '%s' "$fin_page" | grep -qF -- "$eur"; then
ok "sales total renders as $eur"
else
bad "financials formatting" "page lacks $eur"
fi
else
echo "== checkout (coming soon) =="
# A perfectly valid order must be refused while the shop is closed: after