docs(form-builder): API.md Form Builder (Public), SCHEMA v2.1, ARCH §10.4, BACKLOG
S2c Phase 8.
- API.md: new **Form Builder (Public)** section documenting all 6
public endpoints (GET schema + time-slots + sections; POST draft,
PUT save, POST submit) with request/response examples, error codes,
and the identity_match / schema_drift contracts. No PII-echo noted
explicitly.
- SCHEMA.md bumped to v2.1:
- changelog entry for S2c.
- form_submissions table gains schema_version_at_open +
identity_match_status columns; UNIQUE (form_schema_id,
idempotency_key) replaces the composite index; a new composite
index (form_schema_id, identity_match_status) landed for the
organiser "pending-match" dashboard.
- ARCH-FORM-BUILDER.md bumped to v1.3 with new §10.4 "Public
submission lifecycle — draft/save/submit split" documenting the
three-endpoint contract, idempotency, schema-drift detection,
access rules, the standardised error envelope, and the dependency
data sub-endpoints.
- BACKLOG.md adds:
- FORM-04 (grace_days configurable — current implementation still
uses the hard-coded 7-day window)
- DOC-01 (Scramble / OpenAPI generator for API.md to reduce the
docs-drift effort going forward).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
213
dev-docs/API.md
213
dev-docs/API.md
@@ -776,6 +776,219 @@ fields never leak to non-admins.
|
||||
`TAG_PICKER` fields and resolves `label` / `help_text` / `options`
|
||||
through `FormLocaleResolver` + the `translations` JSON column.
|
||||
|
||||
## Form Builder (Public)
|
||||
|
||||
S2c contract for the unauthenticated portal. Six endpoints at
|
||||
`/api/v1/public/forms/*`, every one rate-limited via the
|
||||
`throttle:30,1` middleware and served behind
|
||||
`PublicFormTokenResolver` (7-day grace window on
|
||||
`public_token_previous`; BACKLOG FORM-04 makes this configurable).
|
||||
|
||||
All errors use the standardised envelope:
|
||||
|
||||
```json
|
||||
{ "message": "Human text", "code": "MACHINE_CODE", "errors"?: {"values.slug": ["..."]} }
|
||||
```
|
||||
|
||||
Codes: `SCHEMA_NOT_FOUND` (404), `TOKEN_EXPIRED` (410),
|
||||
`TOKEN_REVOKED` (410), `SCHEMA_UNPUBLISHED` (410),
|
||||
`SUBMISSION_ALREADY_SUBMITTED` (409), `RATE_LIMITED` (429, carries
|
||||
`Retry-After`), `VALIDATION_FAILED` (422).
|
||||
|
||||
### `GET /public/forms/{public_token}`
|
||||
|
||||
Returns `PublicFormSchemaResource`. Shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "01HZ...", "name": "...", "slug": "...",
|
||||
"purpose": "event_registration",
|
||||
"locale": "nl",
|
||||
"version": 3,
|
||||
"opened_at": "2026-04-17T20:29:16+00:00",
|
||||
"consent_version": null,
|
||||
"submission_deadline": null,
|
||||
"section_level_submit": false,
|
||||
"sections": [],
|
||||
"fields": [
|
||||
{
|
||||
"id": "01HZ...",
|
||||
"slug": "shirtmaat",
|
||||
"field_type": "SELECT",
|
||||
"label": "Shirtmaat",
|
||||
"help_text": null,
|
||||
"options": ["XS","S","M","L","XL","XXL"],
|
||||
"available_tags": null,
|
||||
"validation_rules": null,
|
||||
"is_required": true,
|
||||
"display_width": "half",
|
||||
"conditional_logic": null,
|
||||
"sort_order": 2,
|
||||
"form_schema_section_id": null
|
||||
},
|
||||
{
|
||||
"slug": "vaardigheden",
|
||||
"field_type": "TAG_PICKER",
|
||||
"available_tags": [
|
||||
{"id": "01HZ...", "name": "EHBO", "category": "Veiligheid"},
|
||||
{"id": "01HZ...", "name": "Tapper", "category": "Horeca"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `available_tags` is populated on `TAG_PICKER` fields only. Filter
|
||||
via `form_fields.validation_rules.tag_categories` when set, else
|
||||
returns every active `person_tag` for the org.
|
||||
- `conditional_logic` references peers by `field_slug`.
|
||||
- Admin-only / non-portal-visible fields are filtered out entirely.
|
||||
|
||||
### `GET /public/forms/{public_token}/time-slots`
|
||||
|
||||
`AVAILABILITY_PICKER` dependency data. Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "01HZ...",
|
||||
"name": "Zaterdag ochtend",
|
||||
"date": "2026-07-11",
|
||||
"start_time": "08:00:00",
|
||||
"end_time": "13:00:00",
|
||||
"duration_hours": 5.0,
|
||||
"event_id": "01HZ...",
|
||||
"event_name": "Echt Feesten 2026 — Dag 2"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- Volunteer-only: `person_type = 'VOLUNTEER'`.
|
||||
- Festival-aware: parent + children surfaced; deduplication is by id.
|
||||
|
||||
### `GET /public/forms/{public_token}/sections`
|
||||
|
||||
`SECTION_PRIORITY` dependency data. Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "01HZ...",
|
||||
"name": "Bar",
|
||||
"category": "Horeca",
|
||||
"icon": "tabler-beer",
|
||||
"registration_description": "Tappen en serveren"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `show_in_registration = true AND type = 'standard'`.
|
||||
- Festival-aware: children surfaced, deduplicated by name.
|
||||
|
||||
### `POST /public/forms/{public_token}/submissions` — create draft
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{
|
||||
"idempotency_key": "01HZ...",
|
||||
"opened_at": "2026-04-17T20:20:00+00:00",
|
||||
"submitted_in_locale": "nl",
|
||||
"public_submitter_name": "Anonieme tester",
|
||||
"public_submitter_email": "test@example.nl"
|
||||
}
|
||||
```
|
||||
|
||||
- `idempotency_key` is REQUIRED (6–30 chars). Duplicate replay
|
||||
returns the existing draft with `HTTP 200` instead of `201`.
|
||||
- Response is a `PublicFormSubmissionResource` with
|
||||
`status: "draft"` and `schema_version_at_open` stamped.
|
||||
|
||||
### `PUT /public/forms/{public_token}/submissions/{submission_id}` — auto-save
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{
|
||||
"values": {"shirtmaat": "L", "vaardigheden": ["01HZ...", "01HZ..."]},
|
||||
"first_interacted_at": "2026-04-17T20:21:05+00:00"
|
||||
}
|
||||
```
|
||||
|
||||
- Partial updates allowed. Only slugs present in the body are
|
||||
written; unrelated saved values stay intact.
|
||||
- Relaxed rule set at the request layer (nullable + type check). The
|
||||
service layer still enforces `validation_rules.min/max/regex/unique`.
|
||||
- Every PUT increments `auto_save_count` and fires
|
||||
`FormSubmissionDraftUpdated` on the domain event bus.
|
||||
- 409 if the submission is not status=draft. 404 if
|
||||
`submission_id` belongs to a different schema.
|
||||
|
||||
### `POST /public/forms/{public_token}/submissions/{submission_id}/submit` — finalize
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{
|
||||
"values": {"opmerkingen": "final remark"},
|
||||
"captcha_token": "..."
|
||||
}
|
||||
```
|
||||
|
||||
- Merges the body with already-saved values and runs the strict rule
|
||||
set against the merged map. Required fields must be present
|
||||
somewhere (saved or in the body) or the server returns
|
||||
`VALIDATION_FAILED`.
|
||||
- Fires `FormSubmissionSubmitted` — triggers the §31.10 TAG_PICKER
|
||||
sync and §31.1 identity-match listeners.
|
||||
- Rate-limited per `(public_token, ip)` per hour. Exceed returns
|
||||
`RATE_LIMITED` with `Retry-After` header.
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "01HZ...",
|
||||
"form_schema_id": "01HZ...",
|
||||
"status": "submitted",
|
||||
"auto_save_count": 4,
|
||||
"submitted_in_locale": "nl",
|
||||
"schema_version_at_submit": 3,
|
||||
"schema_drift": false,
|
||||
"values": { "shirtmaat": {"value": "L", "value_anonymised": false} },
|
||||
"identity_match": {
|
||||
"status": "pending",
|
||||
"message": "We controleren of je al bekend bent bij de organisator. Je gegevens worden gekoppeld zodra zij dit bevestigen."
|
||||
},
|
||||
"opened_at": "...",
|
||||
"first_interacted_at": "...",
|
||||
"submitted_at": "...",
|
||||
"submission_duration_seconds": 120,
|
||||
"created_at": "...",
|
||||
"updated_at": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `schema_drift` is true when `schema_version_at_open !=
|
||||
schema_version_at_submit` (organiser edited the schema during the
|
||||
draft).
|
||||
- `identity_match.status` is one of `null | pending | matched | none`
|
||||
per ARCH §31.1.
|
||||
- **No PII echo.** `public_submitter_name`, `public_submitter_email`,
|
||||
`public_submitter_ip`, and `submitted_by_user_id` are never
|
||||
included in the response.
|
||||
|
||||
## Person List Filtering (extended)
|
||||
|
||||
Additional filter parameters on `GET /organisations/{org}/events/{event}/persons`:
|
||||
|
||||
Reference in New Issue
Block a user