docs(form-builder): API.md, ARCH §31.10, BACKLOG
Phase 7 of S2b. - API.md: "Form Builder" section rewritten with every new route (schemas / fields / submissions / values / delegations / templates / field library / webhooks / filter registry / public token flow). Calls out §22.8 typed-confirmation deletes, §6.5 binding-change guard, §9 signature hash on submit, §7.4–§7.5 FilterQueryBuilder contract, and that FormSubmissionSubmitted is the trigger for the §31.10 TAG_PICKER sync listener. - BACKLOG.md: FORM-02 marked done with the shipped artefacts and the deferred §31.9 contract tests spelled out. - ARCH-FORM-BUILDER.md §31.10 already rewrote authoritatively in Phase 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
128
dev-docs/API.md
128
dev-docs/API.md
@@ -653,12 +653,128 @@ Response: `{ "confirmed": 2, "errors": [{ "match_id": "ulid3", "error": "User al
|
||||
|
||||
## Form Builder
|
||||
|
||||
The legacy registration-form-fields / person-field-values /
|
||||
registration-field-templates / person-section-preferences endpoints were
|
||||
purged in S2a. Their replacements (new Form Builder CRUD, public form
|
||||
submission endpoints, tag-sync listener) land in S2b+. This section will
|
||||
be filled in then — see `/dev-docs/ARCH-FORM-BUILDER.md` §4 for the
|
||||
target schema and §31 for integration contracts.
|
||||
Universal form builder per `/dev-docs/ARCH-FORM-BUILDER.md`. Replaces the
|
||||
legacy registration-form-fields / person-field-values /
|
||||
registration-field-templates / person-section-preferences endpoints
|
||||
purged in S2a. All authenticated routes are namespaced under
|
||||
`/organisations/{organisation}/forms/*` with `auth:sanctum` + FormBuilder
|
||||
policies. Public routes live at `/public/forms/*`.
|
||||
|
||||
### Authenticated — Form Schemas
|
||||
|
||||
- `GET /organisations/{organisation}/forms/schemas` — paginated list
|
||||
(default 25). Returns `FormSchemaSummaryResource` items.
|
||||
- `POST /organisations/{organisation}/forms/schemas` — body:
|
||||
`{ name, purpose, submission_mode?, locale?, snapshot_mode?,
|
||||
freeze_on_submit?, retention_days?, consent_version?, ... }`. Returns
|
||||
`FormSchemaResource`.
|
||||
- `GET /organisations/{organisation}/forms/schemas/{form_schema}` — full
|
||||
resource with filtered `fields`.
|
||||
- `PUT /organisations/{organisation}/forms/schemas/{form_schema}` —
|
||||
updates fields; structural changes bump `version`.
|
||||
- `DELETE /organisations/{organisation}/forms/schemas/{form_schema}` —
|
||||
soft delete. If submissions exist, requires
|
||||
`?confirmed_name=<schema.name>` per §22.8 (422 without).
|
||||
- `POST /organisations/{organisation}/forms/schemas/{form_schema}/duplicate`
|
||||
- `POST /organisations/{organisation}/forms/schemas/{form_schema}/publish`
|
||||
- `POST /organisations/{organisation}/forms/schemas/{form_schema}/unpublish`
|
||||
- `POST /organisations/{organisation}/forms/schemas/{form_schema}/rotate-public-token`
|
||||
— body: `{ grace_days?: int (default 7) }`. Moves current
|
||||
`public_token` to `public_token_previous`; old token returns 410 after
|
||||
grace window.
|
||||
- `POST /organisations/{organisation}/forms/schemas/{form_schema}/edit-lock`
|
||||
— 409 if another user holds a valid lock.
|
||||
- `DELETE /organisations/{organisation}/forms/schemas/{form_schema}/edit-lock`
|
||||
|
||||
### Authenticated — Form Fields (within a schema)
|
||||
|
||||
- `GET /organisations/{organisation}/forms/schemas/{form_schema}/fields`
|
||||
- `POST /organisations/{organisation}/forms/schemas/{form_schema}/fields`
|
||||
— body validates `field_type` against `FormFieldType` enum + any
|
||||
registered `custom_field_types`.
|
||||
- `PUT /organisations/{organisation}/forms/schemas/{form_schema}/fields/{form_field}`
|
||||
— setting `force_binding_change=true` bypasses the §6.5 guard.
|
||||
- `DELETE /organisations/{organisation}/forms/schemas/{form_schema}/fields/{form_field}`
|
||||
— requires `?confirmed_name=<field.label>` when the field has values.
|
||||
- `POST /organisations/{organisation}/forms/schemas/{form_schema}/fields/reorder`
|
||||
— body: `{ field_ids: [<ulid>, ...] }`.
|
||||
- `POST /organisations/{organisation}/forms/schemas/{form_schema}/fields/insert-from-library`
|
||||
— body: `{ library_field_id, overrides? }`.
|
||||
|
||||
### Authenticated — Form Submissions
|
||||
|
||||
- `GET /organisations/{organisation}/forms/schemas/{form_schema}/submissions`
|
||||
- `POST /organisations/{organisation}/forms/schemas/{form_schema}/submissions`
|
||||
— creates a draft. Body:
|
||||
`{ subject_type?, subject_id?, is_test?, opened_at?, idempotency_key? }`.
|
||||
- `GET /organisations/{organisation}/forms/submissions/{form_submission}`
|
||||
- `PUT /organisations/{organisation}/forms/submissions/{form_submission}/field-values`
|
||||
— bulk upsert draft values. Body: `{ values: { <slug>: <value_or_array> } }`.
|
||||
403 when `FieldAccessService::canWrite` rejects a slug.
|
||||
- `POST /organisations/{organisation}/forms/submissions/{form_submission}/submit`
|
||||
— optional `values` accepted in-place. On submit: stores
|
||||
`schema_version_at_submit`; when `schema.snapshot_mode != 'never'`
|
||||
stores `schema_snapshot`; computes SIGNATURE hashes per §9; fires
|
||||
`FormSubmissionSubmitted` — **triggering the §31.10 TAG_PICKER sync
|
||||
listener**.
|
||||
- `POST /organisations/{organisation}/forms/submissions/{form_submission}/review`
|
||||
— body: `{ status: FormSubmissionReviewStatus, review_notes? }`.
|
||||
- `POST /organisations/{organisation}/forms/submissions/{form_submission}/delegate`
|
||||
— body: `{ delegated_to_user_id (scoped to org), message? }`.
|
||||
- `DELETE /organisations/{organisation}/forms/submissions/{form_submission}/delegations/{delegation}`
|
||||
- `DELETE /organisations/{organisation}/forms/submissions/{form_submission}`
|
||||
|
||||
### Authenticated — Templates, Field Library, Webhooks
|
||||
|
||||
- `GET/POST/PUT/DELETE /organisations/{organisation}/forms/templates[/{form_template}]`
|
||||
— system templates are read-only for non-super-admins.
|
||||
- `GET/POST/PUT/DELETE /organisations/{organisation}/forms/field-library[/{field_library}]`
|
||||
- `GET/POST/PUT/DELETE /organisations/{organisation}/forms/schemas/{form_schema}/webhooks[/{webhook}]`
|
||||
— responses return `url_host` + `has_secret`; the raw URL and secret
|
||||
never leak out.
|
||||
|
||||
### Authenticated — Filter Registry
|
||||
|
||||
- `GET /organisations/{organisation}/forms/filter-registry?event_id=<ulid?>`
|
||||
— combines entity_column definitions (`config/form_filter_registry.php`)
|
||||
with TAG_PICKER-backed tags and every `is_filterable=true`
|
||||
`form_field`. Response items carry a `source` discriminator of
|
||||
`entity_column` / `tags` / `form_field`. Cached per
|
||||
`(organisation_id, event_id?)`; used by the Personen module through
|
||||
`FilterQueryBuilder` (ARCH §7.4–§7.5). The builder rejects filters
|
||||
referencing invisible fields with 403 (tied to `FieldAccessService`).
|
||||
|
||||
### Public (no auth, rate-limited)
|
||||
|
||||
- `GET /public/forms/{public_token}` — returns
|
||||
`PublicFormSchemaResource` (portal-visible, non-admin-only fields
|
||||
only; no PII hints; no submissions_count; no role_restrictions bleed).
|
||||
`public_token` is matched against `form_schemas.public_token` first
|
||||
and `public_token_previous` second; if the rotated token has exceeded
|
||||
the 7-day grace window the response is 410 Gone.
|
||||
- `POST /public/forms/{public_token}/submissions` — body:
|
||||
`{ values, public_submitter_name?, public_submitter_email?,
|
||||
captcha_token?, idempotency_key? }`. Captcha (Cloudflare Turnstile) is
|
||||
enforced for purposes listed under
|
||||
`config('form_builder.captcha.required_for_purposes')`. Rate-limited
|
||||
per-IP per-token per-hour
|
||||
(`form_builder.limits.max_submissions_per_public_schema_per_ip_per_hour`).
|
||||
Private-IP webhook targets are rejected SSRF-style in
|
||||
`DeliverFormWebhookJob`.
|
||||
|
||||
### Response shapes
|
||||
|
||||
`FormSchemaResource` includes `fields_count`, `submissions_count`,
|
||||
`has_submissions`, `is_locked`, `public_form_url` (when `public_token`
|
||||
is set), and a filtered `fields` collection.
|
||||
|
||||
`FormSubmissionResource.values` is keyed by field slug and already
|
||||
filtered through `FieldAccessService::filterVisibleFields` so admin-only
|
||||
fields never leak to non-admins.
|
||||
|
||||
`FormFieldResource` carries `available_tags` (category-filtered) for
|
||||
`TAG_PICKER` fields and resolves `label` / `help_text` / `options`
|
||||
through `FormLocaleResolver` + the `translations` JSON column.
|
||||
|
||||
## Person List Filtering (extended)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user