feat(form-builder): add apply_status columns and action-failures table (WS-6)

- form_submissions: apply_status (nullable, NO default for legacy rows
  per RFC O1), apply_completed_at, indexed on (form_schema_id, apply_status)
  and (organisation_id, apply_status)
- form_submission_action_failures: ULID PK, FK to submission + binding,
  resolve/dismiss state separated (RFC V2), retention via parent
  cascade-delete
- Migration rehearsal test added (invokes down() directly because the new
  migrations land between WS-5a and WS-5b chronologically, not at the tail
  of the migration list)

Three pre-existing WS-5 backfill tests also bump their --step rollback
counts by +2 (FormFieldBindingMigrationTest, FormFieldConfigBackfillAndDropTest,
FormFieldValidationRuleBackfillTest) to account for the two new migrations
sitting in the chronological middle of the WS-5 stack — required to keep
those tests' pre-WS-5b rollback target reachable.

SCHEMA.md updated to v2.3.
Refs: RFC-WS-6.md §3 (Q4, Q5), §4 (V2), §5 (O1)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 22:33:39 +02:00
parent 47a0dc875b
commit c033dc6cd2
7 changed files with 337 additions and 23 deletions

View File

@@ -34,12 +34,13 @@ final class FormFieldValidationRuleBackfillTest extends TestCase
// Roll back: 3 WS-5d migrations (drop-options-cols, backfill-options,
// create-options) + 4 WS-5c migrations (drop-conditional-logic-col,
// backfill-conditional-logic, create-conditional-logic-conditions,
// create-conditional-logic-groups) + 5 WS-5b migrations
// create-conditional-logic-groups) + 2 WS-6 migrations
// (action-failures, apply-status) + 5 WS-5b migrations
// (drop-cols + configs-backfill + create-configs +
// validation-rules-backfill + create-validation-rules) = 12.
// validation-rules-backfill + create-validation-rules) = 14.
// Brings us to the pre-WS-5b state: validation_rules JSON column
// present, no relational tables for WS-5b/c/d.
$this->artisan('migrate:rollback', ['--step' => 12])->assertSuccessful();
$this->artisan('migrate:rollback', ['--step' => 14])->assertSuccessful();
$this->assertFalse(Schema::hasTable('form_field_validation_rules'));
$this->assertTrue(Schema::hasColumn('form_fields', 'validation_rules'));
@@ -100,7 +101,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase
// (validation_rules JSON column present; no relational tables for
// WS-5b). Step count: drop-cols + configs-backfill + create-configs
// + validation-rules-backfill + create-validation-rules = 5.
$this->artisan('migrate:rollback', ['--step' => 12])->assertSuccessful();
$this->artisan('migrate:rollback', ['--step' => 14])->assertSuccessful();
$fieldId = $this->seedFieldWithJson([
'field_type' => 'TAG_PICKER',
@@ -124,7 +125,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase
// (validation_rules JSON column present; no relational tables for
// WS-5b). Step count: drop-cols + configs-backfill + create-configs
// + validation-rules-backfill + create-validation-rules = 5.
$this->artisan('migrate:rollback', ['--step' => 12])->assertSuccessful();
$this->artisan('migrate:rollback', ['--step' => 14])->assertSuccessful();
$fieldId = $this->seedFieldWithJson([
'field_type' => 'TEXT',
@@ -151,7 +152,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase
// (validation_rules JSON column present; no relational tables for
// WS-5b). Step count: drop-cols + configs-backfill + create-configs
// + validation-rules-backfill + create-validation-rules = 5.
$this->artisan('migrate:rollback', ['--step' => 12])->assertSuccessful();
$this->artisan('migrate:rollback', ['--step' => 14])->assertSuccessful();
$this->seedFieldWithJson([
'field_type' => 'TEXT',
@@ -168,7 +169,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase
// (validation_rules JSON column present; no relational tables for
// WS-5b). Step count: drop-cols + configs-backfill + create-configs
// + validation-rules-backfill + create-validation-rules = 5.
$this->artisan('migrate:rollback', ['--step' => 12])->assertSuccessful();
$this->artisan('migrate:rollback', ['--step' => 14])->assertSuccessful();
$this->seedFieldWithJson([
'field_type' => 'BOOLEAN',
@@ -187,7 +188,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase
// full-back-then-full-forward cycle — rolling back all WS-5b
// migrations restores the pre-WS-5b state (columns present on
// source tables; validation rules relational table gone).
$this->artisan('migrate:rollback', ['--step' => 12])->assertSuccessful();
$this->artisan('migrate:rollback', ['--step' => 14])->assertSuccessful();
[$numberId] = $this->seedFields();
$this->artisan('migrate')->assertSuccessful();
@@ -202,7 +203,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase
// Roll back WS-5b fully → column reappears and carries canonical JSON
// reconstructed from the relational rows.
$this->artisan('migrate:rollback', ['--step' => 12])->assertSuccessful();
$this->artisan('migrate:rollback', ['--step' => 14])->assertSuccessful();
$this->assertTrue(Schema::hasColumn('form_fields', 'validation_rules'));
$field = DB::table('form_fields')->where('id', $numberId)->first();