Commit Graph

10 Commits

Author SHA1 Message Date
895a1690e7 feat(timetable): ArtistAdvanceDefault seeder + bootstrap
Seeds 5 default sections per RFC v0.2 D15 (General Info, Contacts,
Production, Technical Rider, Hospitality) on a per-organisation
artist_advance FormSchema with section_level_submit=true. Each
section ships with 3-4 illustrative form_fields; organisations
customise via the FormBuilder UI later.

Wired into org-creation via the new OrganisationObserver so new
tenants receive the schema automatically. Existing orgs get
coverage via the new artist:seed-advance-default artisan command
(idempotent — orgs that already own a schema are skipped).

Note: introduces a new production-grade default-seeder convention.
Prior FormBuilder defaults were dev-only via FormBuilderDevSeeder
called from DevSeeder::run(). This is the first non-dev path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:16:25 +02:00
609280d061 feat(timetable): DemoteExpiredOptions scheduled command
`artist:demote-expired-options` artisan command finds every
ArtistEngagement still in Option whose option_expires_at has passed,
transitions it back to Draft via the existing state-machine
(transitionStatus), and writes an `option_expired` activity entry
with the original expiry timestamp captured in properties so the
audit log distinguishes system-driven expiries from manual demotions.

Idempotency: the state-machine bails when the engagement is no longer
in Option, so a second run within the same minute is a no-op for any
given row. The auto-logged `updated` row + the explicit
`status_changed` + the `option_expired` entries are emitted only by
the run that actually performs the transition.

Scheduled in routes/console.php daily at 03:00 Europe/Amsterdam,
matching the existing nightly low-traffic window.

Notification (email project leader on demotion) is deferred to the
notification framework that lands post-Accreditation; tracked under
BACKLOG entry ART-DEMOTE-NOTIFICATION.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:59:39 +02:00
b47e096a55 feat(form-builder): retry history table + integration (WS-6)
Per-attempt retry history (timestamp, user, outcome, exception detail
if failed) replaces the counter-only retry_count tracking.

Changes:

- New `form_submission_action_failure_retry_attempts` table (cascade on
  parent delete, nullOnDelete on user). Explicit short FK names
  (`fsafra_failure_fk`, `fsafra_user_fk`) — auto-generated names exceed
  MySQL's 64-char identifier limit.
- New FormSubmissionActionFailureRetryAttempt model + factory +
  succeeded() state.
- Parent FormSubmissionActionFailure gets retryAttempts() HasMany
  relation (latest('attempted_at')).
- New FormFailureRetryService centralises the retry-flow logic. Both
  the API controller and the artisan command delegate to it. Service
  writes a retry_attempt record per attempt; parent's retry_count
  stays as denormalised cache for index-view performance.
- Successful retry: attempt(succeeded) + parent.retry_count++ +
  parent.resolved_at + parent.resolved_by_user_id + parent.resolved_note
  ("Geslaagde retry door {actor.name}" or "Geslaagde retry
  (geautomatiseerd)" for command-line invocation without an actor).
- Failed retry: attempt(failed) with NEW exception details +
  parent.retry_count++. Parent's exception_class/_message stay
  audit-immutable — they represent the FIRST failure.
- canBeRetried() now correctly checks both resolved_at AND
  dismissed_at (sessie 2's open question Q2 closure).
- New FailureNotRetriableException (controller → 422) and
  ParentSubmissionGoneException (controller → 410) for cleaner
  flow control.

12 new tests:
- FormSubmissionActionFailureRetryAttemptTest (5 unit tests)
- RetryFlowProducesRetryAttemptsTest (7 integration tests covering
  succeeded path, failed path, resolved/dismissed blocking,
  multiple-retries chronological ordering, canBeRetried truth tables)

Pre-existing tests touched:
- FormSubmissionActionFailureTest::test_can_be_retried_only_for_open_state
  — updated to reflect Q2 closure (resolved now blocks too).
- Ws6FoundationMigrationTest::test_down_methods_clean_up_columns_and_table
  — child table must drop before parent (FK constraint).
- 5 backfill test step-counts bumped +1 (new migration sits at top).

SCHEMA.md → v2.9. Schema dump regenerated.

Refs: RFC-WS-6.md §3 Q5 addendum, sessie 2 Q2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:14:19 +02:00
84d57c5bbc feat(form-builder): retry/resolve/dismiss artisan commands (WS-6)
Three CLI commands for ops use, mirroring the API endpoints in Task 9:
  - form-failures:retry   (id|submission|org filter, --dry-run)
  - form-failures:resolve (single id, optional note)
  - form-failures:dismiss (single id, DismissalReasonType + note)

Cross-tenant isolation enforced via form_submissions.organisation_id
FK chain (RFC V3). retry_count incremented on retry; failure history
preserved (new row on repeat failure, not in-place mutation).

Refs: RFC-WS-6.md §3 (Q5), §4 (V2, V3)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 14:25:16 +02:00
e17fc7c2f4 chore: remove dead legacy form-data migration commands
MigrateLegacyFormsData and VerifyFormsDataIntegrity exist to migrate the
pre-form-builder registration_form_fields / registration_field_templates
/ person_field_values tables into the current form_* tables. Those
legacy tables have been dropped from the dev database (verified via the
2026_04_20 drop_remaining_legacy_registration_tables migration), which
means the migrator's top-of-handle() guard always short-circuits the
run. The verify command is only reachable via
MigrateLegacyFormsData::verify() — also dead with its caller gone.

CLAUDE.md delete > adapt. These commands would also break the WS-5d
commit 5 column drop: MigrateLegacyFormsData:225 writes \$rff->options
straight to form_fields.options, which will not exist after commit 5.
Cleaning up before WS-5d starts keeps the dev tree consistent throughout
the refactor.

The two stale comment references in FormBuilderDevSeeder (header docblock
and seedSubmissionsForEvent docblock) plus the migration docblock that
mentions the migrator self-skip behaviour are scrubbed in the same
commit so no orphan references remain.

No production data exists; no migration safety net is being removed.

Tests: 1158 → 1158 green (no test coverage existed for these commands;
they were truly orphaned).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 01:58:19 +02:00
61719bf8bf refactor(form-builder): pre-publish check reads form_field_bindings; drop binding JSON columns 2026-04-24 20:09:27 +02:00
b9343f6eec refactor(form-builder): drop custom purpose escape from schemas
Reduces the FormPurpose vocabulary from 22 variants + a `custom` escape
to the seven v1.0 purposes registered in the new PurposeRegistry.

- Purge migration deletes any form_schemas row whose `purpose` is not
  in the v1.0 set (cascades through form_fields, form_submissions,
  form_values, form_value_options, form_schema_sections,
  form_submission_section_statuses, form_submission_delegations,
  form_schema_webhooks, form_webhook_deliveries via existing FK).
- Drop migration removes the `custom_purpose_slug` column + its index.
- Both migrations declare their `down()` as a hard failure — we do not
  support reversing a purge (pre-launch, no production data).
- `FormPurpose` enum slims to the seven cases; the legacy helpers
  (defaultSubmissionMode / defaultSubjectType / allowsPublicAccess)
  now delegate to PurposeRegistry so callers keep working.
- FormSchema fillable / FormSchemaResource / StoreFormSchemaRequest /
  UpdateFormSchemaRequest / FormSchemaFactory drop every reference to
  `custom_purpose_slug` and the `custom` purpose.
- VerifyFormsDataIntegrity drops the custom-slug mismatch check and
  sources the subject-type allow-list from PurposeRegistry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:35:37 +02:00
ccdfd5b77b fix(forms): gate value_indexed population on is_filterable
FormValueObserver: value_indexed is filter-driven per ARCH §4.4, not
hint-driven. Populating it for every string-hint field produced dead
weight in the partial index and made FilterQueryBuilder logic murkier.

Behaviour after fix:
  hint=string,  is_filterable=true  → populate value_indexed
  hint=string,  is_filterable=false → leave null
  hint=number/date/bool, any filterable → populate typed column (unchanged)
  hint=json, any filterable → leave typed columns null (unchanged)

value_number / value_date / value_bool remain hint-driven — they serve
display and sorting beyond filtering. Only value_indexed is gated.

VerifyFormsDataIntegrity: "value_indexed set on non-filterable field"
is now a FAIL (was WARN) — it means the observer didn't run correctly,
which is a real integrity issue.

Observer tests: split the old "string hint populates value_indexed"
case into filterable/non-filterable pair. Full suite 911/911.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:28:15 +02:00
72892d38f4 feat(forms): add data migration and verification commands
forms:migrate-legacy-data {--dry-run} {--verify-only}
  Per-org transaction (outer loop); inside each org, one form_schema per
  distinct event_id in registration_form_fields, one form_field per legacy
  field (with lowercase→uppercase field_type mapping and PII heuristic),
  one form_submission per distinct person_field_values author, one form_value
  per legacy row. form_templates derive schema_snapshot in ARCH §4.6.1 shape.
  Idempotent via existence checks; skips if registration_form_fields absent.
  Wrapped in App\Support\ActivityLog::suppressed() so --dry-run and re-runs
  don't storm the activity log.

forms:verify-data-integrity {--strict}
  Nine coherence checks: schemas/fields/submissions/values/user_profiles
  structure, data migration counts (skipped when legacy tables absent),
  orphans, section/schema relation consistency, and strict reachability
  (opt-in). Runs all checks to completion; exit 1 on any failure.
  Validates binding JSON against config/form_binding.php registry and
  field_type against FormFieldType::values() ∪ custom_field_types config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 13:18:42 +02:00
9acb27af3a feat: fase 2 backend — crowd types, persons, sections, shifts, invite flow
- Crowd Types + Persons CRUD (73 tests)
- Festival Sections + Time Slots + Shifts CRUD met assign/claim flow (84 tests)
- Invite Flow + Member Management met InvitationService (109 tests)
- Schema v1.6 migraties volledig uitgevoerd
- DevSeeder bijgewerkt met crowd types voor testorganisatie
2026-04-08 01:34:46 +02:00