Files
atomic-design-poc/backend/openzaak/bootstrap-catalogus.sh
T
ehoandClaude Opus 5 6cfd70eeeb fix(backend): resolve besluit endpoint's id via Referentie, not local PK
POST /beoordeling/{id}/besluit always 404'd against a real OpenZaak: {id} is the
FE-facing case id from IZaakSource.ListCases, which under OpenZaakZaakSource is the
ZGW zaak's own uuid, not ApplicationStore's primary key. Resolve the case through
ListCases first (same seam the GET sibling already uses), then to the local Aanvraag
via its Referentie — the one identifier stable across both sources.

Adds ApplicationStore.GetByReferentie and a regression test that reproduces the
divergence with a decorating IZaakSource test double instead of a live OpenZaak.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:20:36 +02:00

278 lines
13 KiB
Bash
Executable File

#!/usr/bin/env bash
# WP-54 (seeding) / WP-56 (idempotency) — seeds business content
# (catalogus/zaaktype/statustype/roltype/zaak/status/rol) into the OpenZaak harness started
# by docker-compose.openzaak.yml. `setup_configuration/data.yaml` only covers infra config
# (JWTSecret + Applicatie) — confirmed by reading the installed `django_setup_configuration`
# steps inside the `openzaak/open-zaak` image itself: the only app-registered step besides the
# generic sites/credentials/applicaties ones is Selectielijst API config. There is NO
# declarative-YAML equivalent upstream for Catalogi/Zaken content, so this script does it the
# same way the BFF itself does at runtime — plain REST calls with a hand-rolled HS256 JWT (see
# ZgwTokenProvider.cs, mirrored here in bash+openssl so this script has no extra dependency
# beyond curl/openssl/python3, python3 already required by the JSON bodies below).
#
# Idempotent: every resource is looked up by its natural key (GET with the same filter OpenZaak
# enforces uniqueness/identity on) before creating it, so re-running against an
# already-seeded instance reuses what's there instead of erroring or duplicating. Safe to run
# repeatedly against a long-lived instance, not just once per fresh volume. Prints the seeded
# zaak's `identificatie` + `url` on success; also writes them to seeded.env (repo-ignored) for
# OpenZaakIntegrationTests.cs to assert against.
#
# WP-57: `bigregister-test` starts with ZERO Autorisaties (data.yaml sets
# heeft_alle_autorisaties: false) — the setup_configuration YAML has no field for granular
# scopes at all (confirmed from vng_api_common's own ApplicatieConfigurationModel), so this
# script grants them itself via `manage.py shell` (Django ORM, inside the `web` container) at
# the two points they become grantable: ztc scopes up front (no zaaktype dependency), zrc
# scopes once `zaaktype_url` exists below. Going through the ORM instead of the
# JWT-authenticated Autorisaties REST API sidesteps a real chicken-and-egg: a client with zero
# scopes cannot grant itself any scope over that API. Re-running this script re-grants the same
# scopes (idempotent, like everything else here).
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")"
BASE="http://localhost:8000"
CLIENT_ID="bigregister-test"
SECRET="bigregister-test-secret"
RSIN="123443210" # elfproef-valid RSIN, already used as the fixture Bronorganisatie
# in OpenZaakZaakSourceTests.cs — reused here for consistency.
BSN="111222333" # elfproef-valid BSN, already used as the fixture caller BSN.
ZAAK_REF="BIG-2026-000123"
b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
jwt() {
local header='{"alg":"HS256","typ":"JWT"}'
local payload
payload=$(printf '{"iss":"%s","iat":%d,"client_id":"%s","user_id":"%s","user_representation":"%s"}' \
"$CLIENT_ID" "$(date +%s)" "$CLIENT_ID" "$CLIENT_ID" "bootstrap")
local h p signing_input sig
h=$(printf '%s' "$header" | b64url)
p=$(printf '%s' "$payload" | b64url)
signing_input="$h.$p"
sig=$(printf '%s' "$signing_input" | openssl dgst -sha256 -hmac "$SECRET" -binary | b64url)
printf '%s.%s' "$signing_input" "$sig"
}
# $1 = method, $2 = path, $3 = JSON body (optional)
oz() {
local method="$1" path="$2" body="${3:-}"
# Content-Crs/Accept-Crs: every ZGW write must declare a coordinate reference system even
# when no geometry is involved — OpenZaak 412s without it.
local args=(-sS -X "$method" -H "Authorization: Bearer $(jwt)" -H "Content-Type: application/json" \
-H "Content-Crs: EPSG:4326" -H "Accept-Crs: EPSG:4326")
[ -n "$body" ] && args+=(-d "$body")
local response
response=$(curl "${args[@]}" -w $'\n%{http_code}' "$BASE$path")
local http_code="${response##*$'\n'}"
local json="${response%$'\n'*}"
if [[ ! "$http_code" =~ ^2 ]]; then
echo "FAILED $method $path -> $http_code: $json" >&2
exit 1
fi
echo "$json"
}
# Grant (replace) an Autorisatie for $CLIENT_ID directly via the ORM (see the WP-57 note up
# top for why this bypasses the REST Autorisaties API). $1 = component, $2 = python list
# literal of scopes, $3.. = extra `Autorisatie(...)` kwargs as `name=value` (value already a
# valid Python literal, e.g. a quoted URL).
grant_scopes() {
local component="$1" scopes="$2"
shift 2
local extra="" kv
for kv in "$@"; do extra+=" $kv,"$'\n'; done
docker compose -f docker-compose.openzaak.yml exec -T --workdir /app/src web python manage.py shell <<PY
from vng_api_common.authorizations.models import Applicatie
app = Applicatie.objects.get(client_ids__contains=["$CLIENT_ID"])
app.autorisaties.filter(component="$component").delete()
app.autorisaties.create(
component="$component",
scopes=$scopes,
$extra)
PY
}
# $1 = list path+query (server-side-filtered to the natural key). Prints the first result's
# `url`, or nothing if the list is empty — the GET-before-POST idempotency check.
existing_url() {
oz GET "$1" | python3 -c 'import json,sys; r=json.load(sys.stdin)["results"]; print(r[0]["url"] if r else "")'
}
# $1 = zaaktype URL, $2 = volgnummer. Statustype has no server-side volgnummer filter, so this
# lists by zaaktype (server-filtered) and matches volgnummer client-side.
existing_statustype_url() {
oz GET "/catalogi/api/v1/statustypen?zaaktype=$1" | python3 -c '
import json, sys
data = json.load(sys.stdin)
vol = int(sys.argv[1])
for r in data["results"]:
if r["volgnummer"] == vol:
print(r["url"])
break
' "$2"
}
echo "Waiting for OpenZaak..."
until curl -sS -o /dev/null -w '%{http_code}' "$BASE/catalogi/api/v1/catalogussen" | grep -q '^2\|^401\|^403'; do
sleep 2
done
echo "Granting ztc scopes (catalogi.lezen, catalogi.schrijven — this script's own content-creation needs; the BFF only ever reads Catalogi)..."
grant_scopes ztc '["catalogi.lezen", "catalogi.schrijven"]'
echo "Catalogus..."
catalogus_url=$(existing_url "/catalogi/api/v1/catalogussen?domein=BIGR&rsin=$RSIN")
if [ -n "$catalogus_url" ]; then
echo " exists: $catalogus_url"
else
catalogus=$(oz POST /catalogi/api/v1/catalogussen "$(printf '{"domein":"BIGR","rsin":"%s","contactpersoonBeheerNaam":"BIG Register"}' "$RSIN")")
catalogus_url=$(echo "$catalogus" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
echo " created: $catalogus_url"
fi
echo "Zaaktype (concept)..."
zaaktype_url=$(existing_url "/catalogi/api/v1/zaaktypen?catalogus=$catalogus_url&identificatie=ZT-HERREG")
if [ -n "$zaaktype_url" ]; then
echo " exists: $zaaktype_url"
else
zaaktype=$(oz POST /catalogi/api/v1/zaaktypen "$(python3 -c '
import json, sys
print(json.dumps({
"identificatie": "ZT-HERREG",
"omschrijving": "Herregistratie arts",
"vertrouwelijkheidaanduiding": "openbaar",
"doel": "Herregistratie in het BIG-register",
"aanleiding": "Aanvraag door de zorgverlener",
"indicatieInternOfExtern": "extern",
"handelingInitiator": "indienen",
"onderwerp": "Herregistratie",
"handelingBehandelaar": "behandelen",
"doorlooptijd": "P30D",
"opschortingEnAanhoudingMogelijk": False,
"verlengingMogelijk": False,
"publicatieIndicatie": False,
"productenOfDiensten": ["https://example.com/producten/herregistratie"],
"referentieproces": {"naam": "Herregistratie"},
"verantwoordelijke": "CIBG",
"catalogus": sys.argv[1],
"beginGeldigheid": "2026-01-01",
"versiedatum": "2026-01-01",
"besluittypen": [],
"gerelateerdeZaaktypen": [],
# Must belong to the same procestype as the resultaattype selectielijstklasse below
# (OpenZaak cross-checks this against the public VNG selectielijst API).
"selectielijstProcestype": "https://selectielijst.openzaak.nl/api/v1/procestypen/e1b73b12-b2f6-4c4e-8929-94f84dd2a57d",
}))
' "$catalogus_url")")
zaaktype_url=$(echo "$zaaktype" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
echo " created: $zaaktype_url"
fi
echo "Granting zrc scopes (zaken.aanmaken, zaken.bijwerken, zaken.lezen, zaken.statussen.toevoegen), scoped to $zaaktype_url — the one zaaktype this harness (and the BFF's Zgw:ZaaktypeUrls config) ever uses. zaken.statussen.toevoegen is needed for WP-66's besluit write: zaken.aanmaken only covers the ONE status set at zaak creation, a later status (the besluit's eindstatus) needs this scope or OpenZaak 403s ('mag je slechts 1 status zetten')..."
grant_scopes zrc '["zaken.aanmaken", "zaken.bijwerken", "zaken.lezen", "zaken.statussen.toevoegen"]' \
"zaaktype=\"$zaaktype_url\"" \
'max_vertrouwelijkheidaanduiding="openbaar"'
echo "Statustypen (publish needs a begin AND an end status)..."
statustype_url=$(existing_statustype_url "$zaaktype_url" 1)
if [ -n "$statustype_url" ]; then
echo " exists (Ontvangen): $statustype_url"
else
statustype_url=$(oz POST /catalogi/api/v1/statustypen "$(printf '{"zaaktype":"%s","omschrijving":"Ontvangen","volgnummer":1}' "$zaaktype_url")" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
echo " created (Ontvangen): $statustype_url"
fi
statustype_eind_url=$(existing_statustype_url "$zaaktype_url" 2)
if [ -n "$statustype_eind_url" ]; then
echo " exists (Afgehandeld): $statustype_eind_url"
else
statustype_eind_url=$(oz POST /catalogi/api/v1/statustypen "$(printf '{"zaaktype":"%s","omschrijving":"Afgehandeld","volgnummer":2}' "$zaaktype_url")" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
echo " created (Afgehandeld): $statustype_eind_url"
fi
echo "Resultaattype (publish needs at least one; existence-only requirement)..."
if [ "$(oz GET "/catalogi/api/v1/resultaattypen?zaaktype=$zaaktype_url" | python3 -c 'import json,sys; print(len(json.load(sys.stdin)["results"]))')" != "0" ]; then
echo " exists"
else
# The two URLs below are real reference-list entries on the public VNG selectielijst API
# (selectielijst.openzaak.nl) — OpenZaak validates both by fetching them, same as it does
# for a zaaktype URL, so a made-up URL 404s here.
oz POST /catalogi/api/v1/resultaattypen "$(printf '{"zaaktype":"%s","omschrijving":"Afgehandeld","resultaattypeomschrijving":"https://selectielijst.openzaak.nl/api/v1/resultaattypeomschrijvingen/7cb315fb-4f7b-4a43-aca1-e4522e4c73b3","selectielijstklasse":"https://selectielijst.openzaak.nl/api/v1/resultaten/cc5ae4e3-a9e6-4386-bcee-46be4986a829","archiefnominatie":"blijvend_bewaren"}' "$zaaktype_url")" >/dev/null
echo " created"
fi
echo "Roltype (initiator)..."
roltype_url=$(existing_url "/catalogi/api/v1/roltypen?zaaktype=$zaaktype_url&omschrijvingGeneriek=initiator")
if [ -n "$roltype_url" ]; then
echo " exists: $roltype_url"
else
roltype_url=$(oz POST /catalogi/api/v1/roltypen "$(printf '{"zaaktype":"%s","omschrijving":"Initiator","omschrijvingGeneriek":"initiator"}' "$zaaktype_url")" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
echo " created: $roltype_url"
fi
echo "Publishing zaaktype..."
zaaktype_uuid=$(echo "$zaaktype_url" | sed 's#.*/##')
zaaktype_concept=$(oz GET "/catalogi/api/v1/zaaktypen/$zaaktype_uuid" | python3 -c 'import json,sys; print(json.load(sys.stdin)["concept"])')
if [ "$zaaktype_concept" = "False" ]; then
echo " already published"
else
oz POST "/catalogi/api/v1/zaaktypen/$zaaktype_uuid/publish" >/dev/null
echo " published"
fi
echo "Zaak..."
zaak_url=$(existing_url "/zaken/api/v1/zaken?identificatie=$ZAAK_REF")
if [ -n "$zaak_url" ]; then
echo " exists: $zaak_url"
else
zaak=$(oz POST /zaken/api/v1/zaken "$(python3 -c '
import json, sys
print(json.dumps({
"zaaktype": sys.argv[1],
"bronorganisatie": sys.argv[2],
"verantwoordelijkeOrganisatie": sys.argv[2],
"startdatum": "2026-07-28",
"identificatie": sys.argv[3],
}))
' "$zaaktype_url" "$RSIN" "$ZAAK_REF")")
zaak_url=$(echo "$zaak" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
echo " created: $zaak_url"
fi
echo "Status..."
if [ "$(oz GET "/zaken/api/v1/statussen?zaak=$zaak_url" | python3 -c 'import json,sys; print(len(json.load(sys.stdin)["results"]))')" != "0" ]; then
echo " exists"
else
oz POST /zaken/api/v1/statussen "$(python3 -c '
import json, sys
print(json.dumps({"zaak": sys.argv[1], "statustype": sys.argv[2], "datumStatusGezet": "2026-07-28T12:00:00Z"}))
' "$zaak_url" "$statustype_url")" >/dev/null
echo " created"
fi
echo "Rol (initiator, seeded BSN)..."
if [ -n "$(existing_url "/zaken/api/v1/rollen?zaak=$zaak_url&omschrijvingGeneriek=initiator")" ]; then
echo " exists"
else
oz POST /zaken/api/v1/rollen "$(python3 -c '
import json, sys
print(json.dumps({
"zaak": sys.argv[1],
"betrokkeneType": "natuurlijk_persoon",
"roltype": sys.argv[2],
"roltoelichting": "Initiator",
"betrokkeneIdentificatie": {"inpBsn": sys.argv[3]},
}))
' "$zaak_url" "$roltype_url" "$BSN")" >/dev/null
echo " created"
fi
cat > seeded.env <<EOF
ZAAK_REFERENTIE=$ZAAK_REF
ZAAK_URL=$zaak_url
ZAAKTYPE_LABEL=Herregistratie arts
CALLER_BSN=$BSN
EOF
echo
echo "Seed complete. $ZAAK_REF ($zaak_url) — see seeded.env"