docs(ws-6): RFC-WS-6 v1.1 addenda + ARCH-BINDINGS §6.4 alignment
Promote RFC-WS-6 to v1.1 with two §3 addenda capturing the post-session-2
cleanup decisions; align ARCH-BINDINGS.md §6.4 (Person provisioning)
with the v1.1 text. No architectural reversals — corrections + one
schema addition.
§3 Q8 v1.1 addendum — Person provisioning is scoped by `event_id`:
- Q8 v1.0 said `Person::firstOrCreate(['email', 'organisation_id'], ...)`.
That is incorrect against the actual model: `Person::$organisationScopeColumn`
is `event_id`. The provisioner looks up and creates by `(email, event_id)`.
- Same email registering across two events in the same org → two distinct
Person rows. Cross-event identity reconciliation remains the job of
`PersonIdentityService` (out of scope WS-6).
- Failsafe: `PersonProvisioningException('no_event', ...)` when
`submission.event_id` is null on event_registration; publish guard
`SchemaHasLinkedEvent` blocks at config time.
§3 Q9 v1.1 addendum — `form_schemas.default_crowd_type_id` replaces
`CrowdType::oldest()`:
- Session 2's PersonProvisioner used a silent oldest()-in-org heuristic
for the new Person's `crowd_type_id` (NOT NULL). Fragile, undocumented,
cross-org broken.
- v1.1 adds `form_schemas.default_crowd_type_id` (nullable ULID) as the
explicit, versioned schema attribute. `RequiresDefaultCrowdType` publish
guard wires into `EventRegistrationGuards`. Runtime failsafe in
`PersonProvisioner::resolveCrowdTypeId()` throws
`PersonProvisioningException('no_default_crowd_type', ...)` when null.
- Schema-level FK omitted intentionally (SQLite cascade-delete on
ALTER TABLE ADD FOREIGN KEY observed in WS-5b/c backfill tests).
Application-level integrity (publish guard + runtime failsafe +
Eloquent `belongsTo`) is sufficient because writes always go through
`FormSchemaService::publish()`.
- Snapshot impact: none. Provisioning reads from live FormSchema by
FK; audit replay uses whatever the schema's current
`default_crowd_type_id` is at retry time.
ARCH-BINDINGS.md §6.4:
- Now references "RFC Q8 + Q9, v1.1" in the heading.
- Default-crowd-type bullet replaces "first active CrowdType in the org"
(the session-2 oldest() heuristic) with the schema attribute lookup.
- Multi-tenancy paragraph clarified for cross-event scoping.
Cross-references touched up:
- `PersonProvisioner::resolveCrowdTypeId()` docblock: §3 Q8 → §3 Q9.
- `RequiresDefaultCrowdType` class docblock: §3 Q8 → §3 Q9.
- `SCHEMA.md` v2.7 changelog and `default_crowd_type_id` column note:
§3 Q8 → §3 Q9.
Document history entry added in §10 documenting v1.1 + the snapshot
dual-key cleanup and route-model-binding fix landed in earlier commits
on this branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -244,7 +244,7 @@ Merge strategy × null winner matrix (Q7 + V1):
|
||||
catch this at config time, but the runtime check protects against
|
||||
live-table edits between publish and apply.
|
||||
|
||||
### 6.4 Person provisioning (RFC Q8)
|
||||
### 6.4 Person provisioning (RFC Q8 + Q9, v1.1)
|
||||
|
||||
`PersonProvisioner::provisionFromSubmission()` (called from
|
||||
`EventRegistrationSubjectResolver`):
|
||||
@@ -258,8 +258,10 @@ live-table edits between publish and apply.
|
||||
4. `Person::query()->where('email', $value)->where('event_id', $eventId)
|
||||
->lockForUpdate()->first()` → returns existing if found.
|
||||
5. Otherwise builds attributes from the OTHER (non-identity-key)
|
||||
bindings filtered to `Person::$fillable`, sets a default
|
||||
`crowd_type_id` (first active CrowdType in the org), and calls
|
||||
bindings filtered to `Person::$fillable`, resolves
|
||||
`crowd_type_id` from `$submission->schema->default_crowd_type_id`
|
||||
(RFC Q9 v1.1 addendum — replaces the silent `CrowdType::oldest()`
|
||||
heuristic), and calls
|
||||
`Person::firstOrCreate(['email' => ..., 'event_id' => ...], $attrs)`.
|
||||
|
||||
`firstOrCreate` semantics resolve the
|
||||
@@ -271,7 +273,25 @@ deferred to BACKLOG `LOAD-TEST-FOUNDATION`).
|
||||
|
||||
Multi-tenancy: Person's `organisationScopeColumn` is `event_id`
|
||||
(not `organisation_id` directly). The provisioner scopes by
|
||||
`event_id` only — cross-event submissions never collide.
|
||||
`event_id` only — cross-event submissions never collide. Same email
|
||||
registering across two events in the same org → two distinct Person
|
||||
rows; identity reconciliation is `PersonIdentityService`'s job
|
||||
(out of scope for WS-6, RFC §6 / RFC Q8 v1.1 addendum).
|
||||
|
||||
Default crowd type:
|
||||
|
||||
- `form_schemas.default_crowd_type_id` (nullable ULID) is the single
|
||||
source of truth for the freshly-provisioned Person's `crowd_type_id`.
|
||||
- `RequiresDefaultCrowdType` publish guard blocks publish when null on
|
||||
an `event_registration` schema.
|
||||
- `PersonProvisioner::resolveCrowdTypeId()` throws
|
||||
`PersonProvisioningException('no_default_crowd_type', ...)` when
|
||||
null at apply time (failsafe for live-table edits between publish
|
||||
and apply).
|
||||
- No DB-level FK — application-level integrity only (SQLite cascade
|
||||
problem, see RFC Q9 v1.1 addendum). The Eloquent
|
||||
`FormSchema::defaultCrowdType()` `belongsTo` relation handles
|
||||
read-side correctness.
|
||||
|
||||
### 6.5 Per-purpose subject resolution (RFC Q9)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user