Expliciete reason-code toevoegen aan de analysis-productrespons wanneer geen risicoklasse kan worden geleverd
Let op — repo: dit betreft de v4-webservice Laixer/FunderMapsWebservice (TypeScript/Bun + Hono), niet de .NET-repo Laixer/FunderMaps. De productie-endpoints (/v4/product/...) draaien hier en geven platte JSON-fouten terug ({"message": "..."}), geen RFC 7807 ProblemDetails.
Context
Voor de NWWI-koppeling (funderingsrisicorapport in het taxatieproces) moet de aanroepende partij per opvraag geautomatiseerd kunnen bepalen waarom er geen risicoklasse is, en welke vervolgstap daarbij hoort:
| Oorzaak |
Gewenste vervolgstap aan afnemerskant |
| a. Objecttype (geen BAG-pand: ligplaats/woonboot/standplaats) |
geen actie — QuickScan niet zinvol |
| b. Onvoldoende broninformatie (pand bekend, geen klasse) |
QuickScan aanvragen |
| c. Onjuiste/ontbrekende adressering |
nieuwe aanvraag met correct adres |
| d. Technische storing |
opnieuw aanbieden (retry/queue) |
Op dit moment kan de afnemer deze vier situaties niet uit elkaar houden op basis van de respons: elke "geen resultaat"-uitkomst geeft dezelfde 404 met body {"message": "Not found"}.
Huidig gedrag
Endpoint: GET /v4/product/analysis/:id in src/routes/product.ts.
De handler kent twee aparte 404-takken die vandaag al bestaan, maar dezelfde body teruggeven:
const externalId = await resolveBuildingExternalId(id);
if (!externalId) return c.json({ message: "Not found" }, 404); // ① geen pand/adres herleidbaar → categorie a/c
const rows = await sql`... FROM data.model_risk_static WHERE building_id = ${externalId} LIMIT 1`;
if (rows.length === 0) return c.json({ message: "Not found" }, 404); // ② pand herleid, geen risicorij → categorie b
Daarnaast bestaat een derde "geen klasse"-uitkomst die géén 404 is: als er wél een rij is maar niet geclassificeerd, komt er een 200 OK terug met foundationType/de *Risk-velden op null (de rij uit model_risk_static wordt verbatim teruggegeven; die velden zijn nullable).
Overige statussen (contract, voor de volledigheid):
- 500
{"message": "Internal server error"} — globale app.onError in src/index.ts (echte storing / DB-fout).
- 429
{"message": "Too Many Requests"} — rateLimit()-middleware bij overschrijding van de billing-/quota-limiet (mét Retry-After + X-RateLimit-*). Dit is een quota-limiet, geen storing.
Belangrijk (afwijking t.o.v. eerdere aanname): v4 registreert de misser-gevallen op dit moment niet. De usage-tracker (trackerMiddleware + c.set("tracker", ...)) wordt alleen op de succes-tak gezet; beide 404-takken keren vroeg terug zonder tracking. Er is dus (anders dan in de oude .NET-service) geen product_tracker_mismatch-registratie — de frequentie per oorzaak is nu niet meetbaar. Dat moet daarom onderdeel van deze wijziging zijn.
Gewenst gedrag
Voeg aan de respons een machine-leesbare reason-code toe, zodat de afnemer zonder de body te interpreteren de juiste vervolgstap kan kiezen — en registreer de misser + reden server-side zodat de frequentie rapporteerbaar wordt.
Voorgestelde enum:
reason |
Situatie |
Techn. herkomst (v4) |
HTTP |
NO_BUILDING |
Identifier geldig maar geen BAG-pand (ligplaats/woonboot/standplaats) of geen BAG-match |
tak ① (resolveBuildingExternalId → null) |
404 |
ADDRESS_INVALID |
Identifier-formaat ongeldig / niet-parsebaar |
geocoder (zie open punt) |
404 |
INSUFFICIENT_SOURCE |
Pand bekend, maar geen (volledige) risicoklasse |
tak ② (rows.length === 0) óf 200 met risicovelden null |
404 óf 200 |
UPSTREAM_ERROR |
Interne/afhankelijkheidsstoring |
onError (500) |
500 |
Waar de code moet landen
-
Bij de 404/500-fouten: extra veld op de bestaande platte JSON-body, bijv.:
{ "message": "Not found", "reason": "NO_BUILDING" }
Concreet in src/routes/product.ts: geef in tak ① NO_BUILDING mee en in tak ② INSUFFICIENT_SOURCE; in src/index.ts onError → UPSTREAM_ERROR. De twee takken maken het onderscheid a/c ↔ b nu al — alleen de body deelt de reden nog niet.
-
Bij een 200 zonder klasse: een veld op de analysis-response (bv. reason: "INSUFFICIENT_SOURCE" of classified: false), zodat "pand bekend maar geen klasse" óók expliciet is en niet uit null-inspectie hoeft te volgen.
-
Misser-registratie (nieuw): leg de 404-missers server-side vast — óf door de tracker ook op de miss-tak te zetten met een reason-kolom, óf via een aparte application.product_tracker_mismatch-tabel. Zonder dit blijft de frequentie per oorzaak onmeetbaar.
Acceptatiecriteria
Open punten / afstemming
NO_BUILDING vs ADDRESS_INVALID: resolveBuildingExternalId(id) geeft nu alleen string | null terug — de reden van een null (ongeldig formaat vs. geldig maar geen pand vs. niet in BAG) is niet zichtbaar. Om deze twee codes te scheiden moet src/geocoder.ts een reden meegeven (bv. een discriminated result i.p.v. null). INSUFFICIENT_SOURCE en UPSTREAM_ERROR zijn wél direct beschikbaar zonder geocoder-wijziging.
- 200 vs 404 bij
INSUFFICIENT_SOURCE: willen we het onderscheid "geen rij" (404) en "rij met null-risico" (200) behouden, of beide onder één reason scharen?
- Eventueel een aparte code voor de grootste subgroep binnen
INSUFFICIENT_SOURCE (niet-onderheid + ontbrekende INSAR/grondwater — o.a. de Waddeneilanden), of gewoon onder INSUFFICIENT_SOURCE laten.
Codeverwijzingen (repo Laixer/FunderMapsWebservice)
- Endpoint + de twee 404-takken:
src/routes/product.ts (/analysis/:id; idem /risk/:id, /light/:id).
- ID-resolutie:
src/geocoder.ts (resolveBuildingExternalId).
- Globale foutafhandeling (500) en 404-fallback:
src/index.ts (app.onError, app.notFound).
- Usage-tracking + 24u-dedup (relevant voor de miss-registratie):
src/tracker.ts (application.product_tracker).
- Overbelasting/quota (429, niet 503):
src/rate-limit.ts.
- Enum-documentatie + sync-discipline:
src/enums.ts / src/enums.test.ts / MIGRATION.md.
Expliciete
reason-code toevoegen aan de analysis-productrespons wanneer geen risicoklasse kan worden geleverdContext
Voor de NWWI-koppeling (funderingsrisicorapport in het taxatieproces) moet de aanroepende partij per opvraag geautomatiseerd kunnen bepalen waarom er geen risicoklasse is, en welke vervolgstap daarbij hoort:
Op dit moment kan de afnemer deze vier situaties niet uit elkaar houden op basis van de respons: elke "geen resultaat"-uitkomst geeft dezelfde 404 met body
{"message": "Not found"}.Huidig gedrag
Endpoint:
GET /v4/product/analysis/:idinsrc/routes/product.ts.De handler kent twee aparte 404-takken die vandaag al bestaan, maar dezelfde body teruggeven:
Daarnaast bestaat een derde "geen klasse"-uitkomst die géén 404 is: als er wél een rij is maar niet geclassificeerd, komt er een 200 OK terug met
foundationType/de*Risk-velden opnull(de rij uitmodel_risk_staticwordt verbatim teruggegeven; die velden zijn nullable).Overige statussen (contract, voor de volledigheid):
{"message": "Internal server error"}— globaleapp.onErrorinsrc/index.ts(echte storing / DB-fout).{"message": "Too Many Requests"}—rateLimit()-middleware bij overschrijding van de billing-/quota-limiet (métRetry-After+X-RateLimit-*). Dit is een quota-limiet, geen storing.Belangrijk (afwijking t.o.v. eerdere aanname): v4 registreert de misser-gevallen op dit moment niet. De usage-tracker (
trackerMiddleware+c.set("tracker", ...)) wordt alleen op de succes-tak gezet; beide 404-takken keren vroeg terug zonder tracking. Er is dus (anders dan in de oude .NET-service) geenproduct_tracker_mismatch-registratie — de frequentie per oorzaak is nu niet meetbaar. Dat moet daarom onderdeel van deze wijziging zijn.Gewenst gedrag
Voeg aan de respons een machine-leesbare
reason-code toe, zodat de afnemer zonder de body te interpreteren de juiste vervolgstap kan kiezen — en registreer de misser + reden server-side zodat de frequentie rapporteerbaar wordt.Voorgestelde enum:
reasonNO_BUILDINGresolveBuildingExternalId→ null)ADDRESS_INVALIDINSUFFICIENT_SOURCErows.length === 0) óf 200 met risicoveldennullUPSTREAM_ERRORonError(500)Waar de code moet landen
Bij de 404/500-fouten: extra veld op de bestaande platte JSON-body, bijv.:
{ "message": "Not found", "reason": "NO_BUILDING" }Concreet in
src/routes/product.ts: geef in tak ①NO_BUILDINGmee en in tak ②INSUFFICIENT_SOURCE; insrc/index.tsonError→UPSTREAM_ERROR. De twee takken maken het onderscheid a/c ↔ b nu al — alleen de body deelt de reden nog niet.Bij een 200 zonder klasse: een veld op de analysis-response (bv.
reason: "INSUFFICIENT_SOURCE"ofclassified: false), zodat "pand bekend maar geen klasse" óók expliciet is en niet uit null-inspectie hoeft te volgen.Misser-registratie (nieuw): leg de 404-missers server-side vast — óf door de tracker ook op de miss-tak te zetten met een
reason-kolom, óf via een aparteapplication.product_tracker_mismatch-tabel. Zonder dit blijft de frequentie per oorzaak onmeetbaar.Acceptatiecriteria
reasonuit de vaste enum — zowel bij 404/500 (in de{"message", "reason"}-body) als bij 200-zonder-klasse (veld op de response).NO_BUILDING,INSUFFICIENT_SOURCE,ADDRESS_INVALIDenUPSTREAM_ERRORzijn onderling te onderscheiden op basis van uitsluitend dereason-waarde.reason), zodat frequentie per oorzaak rapporteerbaar wordt.reason-waarden zijn stabiel en gedocumenteerd inMIGRATION.md(net als de bestaande enum-referenties, met de sync-test-discipline).messagekijken blijven werken (reasonis een extra veld;messageblijft ongewijzigd).reasonwordt consistent toegepast op alle product-endpoints die "not found" kunnen geven (analysis,risk,light, en de nieuwequickscan/foundation-research), niet alleenanalysis.Open punten / afstemming
NO_BUILDINGvsADDRESS_INVALID:resolveBuildingExternalId(id)geeft nu alleenstring | nullterug — de reden van een null (ongeldig formaat vs. geldig maar geen pand vs. niet in BAG) is niet zichtbaar. Om deze twee codes te scheiden moetsrc/geocoder.tseen reden meegeven (bv. een discriminated result i.p.v.null).INSUFFICIENT_SOURCEenUPSTREAM_ERRORzijn wél direct beschikbaar zonder geocoder-wijziging.INSUFFICIENT_SOURCE: willen we het onderscheid "geen rij" (404) en "rij metnull-risico" (200) behouden, of beide onder éénreasonscharen?INSUFFICIENT_SOURCE(niet-onderheid + ontbrekende INSAR/grondwater — o.a. de Waddeneilanden), of gewoon onderINSUFFICIENT_SOURCElaten.Codeverwijzingen (repo
Laixer/FunderMapsWebservice)src/routes/product.ts(/analysis/:id; idem/risk/:id,/light/:id).src/geocoder.ts(resolveBuildingExternalId).src/index.ts(app.onError,app.notFound).src/tracker.ts(application.product_tracker).src/rate-limit.ts.src/enums.ts/src/enums.test.ts/MIGRATION.md.