diff --git a/api/app/Http/Controllers/Api/V1/Portal/EngagementPortalController.php b/api/app/Http/Controllers/Api/V1/Portal/EngagementPortalController.php index 3cd576d3..e5accb70 100644 --- a/api/app/Http/Controllers/Api/V1/Portal/EngagementPortalController.php +++ b/api/app/Http/Controllers/Api/V1/Portal/EngagementPortalController.php @@ -254,23 +254,17 @@ final class EngagementPortalController extends Controller return $existing; } - // Pre-set event_id so the FormSubmissionObserver doesn't fall back - // to the route('event') lookup (this portal route has no {event} - // parameter — the engagement is the source of truth per WS-4). - $submission = $this->submissionService->createDraft( + // Pass event_id via the context bag — the schema is org-owned (not + // event-owned) and this route has no {event} parameter for the + // FormSubmissionObserver fallback. ARCH-FORM-BUILDER §17.3 footnote. + return $this->submissionService->createDraft( schema: $schema, subject: $resolved->subject, submitter: null, context: [ 'idempotency_key' => 'artist_advance:'.$resolved->engagement->id, + 'event_id' => $resolved->eventId, ], ); - - if ($submission->event_id === null) { - $submission->event_id = $resolved->eventId; - $submission->save(); - } - - return $submission->refresh(); } } diff --git a/api/app/Services/FormBuilder/FormSubmissionService.php b/api/app/Services/FormBuilder/FormSubmissionService.php index e86a8859..d3b53f4b 100644 --- a/api/app/Services/FormBuilder/FormSubmissionService.php +++ b/api/app/Services/FormBuilder/FormSubmissionService.php @@ -41,7 +41,13 @@ final class FormSubmissionService ) {} /** - * @param array $context opened_at / public_submitter_* / is_test / idempotency_key + * @param array $context opened_at / public_submitter_* / is_test / idempotency_key / event_id + * + * `event_id` may be supplied for flows where the schema is org-owned (not + * event-owned) and the route has no `{event}` parameter for the + * FormSubmissionObserver fallback to pick up — e.g. the artist-advance + * portal where the engagement is the source of truth per WS-4 + * (ARCH-FORM-BUILDER §17.3 footnote). */ public function createDraft(FormSchema $schema, ?Model $subject, ?User $submitter, array $context = []): FormSubmission { @@ -62,6 +68,9 @@ final class FormSubmissionService $submission->subject_type = $this->morphKeyFor($subject); $submission->subject_id = (string) $subject->getKey(); } + if (isset($context['event_id']) && is_string($context['event_id']) && $context['event_id'] !== '') { + $submission->event_id = $context['event_id']; + } $submission->submitted_by_user_id = $submitter?->id; $submission->status = FormSubmissionStatus::DRAFT->value; $submission->is_test = (bool) ($context['is_test'] ?? false); diff --git a/dev-docs/BACKLOG.md b/dev-docs/BACKLOG.md index e2b0b8c4..0c093d53 100644 --- a/dev-docs/BACKLOG.md +++ b/dev-docs/BACKLOG.md @@ -676,19 +676,6 @@ voor third-party integraties (ticketing, HR, etc.) ## Technische schuld -### ART-OBSERVER-ADVANCE-AGGREGATE — Recompute `advancing_*_count` op AdvanceSection lifecycle - -**Aanleiding:** RFC-TIMETABLE v0.2 Session 1 landed `advancing_completed_count` en -`advancing_total_count` op `artist_engagements` met `default 0`. De observer die -deze tellers bijwerkt op AdvanceSection-lifecycle wijzigingen is nog niet geland. -**Wat:** Implementeer een `AdvanceSectionObserver` die op create/update/delete -(en submission-status transitions) de aggregaat-tellers op de parent-engagement -herberekent. Trigger: section-level submit lands in Session 3. -**Prioriteit:** Middel — alleen relevant zodra de section-lifecycle daadwerkelijk -bestaat. Voor Session 1 is `default 0` voldoende. - ---- - ### RFC-TIMETABLE-V0.2-DOC-CLEANUP — Strip ARCH-PLANNED-MODULES.md mentions from RFC v0.2 **Aanleiding:** RFC-TIMETABLE v0.2 §1 ("Bron-documenten") en §15 @@ -1332,6 +1319,7 @@ deadline implementation). - ~~**WS-7 Observability — closure (mei 2026)**: 4 PRs gemerged op `feat/ws-7-observability` (infra `5f6fc07`, backend SDK `bdb89a2..0379016`, frontend SDK `bc47783..5c42f27`, docs `754222f..e9da01f`). 1551 backend + 252 frontend tests groen. Acceptance criteria 1-14 voldaan; observability volledig operationeel op `monitoring.hausdesign.nl`. Implementation criteria 3, 4, 5, 6, 8, 11, 12, 13, 14 via PRs; operationele criteria 1, 2, 7, 9, 10 via deploy-checklist (DNS, TLS, superuser+2FA, prod DSNs, email-alerting, retention 90d, cron backup). Architecturale patronen vastgelegd in `dev-docs/ARCH-OBSERVABILITY.md` (730 regels) + 2 runbooks (`observability-triage.md`, `observability-erasure.md`). Twee GlitchTip projecten (`crewli-api` + `crewli-app`), één DSN per project, runtime context-split via `actor_scope` tag. Patronen: explicit > implicit listener registration, default-in-listener / override-in-middleware voor binary tags, tenant resolution chain (route-param → portal-token → super_admin platform → user fallback). Volgsporen: OBS-1, OBS-4, OBS-6, OBS-7, OBS-9 (zie "Observability follow-ups" sectie hieronder).~~ ✅ - ~~**WS-6 v1.3-delta — closure (mei 2026)**: Architecturele review-sessie 2026-05-07 identificeerde vijf verfijningen op RFC-WS-6 v1.2 (Q1 listener queueing, Q2 invariant cleanup, Q3 failure-UX additions, plus §19 BACKLOG-pointer). v1.3 amendement gecommit (`845b6e6`, 2026-05-07); v1.3.1 drift closure (`b255879`, 2026-05-08) sloot code-vs-docs gaten pre-implementation. Implementatie geland als D1 + D2: **D1** (PR #10 `c6f4d1b`) leverde de data-laag — `failure_response_code` kolom op `form_submissions`, abstract `FormBindingApplicatorException` hiërarchie + 4 reason-coded subclasses (`FormBindingSchemaConfigException`, `FormBindingInfraException`, `FormBindingApplicatorTimeoutException`, `FormBindingDataIntegrityException`), `IdentityMatchInvariantViolation` sibling, `FormBindingExceptionClassifier` helper, `FormSubmissionIdentityMatchResolved` broadcast event class, `FormFieldBindingMergeStrategy::validForTargetType` matrix method, cast + factory state. **D2** (PR #11 `23a5696`) wired alle building blocks in de listener-chain — `ApplyBindings` initial `pending` write + deadline wrapper + classifier in catch; `TriggerPersonIdentityMatch` queued + gating-invariant + invariant throw + broadcast dispatch; `routes/channels.php` + bootstrap routing (NIEUWE broadcast wiring, submitter-only auth); gating-invariant op `SyncTagPicker`; `AppServiceProvider::boot` v1.3 layout; `FormFailureRetryService::recordFailure` classifier + apply_completed_at symmetrie-fix; `apply_deadline_seconds` config key (default 5). Tests: pre-WS-6 baseline 1208 → pre-D1 1551 → post-D2 1621. 0 Larastan errors. Phase F (`ConditionalRequirement(public_token)` wrapper drop) was no-op — change had silently landed pre-D2. **Open follow-ups:** `TECH-CHANNEL-AUTH-ORG-ADMIN` (extend `submission.{id}` channel auth to org admins na Spatie Permission helper-audit); GlitchTip alert rule op `apply_status=failed AND form_schema.has_public_token=true` (operationele taak in GlitchTip web-UI op `monitoring.hausdesign.nl`; runbook procedure in `dev-docs/runbooks/observability-triage.md` §7); frontend Echo subscription voor `FormSubmissionIdentityMatchResolved` (separate frontend follow-up, out of WS-6 scope, backend-infra ready). `PARTIAL-BINDING-SUCCESS` en `FORM-SCHEMA-DRIFT-DETECTION` blijven open conform v1.3 amendement (trigger-condities nog niet gevuurd). Closure docs-PR: RFC-WS-6.md v1.3.1 implementation-status marker + §10 closure entry, ARCH-BINDINGS.md v1.2 onveranderd, runbook §7 toegevoegd.~~ ✅ - ~~**ARCH-09 — Artist Eloquent model + migration — closure (mei 2026)**: Foundation for the Artist & Timetable module landed as RFC-TIMETABLE v0.2 Session 1 op `feat/timetable-session-1`. Delivered: 10 migrations (genres, artists, companies.handles_buma column, artist_contacts, stages, stage_days, artist_engagements, performances, advance_sections, advance_submissions); 7 PHP enums under `App\Enums\Artist\` (`ArtistEngagementStatus` D9 with Dutch labels, `BumaHandledBy` D26, `FeeType`, `PaymentStatus`, `AdvanceSectionType`, `AdvanceSectionSubmissionStatus`, `AdvanceSubmissionStatus`); 9 Eloquent models with `OrganisationScope` (direct on Artist/Genre/ArtistEngagement, FK-chain via `tenantScopeStrategy()` on the rest) and `LogsActivity` baseline; 2 observers (`ArtistEngagementObserver` for `organisation_id` denorm + cross-tenant guard via `CrossTenantEngagementException` + cascade soft-delete to performances + hard-delete to advance_sections; `PerformanceObserver` for D14 optimistic-lock `version` bump on UPDATE); 8 factories + `ArtistTimetableDevSeeder` reproducing the prototype fixture (4 stages, 12 stage_days, 6 artists, 12 engagements, 13 performances incl. 1 parked); `PURPOSE_SUBJECT_FQCN` switched from string-literal to `Artist::class` (MorphMapAlignmentTest green); SCHEMA.md §3.5.7 rewritten in place (ARCH-PLANNED-MODULES.md was assumed by the RFC pre-amble but did not exist — see `RFC-TIMETABLE-V0.2-DOC-CLEANUP`); ARCH-FORM-BUILDER.md §3.2.5 updated for engagement-scoped sections and §17.3 footnote on `ArtistResolver::fromPortalToken` engagement context resolution. PR #XX, 2026-05-08.~~ ✅ +- ~~**ART-OBSERVER-ADVANCE-AGGREGATE — closure (mei 2026)**: AdvanceSectionObserver implemented in RFC-TIMETABLE v0.2 Session 3 on `feat/timetable-session-3`. Recomputes `artist_engagements.advancing_completed_count` + `advancing_total_count` atomically on every section lifecycle event (created / updated-status-only / deleted). Concurrency safety via `DB::transaction` + `lockForUpdate` on both the parent engagement and sibling section rows; counter writes use `disableLogging()` so housekeeping doesn't pollute the activity log. Section's own `updated` event continues to log via `LogsActivity` on `AdvanceSection`.~~ ✅ - ~~**TECH-CHANNEL-AUTH-ORG-ADMIN — closure (mei 2026)**: `submission.{id}` private channel auth uitgebreid van submitter-only naar drie-paths: submitter (`submitted_by_user_id === user.id`) → super_admin Spatie HasRoles app-wide bypass → org_admin van submission's organisatie via pivot-table check op `user_organisation` (`->wherePivot('role', 'org_admin')`). Pattern: directe port van `FormSubmissionActionFailurePolicy::canAccess`, codebase canonical (gebruikt in 17+ policy sites). Spatie teams is disabled in `config/permission.php`, dus org-scoping leeft in de pivot, niet in Spatie. **super_admin bypass is een audit-surfaced bonus** (origineel BACKLOG entry vroeg alleen om org-admin extension; tijdens Phase A audit bleek dat elke analoge policy super_admin bypass heeft, dus toegevoegd voor consistency — zonder die bypass zouden super_admins op de admin-panel banner mysterieus geen live updates krijgen). Tests: 4 nieuw (`test_super_admin_can_subscribe`, `test_organisation_admin_of_submission_org_can_subscribe`, `test_organisation_admin_of_different_org_cannot_subscribe` (kritische cross-tenant guard), `test_regular_organisation_member_cannot_subscribe`); 1 verwijderd (de "should flip" denied-by-default test uit PR #11). Test count: 1621 → 1624 (+3 net). 0 Larastan errors. Inline TODO uit `routes/channels.php` verwijderd. Sibling `FRONTEND-ECHO-IDENTITY-MATCH-SUBSCRIPTION` blijft open (frontend portal IdentityMatchBanner subscription is de pair met deze backend-auth uitbreiding).~~ ✅ ---