Three byte-identical copies of `normaliseLegacyGroupShape` lived in
FormFieldService, StoreFormFieldRequest, and UpdateFormFieldRequest.
WS-5d (form_fields.options) would have been the fourth copy. Hoist
the helper to a single public static on FormFieldConditionalLogicService
and have all three call sites delegate.
Implementation:
- `FormFieldConditionalLogicService::normaliseLegacyShape(array)` —
pure recursive passthrough. Translates the ARCH §8 JSON group shape
(`{"all": [...]}` / `{"any": [...]}`) into the service's internal
`{"operator", "children"}` form. Does NOT validate; malformed shapes
return as-is and surface downstream as
`InvalidConditionalLogicSpecException` from `assertSpecsValid`.
- Group operator catalogue sourced from
`FormFieldConditionalLogicGroupOperator::values()` instead of an
`['all', 'any']` literal — single source of truth for future
operator additions.
- All three call sites switched to the static method. The two
FormRequests reach it via the existing `use` import; FormFieldService
sits in the same namespace.
Behaviour preserved exactly:
- Existing FormFieldApiTest (cyclic logic rejection),
FormFieldStrictConditionalLogicRequestTest (strict-validator
rejection paths), and FormFieldConditionalLogicServiceTest
(service-level paths) all green without modification.
New unit tests pin the passthrough contract (8 tests):
- Valid ALL / ANY translations
- Recursive nested-group translation (depth 2)
- Internal shape unchanged
- Condition leaf passthrough
- Unknown group key (`xor`) returned unchanged for downstream
`assertSpecsValid` to reject
- Empty array unchanged
- Non-array children stripped silently
Tests: 1150 → 1158 green (3110 → 3124 assertions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WS-5c commit 1 of 4 — relational infrastructure for the conditional-
logic tree that replaces form_fields.conditional_logic JSON (ARCH-
FORM-BUILDER §8; addendum Q3 WS-5c).
Tables: groups (nesting via parent_group_id) + conditions (leaves,
value JSON nullable for empty/not_empty). Simple FK to form_fields —
addendum Q3 explicitly excludes form_field_library from conditional_
logic scope, so no polymorphic morph here.
OrganisationScope cap raised 3 → 5 hops. The conditions chain is
4 hops (condition → group → field → schema → organisation_id column)
and the new cap gives headroom for future deeper trees without
denormalising form_field_id onto conditions.
Cascade observer (FormFieldChildTablesCascadeObserver) extended to
physically delete the new groups table on FormField delete (hard or
soft). Conditions cascade automatically via the group_id FK on the
groups table.
Factories: FormFieldConditionalLogicGroupFactory, FormFieldConditional
LogicConditionFactory, and FormFieldFactory::withConditionalLogic($tree)
for concise test fixtures.
Tests: 16 new under tests/Feature/FormBuilder/ConditionalLogic/
(relation, scope, cascade, enum catalogue). 3 new scope-cap tests in
ScopeLeakageTest verify 4/5-hop chains pass and 6-hop throws. Hardcoded
rollback step counts in WS-5a/b migration tests bumped for the 2 new
WS-5c migrations. Baseline 1104 → 1122 green (2988 → 3032 assertions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WS-5a commit 1 of 4 per ARCH-CONSOLIDATION-ADDENDUM-2026-04-24 Q3.
Creates the relational home for what was form_fields.binding JSON and
form_field_library.default_binding JSON. Owner discriminator is polymorphic
morph (owner_type/owner_id) — the pattern the rest of WS-5 (5b validation_rules,
5d options) will reuse.
Migration backfills rows from both JSON sources in a single transaction and
is genuinely reversible (rollback reconstructs the JSON). Old columns remain
in place until commit 3 has switched all readers.
Pattern B (binding=null) is represented by absence of row. mode enum covers
entity_owned / mirrored only.
Cascade on owner delete via observer — bindings are physical state, not
historical audit. FormFieldBindingScope enforces multi-tenancy via UNION over
both owner chains (form_field → schema → org OR form_field_library → org) —
Q2's declarative tenantScopeStrategy() can't walk morph parents.
Tests: migration forward/back, morph relation, cascade observer, scope
isolation, enum coverage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
9 backed string enums covering purpose, field type, submission status/mode/review,
field width, value storage hint, snapshot mode, webhook delivery status.
FormPurpose/FormFieldType include helper methods per ARCH §3/§5. All with
declare(strict_types=1) and values() helpers for validation rules.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the per-field `section` text property with a dedicated HEADING field type that
organizers add as a separate block for visual grouping. Also fixes duplicate heading bug
on portal radio fields, replaces cramped VBtnToggle with VSelect for field width, and
adds grouped field type dropdown with structure/input categories.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add configurable column widths (full/half) and optional descriptions
for radio/select/checkbox options on registration form fields.
- Migration adds display_width column to both tables
- FieldDisplayWidth enum with smart defaults per field type
- normalized_options accessor for backwards-compatible option format
- Portal form renderer uses display_width for VRow/VCol grid layout
- Radio/select/checkbox options render with descriptions
- Admin field editor supports display_width toggle and description input
- System templates updated with appropriate widths and descriptions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes password from the volunteer registration form. Account
creation is now deferred to the approval step:
Backend:
- Registration creates Person without User (user_id=null)
- On approval, system finds or creates User by person.email
- New accounts get a "set password" email with activation link
- Existing accounts get a portal link email
- Added registration_source column to persons (self/organizer)
- Fuzzy name matching skipped for self-registered persons
- person.email is always source of truth for account linking
Frontend:
- Registration form no longer collects password
- Email check shows info alert with login suggestion
- New wachtwoord-instellen.vue page for account activation
- PasswordRequirements.vue component (reused on reset page)
- Success page updated with activation messaging
Tests: 837 passed (all updated for new flow)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three verification methods (TOTP authenticator, email code, backup codes),
trusted device management with 30-day expiry, role-based enforcement for
super_admin and org_admin, admin reset capability, and full test coverage
(46 tests). Modifies login flow to support MFA challenge/response with
temporary session tokens stored in cache.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds the full transactional email system:
- Redis queue (QUEUE_CONNECTION=redis), SES config in .env.example
- 3 migrations: organisation_email_settings, organisation_email_templates, email_logs
- EmailTemplateType and EmailLogStatus enums with Dutch defaults
- EmailService as central entry point for all email sending
- SendTransactionalEmail queued job with retries and idempotency
- TransactionalMail mailable with responsive HTML + plain text templates
- Organisation-level branding (colors, logo, footer, reply-to)
- Per-type template overrides with {variable} substitution
- Email log with filtering by status, type, date range, recipient
- Preview and send-test endpoints for template management
- API endpoints: email-settings, email-templates (CRUD), email-logs (read-only)
- Integrated into existing flows: invitations, password reset, email
verification, registration approval/rejection
- 37 new tests across 4 test files, all existing tests updated
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Password reset: multi-app support with custom notification linking to correct
frontend (app/portal/admin). Email change: self-service with password
confirmation and admin-initiated, both sending verification to new address
with 24h expiry. Confirmation sent to old email on completion. Password
change: authenticated endpoint revoking other sessions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements the full identity matching engine: email matching (HIGH confidence),
fuzzy name matching with Levenshtein distance (MEDIUM confidence, upgradable to
HIGH with DOB tiebreaker), manual link/unlink, revert confirmed matches, and
automatic detection via PersonObserver. Includes 33 comprehensive tests, frontend
integration with confirm/dismiss/unlink UI, and match indicators in the persons list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement EAV system for dynamic event-specific registration fields
with organisation-level templates, person section preferences with
priority ranking, and TagSyncService for deferred tag_picker sync.
New tables: registration_field_templates, registration_form_fields,
person_field_values, person_section_preferences.
New columns: persons.remarks, events.registration_show_section_preferences,
events.registration_show_availability.
58 tests, 126 assertions — all 432 tests pass (zero regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add cancelled_by, cancellation_source (organiser|volunteer|system), and
cancelled_at columns to shift_assignments. Cancel flow now records who
cancelled and why. Assign flow reactivates existing cancelled/rejected
records instead of creating duplicates, preventing UNIQUE constraint
violations. Assignable-persons endpoint returns previous_assignment data
for contextual UI indicators. Frontend shows cancellation source labels,
previous assignment history in assign dialog, and "Opnieuw toewijzen"
buttons with volunteer-cancelled confirmation dialogs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements the complete ShiftAssignment lifecycle:
- ShiftAssignmentStatus enum with allowed transitions
- ShiftAssignmentService with claim/assign/approve/reject/cancel/bulkApprove
- ShiftAssignmentController with event-scoped endpoints
- ShiftAssignmentPolicy (organizer + volunteer self-cancel)
- VolunteerAvailability model, controller, and sync endpoint
- Refactored ShiftController to delegate to service layer
- 31 workflow tests covering all paths and multi-tenancy
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Audit and complete the Crowd Lists module:
- Add CrowdListType enum (internal/external) with proper casts
- Create CrowdListService for business logic (add/remove person,
max_persons enforcement, auto_approve, activity logging)
- Create CrowdListFactory with Dutch names and states
- Create AddPersonToCrowdListRequest form request
- Fix FormRequests to use Rule::enum instead of hardcoded strings
- Fix CrowdListResource to use enum->value and add is_full field
- Refactor controller to be thin (delegates to service)
- Add eager loading for crowdType and recipientCompany
- Write 18 comprehensive tests (CRUD, auth, edge cases)
- Update API.md with request/response documentation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements enterprise-grade identity resolution (detect → suggest → confirm)
for Person ↔ User linking. Matches are detected automatically on person
creation and user account creation, then surfaced to organisers for explicit
confirmation or dismissal. No silent auto-linking.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>