Commit Graph

11 Commits

Author SHA1 Message Date
9fa8231cf7 refactor(form-field): extract FormFieldChildTableMorphScope abstract base
Closes the WS-5 family follow-up tracked as
FORM-BUILDER-MORPH-SCOPE-BASE-CLASS in BACKLOG.md. Per addendum
§Q3 Uitvoering across WS-5a/b/c/d, base-class extraction was
deliberately deferred until all four concrete morph-scope siblings
existed and the "what actually varies" question could be answered
empirically.

The answer is: nothing. All four siblings —
FormFieldBindingScope (WS-5a), FormFieldValidationRuleScope (WS-5b),
FormFieldConfigScope (WS-5b commit 5), and FormFieldOptionScope
(WS-5d) — are byte-equal in their apply() and resolveOrganisationId()
methods (Phase A diff verification clean: zero lines diverging
across all three pairwise comparisons).

Approach:

  - New abstract class FormFieldChildTableMorphScope holds the full
    UNION-over-two-owner-chains scope logic with the morph alias
    literals extracted as private constants
    (OWNER_TYPE_FIELD, OWNER_TYPE_LIBRARY) for one-location-of-truth.
  - The four concrete scopes become marker subclasses
    (`final class X extends FormFieldChildTableMorphScope {}`) — class
    identity preserved so every existing
    `withoutGlobalScope(FormFieldXScope::class)` call site in cascade
    observers, backfill migrations, and platform super_admin paths
    continues to work unchanged. The 4 test call sites (in the four
    *ScopeTest classes) work without modification.
  - Helper visibility stays `private` per YAGNI. If a future sibling
    needs to vary the morph aliases or owner-chain, the helpers
    promote to `protected` at that point.
  - Stylistic refinement vs. the four originals: `Organisation` and
    `Event` in resolveOrganisationId() now use `use` statements at
    the top of the file instead of inline `\App\Models\…` FQNs.

Net diff:
  Pre:  4 concrete scope files at ~106 lines each (~424 lines total)
  Post: 4 marker subclasses at 20 lines (80 total) +
        1 abstract base at 125 lines = 205 lines total
  Saving: ~219 lines of duplication removed.

Tests: 1208 passed (3260 assertions) → 1208 passed (3260 assertions).
Identical — public behaviour unchanged.

Larastan: clean (no new errors beyond baseline).
Rector: 357 → 355 dry-run suggestions (small reduction from the
deduplication; no apply in this commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 04:47:30 +02:00
11588623c5 feat(form-field): add form_field_options table, service, scope, cascade
Fourth and final WS-5 sibling. Polymorphic morph-owned table for the
RADIO / SELECT / MULTISELECT / CHECKBOX_LIST option rows, shared
between form_fields and form_field_library via the owner_type
discriminator. Matches the WS-5a (bindings) / WS-5b (validation_rules
+ configs) pattern one-for-one: dedicated service as single writer,
UNION-over-two-owner-chains scope, shared cascade observer.

Row shape:
  - value         canonical storage value (string ≤255, UNIQUE per owner)
  - label         default-locale display label (string ≤255)
  - sort_order    int unsigned
  - translations  JSON { "<locale>": "<translated label>" }

The UNIQUE(owner_type, owner_id, value) index ffo_owner_value_unique
is the seed-bug guard — duplicate values per field have no semantic
meaning and must fail at both the service layer (assertSpecsValid)
and the DB level.

Activity log: field.options_replaced emits on FormField subject only,
per the §6.7 WS-5a / §17.4.2 WS-5b convention that library-level
changes are silent in activity log.

No production reads yet. The form_fields.options and
form_field_library.options JSON columns remain the active source of
truth until the commit-3 reader switch — accessing $field->options
still resolves through the JSON cast in commit 1, so model tests
exercise the new morphMany via $field->options() (explicit relation
call). Both FormField and FormFieldLibrary now carry an `options`
morphMany alongside `bindings`, `validation_rules`, and `configs`.

Cascade: FormFieldChildTablesCascadeObserver gains form_field_options
as the fourth child cleaned on owner delete (both FormField soft/
force-delete and FormFieldLibrary delete).

Migration step-count tests in WS-5a/b/c bumped by 1 to account for
the new create_form_field_options_table on the migration stack.

Base scope-class extraction across the four siblings — deliberately
deferred to a follow-up work package per addendum §17.4.3 / §17.5.3.
Now that all four concrete implementations exist, the "what actually
varies" question can be answered empirically.

Tests: 1158 → 1182 green (+24 tests / +42 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:07:53 +02:00
2064b9901e feat(form-builder): form_field_conditional_logic_{groups,conditions} tables + OrganisationScope cap raise to 5
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>
2026-04-24 23:43:34 +02:00
d494478c08 feat(form-builder): form_field_configs relational table + non-validation key split + drop validation_rules JSON columns 2026-04-24 22:42:35 +02:00
fedaed1b32 feat(form-builder): form_field_validation_rules table + polymorphic owner + scope + cascade 2026-04-24 22:01:36 +02:00
af8a9da038 feat(form-builder): form_field_bindings table + polymorphic owner + cascade observer
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>
2026-04-24 18:43:11 +02:00
b688ec26f0 feat(scope): declarative FK-chain strategy for OrganisationScope, register on 14 models per addendum Q2 + D-03/D-04
Refactors OrganisationScope to support a declarative, recursive FK-chain
resolver and registers the scope on 14 models that previously relied on
caller-discipline for tenant isolation.

Scope resolver (app/Models/Scopes/OrganisationScope.php):
Models now declare their strategy via:

    public static function tenantScopeStrategy(): array
    {
        return ['column' => 'organisation_id'];           // terminal
        // OR
        return ['via' => FormSchema::class, 'fk' => 'form_schema_id'];
    }

The apply() path walks the chain recursively, building whereIn subqueries
against parent models until it hits a column-based strategy. Max 3 hops;
deeper chains raise App\Exceptions\TenantScopeResolutionException. The
walker accepts BOTH the new tenantScopeStrategy() and the legacy
$organisationScopeColumn property at every hop — so PersonIdentityMatch
can chain via Person, which still uses the legacy event_id bridge, without
requiring Person/Event/Shift/FestivalSection/TimeSlot to migrate to the
new convention in this work package. That migration is a separate
backlog ticket — explicitly scope-controlled per the addendum.

Fourteen newly-scoped models:

  Form-builder child models (D-03):
    FormSchemaSection             via FormSchema                    (1 hop)
    FormField                     via FormSchema                    (1 hop)
    FormSubmission                column organisation_id (Commit 2)
    FormValue                     via FormSubmission                (1 hop)
    FormValueOption               via FormValue -> FormSubmission   (2 hops)
    FormSubmissionSectionStatus   via FormSubmission                (1 hop)
    FormSubmissionDelegation      via FormSubmission                (1 hop)
    FormSchemaWebhook             via FormSchema                    (1 hop)
    FormWebhookDelivery           via FormSubmission                (1 hop)

  Event-data models (D-04 event-data subset):
    ShiftAssignment               via Shift (legacy festival_section_id)
    ShiftWaitlist                 via Shift
    VolunteerAvailability         via TimeSlot (legacy event_id)
    PersonSectionPreference       via FestivalSection (legacy event_id)
    PersonIdentityMatch           via Person (legacy event_id)

Note — task directive specified VolunteerAvailability "via: Event, fk: event_id",
but the table has no event_id column (only person_id + time_slot_id).
Rerouted via TimeSlot, which carries the legacy event_id bridge; same
end result, correct FK.

Security-relevant callers made explicit:
  PublicFormSchemaResource::toArray() now eagerly loads fields + sections
  with withoutGlobalScope(OrganisationScope::class). Prior to this commit
  the public form endpoint silently relied on those relations being
  unscoped. The PublicFormCrossOrgScopeTest pre-existing assertions still
  pass — behaviour unchanged, intent now explicit.

Test fix: FormSchemaApiTest::test_publish_sets_is_published_true was
flaky (factory randomly picked EVENT_REGISTRATION which requires
bindings). Pinned to USER_PROFILE for determinism; PurposeSchemaLifecycleTest
covers the binding-enforcement path.

Test flip: MultiTenancyTest::test_form_schema_webhook_is_not_globally_scoped
renamed to is_scoped_via_fk_chain and asserts the new behaviour: scope
filters by route org, withoutGlobalScope() still exposes cross-org rows.
The test's original purpose ("pin current behaviour so a future refactor
is intentional") is now satisfied by Commit 3 being that intentional
refactor.

Docs:
  SCHEMA.md §3.5.11 Rule 5 — tenantScopeStrategy() convention documented;
    the 14 newly-scoped models enumerated; link to addendum Q2.
  ARCH-FORM-BUILDER.md §4.14 — new section "Multi-tenancy scope chain"
    with the hop-count table for all 14 chains and the withoutGlobalScope
    pattern for cross-org callers.

Tests: tests/Feature/MultiTenancy/ScopeLeakageTest.php — two orgs with
fully-populated record chains down to each of the 14 leaf models; asserts
scoped queries never cross, withoutGlobalScope still does. Plus: three-
hop chain (FormValueOption) explicitly exercised, legacy-column bridge
verified, over-deep chain raises TenantScopeResolutionException. 16 tests /
31 new assertions. Full suite: 1000 passed (2706 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:08:33 +02:00
090d2b7d89 security: round 2 — multi-tenancy isolation (OrganisationScope, scoped validation, boundary checks)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 06:38:19 +02:00
0d24506c89 feat: consolidate frontend API layer, add query-client, and harden backend Fase 1
Frontend:
- Consolidate duplicate API layers into single src/lib/axios.ts per app
- Remove src/lib/api-client.ts and src/utils/api.ts (admin)
- Add src/lib/query-client.ts with TanStack Query config per app
- Update all imports and auto-import config

Backend:
- Fix organisations.billing_status default to 'trial'
- Fix user_invitations.invited_by_user_id to nullOnDelete
- Add MeResource with separated app_roles and pivot-based org roles
- Add cross-org check to EventPolicy view() and update()
- Restrict EventPolicy create/update to org_admin/event_manager (not org_member)
- Attach creator as org_admin on organisation store
- Add query scopes to Event and UserInvitation models
- Improve factories with Dutch test data
- Expand test suite from 29 to 41 tests (90 assertions)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:35:34 +02:00
fda161ee09 chore: align migrations, docs, and frontends with crewli.app setup
- Replace dated migrations with ordered 2026_04_07_* chain; fold users update into base migration
- Update OrganisationScope, AppServiceProvider, seeders, api routes, and .env.example
- Refresh Cursor rules, CLAUDE.md, Makefile, README, and docs (API, SCHEMA, SETUP)
- Adjust admin/app/portal HTML, packages, api-client, events types, and theme config
- Update docker-compose and VS Code settings; remove stray Office lock files from resources

Made-with: Cursor
2026-04-07 10:45:34 +02:00
1cb7674d52 refactor: align codebase with EventCrew domain and trim legacy band stack
- Update API: events, users, policies, routes, resources, migrations
- Remove deprecated models/resources (customers, setlists, invitations, etc.)
- Refresh admin app and docs; remove apps/band

Made-with: Cursor
2026-03-29 23:19:06 +02:00