feat(form-builder): denormalize organisation_id and event_id on form_submissions per addendum Q2
Adds direct tenant + event columns to form_submissions so rapportage-hot
aggregate queries (dashboards, CSV-exports, counts over thousands of rows
per org or per event) skip the form_schemas join. This is the single
denormalization exception per addendum Q2; every other form-builder child
table continues to resolve tenancy via FK-chain through its parent
(implemented in Commit 3).
Schema:
- form_submissions.organisation_id ULID FK → organisations, cascade delete, NOT NULL
- form_submissions.event_id ULID FK → events, null on delete, nullable
- Indexes: (organisation_id, status), (event_id, status)
Observer: App\Observers\FormBuilder\FormSubmissionObserver::creating
resolves both columns when the caller has not set them.
- organisation_id <- form_schema.organisation_id (always present —
form_schemas carries OrganisationScope's column directly)
- event_id <- schema.owner_id when owner_type === 'event'; else the
active route's {event} parameter; else null (user_profile /
signature_contract purposes)
The observer docblock spells out both resolution paths and is covered
by the observer test below.
Model: FormSubmission gains organisation_id + event_id in $fillable, a
belongsTo organisation() and belongsTo event() relation.
Factory: FormSubmissionFactory gains forOrganisation($org) and
forEvent($event) states for tests that need to override the observer's
automatic resolution (e.g. cross-org leakage scenarios in Commit 3).
Normal factory usage does not need the states — the observer populates
both fields on save.
Docs:
- SCHEMA.md §3.5.12 form_submissions table — organisation_id and event_id
inserted between form_schema_id and subject_type; indexes added;
addendum Q2 rationale paragraph at the bottom explaining why this is
the only denormalized form-builder child.
- ARCH-FORM-BUILDER.md §4.3 — mirror changes + rationale inline on the
columns and in the indexes list.
Tests: tests/Feature/FormBuilder/FormSubmissionObserverTest.php — 7 tests
covering organisation resolution from schema, event resolution from
event-owned schema, null event_id for non-event-owned schemas without
route context, route-based event resolution, organisation_id populated
on every create path (factory / new() / Model::create), index presence,
and belongsTo relations. 13 new assertions. Full suite: 984 passed
(2675 assertions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,8 @@ namespace App\Models\FormBuilder;
|
||||
|
||||
use App\Enums\FormBuilder\FormSubmissionReviewStatus;
|
||||
use App\Enums\FormBuilder\FormSubmissionStatus;
|
||||
use App\Models\Event;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUlids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -27,6 +29,8 @@ final class FormSubmission extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'form_schema_id',
|
||||
'organisation_id',
|
||||
'event_id',
|
||||
'subject_type',
|
||||
'subject_id',
|
||||
'submitted_by_user_id',
|
||||
@@ -77,6 +81,16 @@ final class FormSubmission extends Model
|
||||
return $this->belongsTo(FormSchema::class, 'form_schema_id');
|
||||
}
|
||||
|
||||
public function organisation(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Organisation::class);
|
||||
}
|
||||
|
||||
public function event(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Event::class);
|
||||
}
|
||||
|
||||
public function subject(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
|
||||
81
api/app/Observers/FormBuilder/FormSubmissionObserver.php
Normal file
81
api/app/Observers/FormBuilder/FormSubmissionObserver.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Observers\FormBuilder;
|
||||
|
||||
use App\Models\Event;
|
||||
use App\Models\FormBuilder\FormSubmission;
|
||||
use App\Models\Scopes\OrganisationScope;
|
||||
|
||||
/**
|
||||
* Populates the denormalized organisation_id and event_id columns on
|
||||
* form_submissions per ARCH-CONSOLIDATION-ADDENDUM-2026-04-24.md Q2.
|
||||
*
|
||||
* organisation_id resolves from the parent schema (always present —
|
||||
* form_schemas carries the tenant column directly). event_id resolves
|
||||
* to the schema owner when owner_type === 'event'; otherwise it falls
|
||||
* back to the active route's {event} parameter (for submissions created
|
||||
* inside an events/{event}/... route, e.g. the portal flow). Purposes
|
||||
* like user_profile or signature_contract that have neither an
|
||||
* event-owned schema nor an event-scoped route produce event_id = null,
|
||||
* which is valid per migration 2026_04_24_200000.
|
||||
*/
|
||||
final class FormSubmissionObserver
|
||||
{
|
||||
public function creating(FormSubmission $submission): void
|
||||
{
|
||||
if ($submission->organisation_id === null) {
|
||||
$submission->organisation_id = $this->resolveOrganisationId($submission);
|
||||
}
|
||||
|
||||
if ($submission->event_id === null) {
|
||||
$submission->event_id = $this->resolveEventId($submission);
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveOrganisationId(FormSubmission $submission): ?string
|
||||
{
|
||||
if ($submission->form_schema_id === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$schema = $submission->relationLoaded('schema')
|
||||
? $submission->getRelation('schema')
|
||||
: \App\Models\FormBuilder\FormSchema::query()
|
||||
->withoutGlobalScope(OrganisationScope::class)
|
||||
->find($submission->form_schema_id);
|
||||
|
||||
return $schema?->organisation_id;
|
||||
}
|
||||
|
||||
private function resolveEventId(FormSubmission $submission): ?string
|
||||
{
|
||||
$schema = $submission->relationLoaded('schema')
|
||||
? $submission->getRelation('schema')
|
||||
: \App\Models\FormBuilder\FormSchema::query()
|
||||
->withoutGlobalScope(OrganisationScope::class)
|
||||
->find($submission->form_schema_id);
|
||||
|
||||
if ($schema !== null && $schema->owner_type === 'event' && $schema->owner_id !== null) {
|
||||
return (string) $schema->owner_id;
|
||||
}
|
||||
|
||||
// Fall back to the active route — portal + organizer flows that
|
||||
// scope submissions by the current event via /events/{event}/...
|
||||
$route = request()?->route();
|
||||
if ($route === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$eventParam = $route->parameter('event');
|
||||
if ($eventParam instanceof Event) {
|
||||
return (string) $eventParam->id;
|
||||
}
|
||||
if (is_string($eventParam) && $eventParam !== '') {
|
||||
return $eventParam;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,7 @@ use App\Models\VolunteerAvailability;
|
||||
use App\Events\FormBuilder\FormSubmissionSubmitted;
|
||||
use App\Listeners\FormBuilder\SyncTagPickerSelectionsOnSubmit;
|
||||
use App\Listeners\FormBuilder\TriggerPersonIdentityMatchOnFormSubmit;
|
||||
use App\Observers\FormBuilder\FormSubmissionObserver;
|
||||
use App\Observers\FormBuilder\FormValueObserver;
|
||||
use App\Observers\PersonObserver;
|
||||
use App\Observers\UserObserver;
|
||||
@@ -91,6 +92,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
Person::observe(PersonObserver::class);
|
||||
User::observe(UserObserver::class);
|
||||
FormValue::observe(FormValueObserver::class);
|
||||
\App\Models\FormBuilder\FormSubmission::observe(FormSubmissionObserver::class);
|
||||
|
||||
// ARCH §31.10 — FORM-02 TAG_PICKER sync listener.
|
||||
\Illuminate\Support\Facades\Event::listen(
|
||||
|
||||
@@ -5,8 +5,10 @@ declare(strict_types=1);
|
||||
namespace Database\Factories\FormBuilder;
|
||||
|
||||
use App\Enums\FormBuilder\FormSubmissionStatus;
|
||||
use App\Models\Event;
|
||||
use App\Models\FormBuilder\FormSchema;
|
||||
use App\Models\FormBuilder\FormSubmission;
|
||||
use App\Models\Organisation;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/** @extends Factory<FormSubmission> */
|
||||
@@ -40,4 +42,26 @@ final class FormSubmissionFactory extends Factory
|
||||
{
|
||||
return $this->state(fn () => ['is_test' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a specific tenant. The observer would otherwise resolve it
|
||||
* from the schema parent; this state lets tests override for edge
|
||||
* cases (e.g. cross-org leakage scenarios in Commit 3).
|
||||
*/
|
||||
public function forOrganisation(Organisation $organisation): static
|
||||
{
|
||||
return $this->state(fn () => ['organisation_id' => $organisation->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a specific event + its organisation. Addendum Q2 contract:
|
||||
* any submission tied to an event also has that event's org.
|
||||
*/
|
||||
public function forEvent(Event $event): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'event_id' => $event->id,
|
||||
'organisation_id' => $event->organisation_id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
// Addendum Q2 exception: form_submissions is the single rapportage-hot
|
||||
// table that earns denormalized organisation_id + event_id columns.
|
||||
// Every other form-builder child table resolves its tenant via FK-chain
|
||||
// through its parent (see OrganisationScope).
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('form_submissions', function (Blueprint $table): void {
|
||||
// organisation_id is NOT NULL — every submission belongs to a
|
||||
// tenant. Pre-launch: form_submissions is empty at migration
|
||||
// time, so MySQL/SQLite accept NOT NULL without a default.
|
||||
$table->foreignUlid('organisation_id')
|
||||
->after('form_schema_id')
|
||||
->constrained('organisations')
|
||||
->cascadeOnDelete();
|
||||
|
||||
// event_id is nullable — purposes like user_profile have no
|
||||
// event. The observer resolves it from the schema owner
|
||||
// (owner_type === 'event') or from the active route.
|
||||
$table->foreignUlid('event_id')
|
||||
->nullable()
|
||||
->after('organisation_id')
|
||||
->constrained('events')
|
||||
->nullOnDelete();
|
||||
|
||||
$table->index(['organisation_id', 'status'], 'fs_org_status_idx');
|
||||
$table->index(['event_id', 'status'], 'fs_event_status_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('form_submissions', function (Blueprint $table): void {
|
||||
$table->dropIndex('fs_org_status_idx');
|
||||
$table->dropIndex('fs_event_status_idx');
|
||||
$table->dropConstrainedForeignId('organisation_id');
|
||||
$table->dropConstrainedForeignId('event_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
166
api/tests/Feature/FormBuilder/FormSubmissionObserverTest.php
Normal file
166
api/tests/Feature/FormBuilder/FormSubmissionObserverTest.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\FormBuilder;
|
||||
|
||||
use App\Models\Event;
|
||||
use App\Models\FormBuilder\FormSchema;
|
||||
use App\Models\FormBuilder\FormSubmission;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* WS-4 Commit 2 coverage — addendum Q2 denormalization observer.
|
||||
*
|
||||
* Guarantees that every form_submissions row carries correct
|
||||
* organisation_id (always) and event_id (when resolvable from schema
|
||||
* owner or active route).
|
||||
*/
|
||||
final class FormSubmissionObserverTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_organisation_id_resolves_from_schema(): void
|
||||
{
|
||||
$organisation = Organisation::factory()->create();
|
||||
$schema = FormSchema::factory()->for($organisation)->create();
|
||||
|
||||
$submission = FormSubmission::factory()->for($schema, 'schema')->create();
|
||||
|
||||
$this->assertSame(
|
||||
$organisation->id,
|
||||
$submission->organisation_id,
|
||||
'Observer must denormalize organisation_id from schema parent'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_event_id_resolves_from_event_owned_schema(): void
|
||||
{
|
||||
$organisation = Organisation::factory()->create();
|
||||
$event = Event::factory()->for($organisation)->create();
|
||||
$schema = FormSchema::factory()->for($organisation)->create([
|
||||
'owner_type' => 'event',
|
||||
'owner_id' => $event->id,
|
||||
]);
|
||||
|
||||
$submission = FormSubmission::factory()->for($schema, 'schema')->create();
|
||||
|
||||
$this->assertSame(
|
||||
$event->id,
|
||||
$submission->event_id,
|
||||
'Observer must resolve event_id from schema.owner_id when owner_type = event'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_event_id_is_null_when_schema_has_no_event_owner_and_no_route(): void
|
||||
{
|
||||
$organisation = Organisation::factory()->create();
|
||||
$schema = FormSchema::factory()->for($organisation)->create([
|
||||
'owner_type' => 'organisation',
|
||||
'owner_id' => $organisation->id,
|
||||
]);
|
||||
|
||||
$submission = FormSubmission::factory()->for($schema, 'schema')->create();
|
||||
|
||||
$this->assertNull(
|
||||
$submission->event_id,
|
||||
'Non-event-owned schema outside an event route yields event_id = null'
|
||||
);
|
||||
$this->assertSame($organisation->id, $submission->organisation_id);
|
||||
}
|
||||
|
||||
public function test_event_id_resolves_from_active_route_when_schema_has_no_event_owner(): void
|
||||
{
|
||||
$organisation = Organisation::factory()->create();
|
||||
$event = Event::factory()->for($organisation)->create();
|
||||
$schema = FormSchema::factory()->for($organisation)->create([
|
||||
'owner_type' => 'organisation',
|
||||
'owner_id' => $organisation->id,
|
||||
]);
|
||||
|
||||
// Simulate a request bound to /events/{event}/... by firing the
|
||||
// observer inside a controller invocation with the route
|
||||
// parameter populated.
|
||||
Route::get(
|
||||
'/_test/events/{event}/submissions',
|
||||
function (Event $event) use ($schema): array {
|
||||
$submission = new FormSubmission();
|
||||
$submission->form_schema_id = $schema->id;
|
||||
$submission->status = 'draft';
|
||||
$submission->is_test = false;
|
||||
$submission->save();
|
||||
|
||||
return ['id' => $submission->id, 'event_id' => $submission->event_id];
|
||||
}
|
||||
);
|
||||
|
||||
$response = $this->getJson("/_test/events/{$event->id}/submissions");
|
||||
$response->assertOk();
|
||||
|
||||
$this->assertSame(
|
||||
$event->id,
|
||||
$response->json('event_id'),
|
||||
'Route {event} parameter resolves event_id when schema has no event owner'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_organisation_id_is_populated_on_every_create_path(): void
|
||||
{
|
||||
$organisation = Organisation::factory()->create();
|
||||
$schema = FormSchema::factory()->for($organisation)->create();
|
||||
|
||||
// Direct factory
|
||||
$s1 = FormSubmission::factory()->for($schema, 'schema')->create();
|
||||
// Direct new() + save()
|
||||
$s2 = new FormSubmission();
|
||||
$s2->form_schema_id = $schema->id;
|
||||
$s2->status = 'draft';
|
||||
$s2->is_test = false;
|
||||
$s2->save();
|
||||
// Model::create()
|
||||
$s3 = FormSubmission::create([
|
||||
'form_schema_id' => $schema->id,
|
||||
'status' => 'draft',
|
||||
'is_test' => false,
|
||||
]);
|
||||
|
||||
foreach ([$s1, $s2, $s3] as $submission) {
|
||||
$this->assertSame(
|
||||
$organisation->id,
|
||||
$submission->organisation_id,
|
||||
'organisation_id must never be NULL — resolved on every create path'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_denormalized_indexes_exist(): void
|
||||
{
|
||||
// Indexes named in the migration — addendum Q2 rapportage-hot.
|
||||
$indexNames = collect(DB::select(
|
||||
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='form_submissions'"
|
||||
))->pluck('name')->toArray();
|
||||
|
||||
$this->assertContains('fs_org_status_idx', $indexNames);
|
||||
$this->assertContains('fs_event_status_idx', $indexNames);
|
||||
}
|
||||
|
||||
public function test_organisation_and_event_belongs_to_relationships_resolve(): void
|
||||
{
|
||||
$organisation = Organisation::factory()->create();
|
||||
$event = Event::factory()->for($organisation)->create();
|
||||
$schema = FormSchema::factory()->for($organisation)->create([
|
||||
'owner_type' => 'event',
|
||||
'owner_id' => $event->id,
|
||||
]);
|
||||
|
||||
$submission = FormSubmission::factory()->for($schema, 'schema')->create();
|
||||
|
||||
$this->assertTrue($submission->organisation->is($organisation));
|
||||
$this->assertTrue($submission->event->is($event));
|
||||
}
|
||||
}
|
||||
@@ -666,6 +666,8 @@ submissions of the same schema. Enforcement:
|
||||
|---|---|---|
|
||||
| `id` | ULID | PK |
|
||||
| `form_schema_id` | ULID FK | → form_schemas |
|
||||
| `organisation_id` | ULID FK | → organisations, cascade delete. **Denormalized per ARCH-CONSOLIDATION-ADDENDUM-2026-04-24 Q2** — the single rapportage-hot exception. Populated by `FormSubmissionObserver::creating` from the schema parent. |
|
||||
| `event_id` | ULID FK nullable | → events, null on delete. Denormalized per addendum Q2. Observer resolves from `form_schemas.owner_id` when `owner_type = event`; else from the active route's `{event}` parameter. Null for purposes without an event context (`user_profile`, `signature_contract`). |
|
||||
| `subject_type` | string nullable | polymorph |
|
||||
| `subject_id` | ULID nullable | polymorph target |
|
||||
| `submitted_by_user_id` | ULID FK nullable | Authenticated submitter |
|
||||
@@ -694,7 +696,7 @@ submissions of the same schema. Enforcement:
|
||||
|
||||
**Relations:** belongsTo form_schema, hasMany form_values, hasMany form_submission_section_statuses, hasMany form_submission_delegations, belongsTo submittedBy / reviewedBy (User), morphsTo subject
|
||||
|
||||
**Indexes:** `(form_schema_id, status)`, `(subject_type, subject_id)`, `(submitted_by_user_id)`, `(form_schema_id, review_status)` partial where not null, `FULLTEXT(search_index)` (MySQL)
|
||||
**Indexes:** `(form_schema_id, status)`, `(organisation_id, status)` (addendum Q2 — dashboards + CSV exports aggregate by tenant + status), `(event_id, status)` (addendum Q2 — event-scoped reporting), `(subject_type, subject_id)`, `(submitted_by_user_id)`, `(form_schema_id, review_status)` partial where not null, `FULLTEXT(search_index)` (MySQL)
|
||||
|
||||
**Unique:** `(form_schema_id, idempotency_key)` partial where not null
|
||||
|
||||
|
||||
@@ -2033,6 +2033,8 @@ that aggregates the user's submitted, non-test `form_submissions`.
|
||||
| ------------------------------------- | ------------------ | ---------------------------------------------------------------------------- |
|
||||
| `id` | ULID | PK |
|
||||
| `form_schema_id` | ULID FK | → form_schemas, cascade delete |
|
||||
| `organisation_id` | ULID FK | → organisations, cascade delete. Denormalized per addendum Q2 — this is the **only** form-builder child that carries a direct tenant column; every other child resolves tenant via FK-chain through its parent. Populated by `FormSubmissionObserver` from the schema parent. |
|
||||
| `event_id` | ULID FK nullable | → events, null on delete. Denormalized per addendum Q2. Observer resolves from `form_schemas.owner_id` when `owner_type = event`; otherwise from the active route's `{event}` parameter. Null for purposes like `user_profile` / `signature_contract`. |
|
||||
| `subject_type` | string(50) null | polymorph |
|
||||
| `subject_id` | ULID nullable | polymorph target |
|
||||
| `submitted_by_user_id` | ULID FK nullable | → users, null on delete |
|
||||
@@ -2062,11 +2064,22 @@ that aggregates the user's submitted, non-test `form_submissions`.
|
||||
| `created_at`, `updated_at` | timestamps | |
|
||||
| `deleted_at` | timestamp nullable | Soft delete |
|
||||
|
||||
**Relations:** `belongsTo` schema, submittedBy / reviewedBy (User); `morphsTo` subject; `hasMany` values, section statuses, delegations
|
||||
**Indexes:** `(form_schema_id, status)`, `(subject_type, subject_id)`, `(submitted_by_user_id)`, `(form_schema_id, review_status)`, **UNIQUE** `(form_schema_id, idempotency_key)` (v2.1; replaced the non-unique composite index from v2.0), `(form_schema_id, identity_match_status)` (v2.1), `FULLTEXT(search_index)` (MySQL/InnoDB — best-effort, skipped gracefully on SQLite)
|
||||
**Relations:** `belongsTo` schema, organisation, event, submittedBy / reviewedBy (User); `morphsTo` subject; `hasMany` values, section statuses, delegations
|
||||
**Indexes:** `(form_schema_id, status)`, `(organisation_id, status)` (v2.2 — addendum Q2), `(event_id, status)` (v2.2 — addendum Q2), `(subject_type, subject_id)`, `(submitted_by_user_id)`, `(form_schema_id, review_status)`, **UNIQUE** `(form_schema_id, idempotency_key)` (v2.1; replaced the non-unique composite index from v2.0), `(form_schema_id, identity_match_status)` (v2.1), `FULLTEXT(search_index)` (MySQL/InnoDB — best-effort, skipped gracefully on SQLite)
|
||||
**Events fired:** `FormSubmissionCreated`, `FormSubmissionDraftUpdated`, `FormSubmissionSubmitted`, `FormSubmissionReviewed`, `FormSubmissionSectionSubmitted`, `FormSubmissionSectionReviewed`, `FormSubmissionAnonymised`, `FormSubmissionArchived`, `FormSubmissionDeleted`
|
||||
**Soft delete:** yes
|
||||
|
||||
> **Denormalization rationale (addendum Q2):** `form_submissions` is the
|
||||
> single rapportage-hot table that earns a denormalized `organisation_id`
|
||||
> (and `event_id`). Aggregerende queries over duizenden rijen — dashboards,
|
||||
> CSV-exports, counts — run directly against these columns instead of
|
||||
> joining through `form_schemas`. Every other form-builder child table
|
||||
> (`form_schema_sections`, `form_fields`, `form_values`, `form_value_options`,
|
||||
> `form_submission_section_statuses`, `form_submission_delegations`,
|
||||
> `form_schema_webhooks`, `form_webhook_deliveries`) resolves its tenant
|
||||
> via the FK-chain strategy added to `OrganisationScope` in the same
|
||||
> addendum.
|
||||
|
||||
---
|
||||
|
||||
### `form_submission_section_statuses`
|
||||
|
||||
Reference in New Issue
Block a user