0910bc32c1
Uniform stock management for healthcare linen rooms: the coordinator app, the phone counter and the staff app, for your own server. Built from 38e16eb on 2026-09-15. Licensed under the Functional Source License (FSL-1.1-ALv2).
79 lines
4.8 KiB
Bash
79 lines
4.8 KiB
Bash
#!/usr/bin/env bash
|
|
# Cost history: that a price is remembered, that it records what it moved from, and that a change
|
|
# which isn't a change doesn't produce a row.
|
|
set -u
|
|
B=${BASE:-http://127.0.0.1:3111}
|
|
# Refuses early, with the fix, when the server under test is in production mode with
|
|
# Turnstile refusing every auth route — otherwise the first signup fails and every check
|
|
# after it reports a security-check error instead of what it was testing.
|
|
. "$(dirname "$0")/e2e-preflight.sh"; e2e_preflight "$B"
|
|
T=${TMP:-/tmp}; J="$T/tc-cost-cj.txt"; rm -f "$J"
|
|
PASS=0; FAIL=0
|
|
ok() { PASS=$((PASS+1)); echo " ✓ $1"; }
|
|
fail() { FAIL=$((FAIL+1)); echo " ✗ $1 :: $2"; }
|
|
mut() { curl -s -b "$J" -c "$J" -X POST "$B/api/mutate" -H 'content-type: application/json' -d "{\"op\":\"$1\",\"payload\":$2}"; }
|
|
check(){ local name=$1 out=$2 pat=$3; if echo "$out" | grep -q "$pat"; then ok "$name"; else fail "$name" "$(echo "$out" | head -c 250)"; fi; }
|
|
py() { python3 -c "import sys,json; d=json.load(sys.stdin); $1"; }
|
|
bk() { curl -s -b "$J" "$B/api/backup"; }
|
|
costs(){ curl -s -b "$J" "$B/m/catalogue" > /dev/null; bk; }
|
|
|
|
TS=$(date +%s)
|
|
echo "== setup"
|
|
check "signup" "$(curl -s -c "$J" -X POST "$B/api/auth/signup" -H 'content-type: application/json' \
|
|
-H "x-forwarded-for: 10.16.$((RANDOM%250)).$((RANDOM%250))" \
|
|
-d "{\"first\":\"Cora\",\"last\":\"Cost\",\"facility\":\"Cost Hospital $TS\",\"email\":\"cost$TS@example.com\",\"password\":\"password123\"}")" '"ok":true'
|
|
check "the facility names its staff groups" "$(e2e_groups "$B" "$J")" '"ok":true'
|
|
|
|
R=$(mut catalog.add '{"item":"Priced Gown","sku":"PG1","supplier":"Alpha Supply","cost":30,"sizes":["S","M"]}')
|
|
check "garment created" "$R" '"id"'
|
|
ID=$(echo "$R" | py "print(d['result']['id'])")
|
|
|
|
# The whole history for this garment, oldest first, each row as "cost<-previous". Counting rows can
|
|
# only ever say that a row appeared; the figures in it are what finance actually reads back.
|
|
hist() { bk | py "print(' | '.join('%g<-%s' % (c['cost'], 'none' if c['previous'] is None else '%g' % c['previous']) for c in d.get('costs',[]) if c['itemId']=='$ID'))"; }
|
|
itemcost() { bk | py "print('%g' % [i['cost'] for i in d['items'] if i['id']=='$ID'][0])"; }
|
|
|
|
echo "== the opening price is remembered"
|
|
check "the price landed on the garment" "$(itemcost)" '^30$'
|
|
# previous is null rather than a figure, which is what makes this row the start of the history
|
|
# instead of an edit somebody made on day one.
|
|
check "one opening row, at the opening price" "$(hist)" '^30<-none$'
|
|
|
|
echo "== a change records what it moved from"
|
|
check "raise the price" "$(mut catalog.update "{\"id\":\"$ID\",\"cost\":34.5}")" '"ok":true'
|
|
N1=$(bk | py "print(len([c for c in d.get('costs',[]) if c['itemId']=='$ID']))")
|
|
check "two rows now" "$N1" '^2$'
|
|
check "the latest is the new price" "$(bk | py "print([c['cost'] for c in d.get('costs',[]) if c['itemId']=='$ID'][-1])")" '34.5'
|
|
check "and remembers the old one" "$(bk | py "print([c['previous'] for c in d.get('costs',[]) if c['itemId']=='$ID'][-1])")" '^30'
|
|
check "and who changed it" "$(bk | py "print([c['byName'] for c in d.get('costs',[]) if c['itemId']=='$ID'][-1])")" 'Cora Cost'
|
|
|
|
echo "== setting the same price again is not a change"
|
|
check "save the identical price" "$(mut catalog.update "{\"id\":\"$ID\",\"cost\":34.5}")" '"ok":true'
|
|
check "no row for the identical price" "$(hist)" '^30<-none | 34\.5<-30$'
|
|
|
|
echo "== editing other fields leaves the history alone"
|
|
check "rename it" "$(mut catalog.update "{\"id\":\"$ID\",\"item\":\"Priced Gown (Blue)\"}")" '"ok":true'
|
|
check "no row for a rename" "$(hist)" '^30<-none | 34\.5<-30$'
|
|
|
|
echo "== a price drop is recorded as a drop"
|
|
check "lower the price" "$(mut catalog.update "{\"id\":\"$ID\",\"cost\":29}")" '"ok":true'
|
|
check "a third row, 34.5 down to 29" "$(hist)" '^30<-none | 34\.5<-30 | 29<-34\.5$'
|
|
|
|
echo "== a negative price is refused on an edit"
|
|
check "negative refused" "$(mut catalog.update "{\"id\":\"$ID\",\"cost\":-5}")" 'Invalid cost'
|
|
# Refused has to mean nothing was written. An error message sent after the update had already gone
|
|
# through would read exactly the same from out here, with the garment left priced at -5.
|
|
check "the price is untouched" "$(itemcost)" '^29$'
|
|
check "and the history is untouched" "$(hist)" '^30<-none | 34\.5<-30 | 29<-34\.5$'
|
|
|
|
echo "== a negative price on creation is clamped, not refused"
|
|
# catalog.add takes Math.max(0, cost) where catalog.update throws, so the two doors disagree about
|
|
# the same bad figure. Pinned here so that whichever way it is settled, it is settled deliberately.
|
|
R2=$(mut catalog.add '{"item":"Odd Gown","sku":"OG1","supplier":"Alpha Supply","cost":-5,"sizes":["S"]}')
|
|
check "creation accepted" "$R2" '"id"'
|
|
ID=$(echo "$R2" | py "print(d['result']['id'])")
|
|
check "the price is zero, not negative" "$(itemcost)" '^0$'
|
|
check "and a zero price opens no history" "$(hist)" '^$'
|
|
|
|
echo; echo "PASS=$PASS FAIL=$FAIL"; [ "$FAIL" -eq 0 ]
|