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:
@@ -1,14 +1,15 @@
|
||||
# ARCH — Universal Form Builder (v1.2)
|
||||
# ARCH — Universal Form Builder (v1.3)
|
||||
|
||||
> **Source of truth** for Crewli's universal Form Builder architecture.
|
||||
> Any discrepancy with SCHEMA.md is resolved in favour of this document
|
||||
> during the refactor. SCHEMA.md is updated at the end of the refactor.
|
||||
>
|
||||
> **Status:** Approved — S2b in progress (service + API layer)
|
||||
> **Version:** 1.2.1 (§31.10 FORM-02 contract rewritten authoritatively)
|
||||
> **Previous version:** 1.2 April 2026 — per-purpose lifecycles, integration
|
||||
> contracts, user guidance principles, documentation coverage requirements,
|
||||
> in-app copy catalogue, and concrete gap fills from v1.1 review
|
||||
> **Status:** Approved — S2c landed (public API completion)
|
||||
> **Version:** 1.3 (§10.4 public submission lifecycle — draft/save/submit
|
||||
> split with error envelope and drift detection)
|
||||
> **Previous version:** 1.2.1 April 2026 (§31.10 FORM-02 contract),
|
||||
> 1.2 April 2026 (per-purpose lifecycles, integration contracts, user
|
||||
> guidance principles, documentation coverage, in-app copy catalogue)
|
||||
> **Created:** April 2026
|
||||
> **Owner:** Architecture doc; every session reads this before starting
|
||||
>
|
||||
@@ -1388,6 +1389,77 @@ When anonymised: IP replaced with `"[anonymised]"` and
|
||||
Exception: if submission is flagged for investigation (organiser marks it),
|
||||
anonymisation is paused until flag is cleared.
|
||||
|
||||
### 10.4 [v1.3] Public submission lifecycle — draft / save / submit split
|
||||
|
||||
S2c split the atomic "one-POST does everything" flow into three
|
||||
REST endpoints so the portal can auto-save drafts without firing
|
||||
submit-events on every keystroke:
|
||||
|
||||
```
|
||||
POST /api/v1/public/forms/{public_token}/submissions
|
||||
Body: { idempotency_key (required, 6–30 chars),
|
||||
opened_at?, submitted_in_locale?,
|
||||
public_submitter_name?, public_submitter_email? }
|
||||
Creates a draft. Returns PublicFormSubmissionResource.
|
||||
Duplicate POST with the same idempotency_key returns the existing
|
||||
draft as HTTP 200 (vs 201 for fresh). Race-safe via UNIQUE
|
||||
(form_schema_id, idempotency_key).
|
||||
|
||||
PUT /api/v1/public/forms/{public_token}/submissions/{submission_id}
|
||||
Body: { values: { <slug>: <value>|<array>, ... }, first_interacted_at? }
|
||||
Auto-save — partial updates allowed, only provided slugs are
|
||||
written. Status stays 'draft'. auto_save_count increments per call;
|
||||
FormSubmissionDraftUpdated event fires. Rule layer is relaxed
|
||||
(nullable + type checks); service layer enforces
|
||||
form_fields.validation_rules (min/max/regex/unique).
|
||||
|
||||
POST /api/v1/public/forms/{public_token}/submissions/{submission_id}/submit
|
||||
Body: { values?: {...}, captcha_token? }
|
||||
Final submit. Merges body values with already-saved values, runs
|
||||
STRICT rule set (required, in:options, types, min/max) against the
|
||||
merged map. On success: status draft → submitted, schema_snapshot
|
||||
stored per form_schemas.snapshot_mode, schema_version_at_submit set,
|
||||
fires FormSubmissionSubmitted (→ §31.10 tag sync, §31.1 identity
|
||||
match). Rate-limited per
|
||||
(public_token, ip) per hour via RateLimiter 'form-submit:TOKEN:IP'.
|
||||
```
|
||||
|
||||
**Schema drift detection.**
|
||||
`form_submissions.schema_version_at_open` is stamped at draft-create
|
||||
time; `schema_version_at_submit` is stamped at submit. Any difference
|
||||
(or — for active drafts — a difference against current `schema.version`)
|
||||
surfaces in `PublicFormSubmissionResource.schema_drift: true` so the
|
||||
portal can warn "this form has changed since you started".
|
||||
|
||||
**Access rules.**
|
||||
- Submission must belong to the resolved schema (URL
|
||||
`(public_token, submission_id)` pair must match).
|
||||
- Submission must be `status = draft` on both PUT and POST-submit —
|
||||
409 `SUBMISSION_ALREADY_SUBMITTED` otherwise.
|
||||
- Rotated-token grace window applies the same way as the GET: resolve
|
||||
via `public_token` first, then `public_token_previous` (rejected
|
||||
with 410 `TOKEN_EXPIRED` if past the hard-coded 7-day grace).
|
||||
Making the window configurable is BACKLOG FORM-04.
|
||||
|
||||
**Error envelope (D6).**
|
||||
Every public form endpoint responds with the shared shape:
|
||||
|
||||
```json
|
||||
{ "message": "...", "code": "...", "errors"?: {"values.slug": ["..."]} }
|
||||
```
|
||||
|
||||
Codes: `SCHEMA_NOT_FOUND`, `TOKEN_EXPIRED`, `TOKEN_REVOKED`,
|
||||
`SCHEMA_UNPUBLISHED`, `SUBMISSION_ALREADY_SUBMITTED`, `RATE_LIMITED`
|
||||
(carries `Retry-After` header), `VALIDATION_FAILED`.
|
||||
|
||||
**Dependency-data endpoints.** The public GET `/{public_token}` embeds
|
||||
`available_tags` per TAG_PICKER field. `AVAILABILITY_PICKER` and
|
||||
`SECTION_PRIORITY` use sibling read endpoints:
|
||||
- `GET /{public_token}/time-slots` — VOLUNTEER person_type only,
|
||||
festival-parent query surfaces parent + children time slots.
|
||||
- `GET /{public_token}/sections` — `show_in_registration=true +
|
||||
type=standard`, dedup by name across festival children.
|
||||
|
||||
---
|
||||
|
||||
## 11. Migration plan
|
||||
|
||||
Reference in New Issue
Block a user