ADR-0001's own canonical "config value" example was unenforced: GET
/intake/policy echoed ScholingThreshold, but no request DTO carried a
scholing answer, so the server had nothing to re-validate. A crafted
POST could skip a requirement the wizard presents as mandatory.
IntakePolicy.RejectIncompleteScholing is the authority — three-valued
completeness (below threshold an answer is required; "nee" is legal and
still submits; punten only belong to a followed scholing), living in the
class that owns the constant so scripts/check-seam.sh keeps guarding the
FE/BE literal pair. Both submit paths call it; a violation 400s with
ProblemDetails and leaves the aanvraag a Concept. Gated on
Type == "intake" (the endpoint's switch lumps herregistratie with
intake, which has no scholing question), and guarded by `reject is null`
so a zero-uren submission is still decided on its merits.
Also fixes a live FE bug in the same rule: validateStep required punten
whenever scholingGevolgd was 'ja' regardless of lageUren, while the
template renders those fields only when lageUren — so answering 'ja'
then raising uren either blocked the user on an invisible field or
emitted aanvullendeScholing: undefined alongside punten. punten now
derives from aanvullendeScholing, so that combination is unrepresentable
in ValidIntake.
Note: EndpointTests' Worked_hours_submission_succeeds was itself
asserting the vulnerable payload ({ uren: 40 }, no answer) and needed a
complete answer added; the zero-hours rows are the ordering regression
net and are unmodified.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
124 lines
4.8 KiB
Markdown
124 lines
4.8 KiB
Markdown
# BIG-register BFF (ASP.NET Core)
|
|
|
|
The backend that hosts the **business rules** for the BIG-register portal. The
|
|
frontend renders the decisions this service computes; it does not recompute them
|
|
(BFF-lite + decision DTOs — see `../docs/reference/architecture/0001-bff-lite-decision-dtos.md`).
|
|
|
|
No real BRP/DUO: the reference data they'd return (registration, person, diplomas,
|
|
notes — `Data/SeedData.cs`) is in-memory and seeded, but the endpoints, DTOs,
|
|
status codes and error envelope are production-shaped.
|
|
|
|
**Applications, documents and the brief persist** to a SQLite file
|
|
(`src/BigRegister.Api/bigregister.db`, EF Core-backed — `Data/AppDbContext.cs`,
|
|
`Data/Db.cs`) created and migrated on first run; restarting the process (or
|
|
`docker compose restart api` — the existing `./backend:/src` bind mount already
|
|
covers it, see `docker-compose.yml`) does **not** lose data. Delete the file to
|
|
reset demo data back to empty, the same state a fresh clone starts from. This is
|
|
a deliberate, right-sized choice for a POC (SQLite, no external DB service) — see
|
|
`docs/project/backlog/WP-22-durable-persistence.md`.
|
|
|
|
## Run
|
|
|
|
### Everything (docker-compose, from repo root)
|
|
|
|
```bash
|
|
docker compose up
|
|
```
|
|
|
|
- App: <http://localhost:4200>
|
|
- Swagger UI: <http://localhost:5000/swagger>
|
|
|
|
### Backend only (local)
|
|
|
|
```bash
|
|
cd backend
|
|
dotnet run --project src/BigRegister.Api
|
|
# → http://localhost:5000/swagger
|
|
```
|
|
|
|
### Frontend against a local backend
|
|
|
|
```bash
|
|
npm start # ng serve, proxies /api → http://localhost:5000 (proxy.conf.json)
|
|
```
|
|
|
|
### Tests
|
|
|
|
```bash
|
|
cd backend && dotnet test # rule unit tests + endpoint integration tests
|
|
```
|
|
|
|
## API
|
|
|
|
| Method | Route | Purpose |
|
|
|---|---|---|
|
|
| GET | `/api/dashboard-view` | registration + person + computed herregistratie decision |
|
|
| GET | `/api/notes` | specialisms / aantekeningen |
|
|
| GET | `/api/brp/address` | BRP address lookup (`gevonden:false` = no address) |
|
|
| GET | `/api/duo/diplomas` | diplomas with derived profession + applicable policy questions, + manual fallback |
|
|
| GET | `/api/intake/policy` | scholing threshold (config value) |
|
|
| POST | `/api/registrations` | submit registration → reference, or 422 (manual diploma) |
|
|
| POST | `/api/herregistraties` | submit re-registration → reference, or 422 (0 hours) |
|
|
| POST | `/api/intakes` | submit intake → reference, or 422 (0 hours) / 400 (incomplete scholing answer) |
|
|
|
|
Rejections use **ProblemDetails (RFC 7807)** with status **422**. Every request
|
|
carries an `X-Correlation-Id` (set by the FE fetch adapter); the backend echoes it
|
|
into a no-PII submit-audit log line (`kind`, `outcome`, `reference`, correlation id)
|
|
— the seam for real structured logging / an audit store.
|
|
|
|
### Versioning
|
|
|
|
Endpoints live under **`/api/v1`**. Additive changes (a new optional field) stay on
|
|
v1: the NSwag-generated client and the FE `parse*` boundary ignore unknown fields,
|
|
so old clients keep working. A breaking change (renamed/removed field, changed
|
|
semantics) is introduced as **`/api/v2`** served alongside v1 until clients migrate.
|
|
|
|
## Where the rules live (`src/BigRegister.Api/Domain/`)
|
|
|
|
- `Diplomas/DiplomaRules.cs` — profession derivation + which policy questions apply.
|
|
- `Registrations/HerregistratieRule.cs` — eligibility + reason + status invariant.
|
|
- `Intake/IntakePolicy.cs` — scholing threshold + completeness re-validation on submit
|
|
(`RejectIncompleteScholing`, WP-69).
|
|
- `Submissions/SubmissionRules.cs` — submit rejections + reference generation.
|
|
|
|
## Typed client (NSwag)
|
|
|
|
The frontend calls this API through a generated TypeScript client. Regenerate it
|
|
from the contract after a **shape** change:
|
|
|
|
```bash
|
|
npm run gen:api # builds backend → swagger.json → src/app/shared/infrastructure/api-client.ts
|
|
```
|
|
|
|
## Maintainability: changing a policy is one backend change
|
|
|
|
**Goal:** require every *Verpleegkundige* diploma to confirm a Dutch skills
|
|
assessment. This is a new policy question on a diploma type.
|
|
|
|
Edit **one file** — `Domain/Diplomas/DiplomaRules.cs`:
|
|
|
|
```diff
|
|
public static IReadOnlyList<PolicyQuestion> QuestionsFor(Diploma d)
|
|
{
|
|
var questions = new List<PolicyQuestion>();
|
|
if (d.Engelstalig)
|
|
questions.Add(NlTaalEngelstalig);
|
|
+ if (d.Opleiding == "verpleegkunde")
|
|
+ questions.Add(new PolicyQuestion(
|
|
+ "bekwaamheid",
|
|
+ "Heeft u in de afgelopen vijf jaar een bekwaamheidstoets afgelegd?",
|
|
+ QuestionType.JaNee));
|
|
return questions;
|
|
}
|
|
```
|
|
|
|
Rebuild the backend (`docker compose up` or `dotnet run`). The new question now
|
|
appears in the registration wizard for HBO-Verpleegkunde.
|
|
|
|
- **No frontend change.** The FE renders whatever questions the API returns.
|
|
- **No client regeneration.** The wire shape (`PolicyQuestionDto`) is unchanged —
|
|
only the data behind it. `npm run gen:api` is only needed when a DTO *shape* changes.
|
|
|
|
Add a unit test for the new rule in `tests/BigRegister.Tests/RuleTests.cs` and
|
|
you're done.
|