feat(companies): add kvk_number column for B2B identity binding (WS-6)
WS-6 binding-target registry references company.kvk_number as a B2B identity-key candidate. The column needed to exist on the model before the registry could legitimately reference it. Nullable because not every Company has a registered KvK (foreign companies, partners, agencies); identity-key publish guards enforce presence where required, not at schema level. Changes: - New migration `2026_04_28_140000_add_kvk_number_to_companies_table` adds nullable string column + index after `type`. - Company::$fillable expanded. - CompanyFactory generates an 8-digit KvK by default. - CompanyKvkNumberTest covers attribute persistence, nullability, and information_schema-verified index existence. - SCHEMA.md → v2.8 with the new column row + indexes line. - Schema dump regenerated (CI fast-path). Migration step counts in 5 backfill tests bumped +1 (the new migration sits at the top of the migration stack): - FormFieldBindingMigrationTest: 18→19, 16→17 - ConditionalLogicBackfillTest: 7→8 - FormFieldConfigBackfillAndDropTest: 13→14 - FormFieldOptionsBackfillTest: 3→4 - FormFieldValidationRuleBackfillTest: 16→17 Refs: WS-6 sessie 3a binding-target drift audit Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -57,7 +57,7 @@ final class FormFieldBindingMigrationTest extends TestCase
|
||||
// validation-rules-backfill, create-validation-rules) +
|
||||
// 2 WS-6 migrations (action-failures, apply-status) +
|
||||
// 2 WS-5a migrations (drop-binding-cols, create-bindings) = 16.
|
||||
$this->artisan('migrate:rollback', ['--step' => 18])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 19])->assertSuccessful();
|
||||
$this->assertFalse(Schema::hasTable('form_field_bindings'));
|
||||
$this->assertTrue(Schema::hasColumn('form_fields', 'binding'));
|
||||
$this->assertTrue(Schema::hasColumn('form_field_library', 'default_binding'));
|
||||
@@ -119,7 +119,7 @@ final class FormFieldBindingMigrationTest extends TestCase
|
||||
public function test_rollback_reconstructs_json_and_drops_table(): void
|
||||
{
|
||||
// Walk back the full WS-5d + WS-5c + WS-6 + WS-5b + WS-5a stack (16 migrations).
|
||||
$this->artisan('migrate:rollback', ['--step' => 18])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 19])->assertSuccessful();
|
||||
[$fieldAId] = $this->seedFieldsWithBindingJson();
|
||||
[$libAId] = $this->seedLibraryWithBindingJson();
|
||||
|
||||
@@ -134,7 +134,7 @@ final class FormFieldBindingMigrationTest extends TestCase
|
||||
// the pre-WS-5b state (conditional-logic, validation-rules, configs
|
||||
// and options tables gone, validation_rules + options JSON columns
|
||||
// reappear on source tables; binding contract intact).
|
||||
$this->artisan('migrate:rollback', ['--step' => 16])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful();
|
||||
$this->assertFalse(Schema::hasTable('form_field_options'));
|
||||
$this->assertFalse(Schema::hasTable('form_field_conditional_logic_groups'));
|
||||
$this->assertFalse(Schema::hasTable('form_field_conditional_logic_conditions'));
|
||||
|
||||
@@ -49,7 +49,7 @@ final class ConditionalLogicBackfillTest extends TestCase
|
||||
// create-options + WS-5c drop-cl-col + WS-5c backfill-cl
|
||||
// migrations to land in the conditional-logic JSON-era state with
|
||||
// no relational form_field_options table yet.
|
||||
$this->artisan('migrate:rollback', ['--step' => 7])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 8])->assertSuccessful();
|
||||
$this->assertTrue(Schema::hasColumn('form_fields', 'conditional_logic'));
|
||||
|
||||
$fieldId = $this->seedFieldWithJson([
|
||||
@@ -170,7 +170,7 @@ final class ConditionalLogicBackfillTest extends TestCase
|
||||
]);
|
||||
|
||||
// Roll back only the backfill migration — writes the JSON back.
|
||||
$this->artisan('migrate:rollback', ['--step' => 7])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 8])->assertSuccessful();
|
||||
|
||||
$reconstructed = DB::table('form_fields')
|
||||
->where('id', $fieldId)
|
||||
@@ -203,7 +203,7 @@ final class ConditionalLogicBackfillTest extends TestCase
|
||||
|
||||
public function test_unknown_top_level_key_fails_migration(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 7])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 8])->assertSuccessful();
|
||||
|
||||
$this->seedFieldWithJson([
|
||||
'hide_when' => ['all' => [['field_slug' => 'x', 'operator' => 'equals', 'value' => 1]]],
|
||||
@@ -216,7 +216,7 @@ final class ConditionalLogicBackfillTest extends TestCase
|
||||
|
||||
public function test_unknown_comparison_operator_fails_migration(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 7])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 8])->assertSuccessful();
|
||||
|
||||
$this->seedFieldWithJson([
|
||||
'show_when' => ['all' => [['field_slug' => 'x', 'operator' => 'matches_regex', 'value' => 'y']]],
|
||||
|
||||
@@ -30,7 +30,7 @@ final class FormFieldConfigBackfillAndDropTest extends TestCase
|
||||
// Roll back 4 WS-5c migrations + 2 WS-6 migrations + 5 WS-5b
|
||||
// migrations = 11, to get the pre-WS-5b state where the JSON column
|
||||
// still exists on form_fields / form_field_library.
|
||||
$this->artisan('migrate:rollback', ['--step' => 13])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 14])->assertSuccessful();
|
||||
$this->assertTrue(Schema::hasColumn('form_fields', 'validation_rules'));
|
||||
|
||||
$fieldId = $this->seedField([
|
||||
|
||||
@@ -47,7 +47,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
// Roll back only the backfill migration (latest WS-5d step).
|
||||
// Leaves the form_field_options table in place, JSON columns
|
||||
// present on the source tables, and snapshots in pre-WS-5d shape.
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 4])->assertSuccessful();
|
||||
$this->assertTrue(Schema::hasTable('form_field_options'));
|
||||
$this->assertTrue(Schema::hasColumn('form_fields', 'options'));
|
||||
|
||||
@@ -136,7 +136,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
|
||||
public function test_rollback_reconstructs_json_columns_and_snapshots(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 4])->assertSuccessful();
|
||||
[$selectId, $multiId, $libraryId] = $this->seedFieldsAndLibraryWithJson();
|
||||
$submissionId = $this->seedSubmissionWithSnapshot($selectId);
|
||||
|
||||
@@ -149,7 +149,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
|
||||
// Step back over only the backfill migration → JSON columns repopulate
|
||||
// and snapshots revert to flat-string-array shape.
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 4])->assertSuccessful();
|
||||
$this->assertSame(0, DB::table('form_field_options')->count());
|
||||
|
||||
$select = DB::table('form_fields')->where('id', $selectId)->first();
|
||||
@@ -168,7 +168,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
|
||||
public function test_fails_when_options_present_on_non_option_field_type(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 4])->assertSuccessful();
|
||||
$this->seedFieldWithOptions('TAG_PICKER', ['Veiligheid', 'Horeca']);
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
@@ -178,7 +178,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
|
||||
public function test_fails_when_options_contains_non_string_entry(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 4])->assertSuccessful();
|
||||
|
||||
$this->seedFieldWithOptionsRaw('SELECT', json_encode([
|
||||
['label' => 'A'],
|
||||
@@ -192,7 +192,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
|
||||
public function test_fails_when_options_is_object_shape(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 4])->assertSuccessful();
|
||||
|
||||
$this->seedFieldWithOptionsRaw('SELECT', json_encode([
|
||||
'XS' => 'Extra small',
|
||||
@@ -206,7 +206,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
|
||||
public function test_fails_on_translations_length_mismatch(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 4])->assertSuccessful();
|
||||
$this->seedFieldWithOptionsRaw('SELECT', json_encode(['XS', 'S', 'M']), json_encode([
|
||||
'de' => ['options' => ['Klein', 'Mittel']], // 2 vs 3
|
||||
]));
|
||||
@@ -218,7 +218,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
|
||||
public function test_fails_on_non_string_translation(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 4])->assertSuccessful();
|
||||
$this->seedFieldWithOptionsRaw('SELECT', json_encode(['XS', 'S']), json_encode([
|
||||
'de' => ['options' => ['Klein', 42]],
|
||||
]));
|
||||
@@ -230,7 +230,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
|
||||
public function test_fails_on_oversized_translation(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 4])->assertSuccessful();
|
||||
$this->seedFieldWithOptionsRaw('SELECT', json_encode(['XS']), json_encode([
|
||||
'de' => ['options' => [str_repeat('x', 256)]],
|
||||
]));
|
||||
@@ -242,7 +242,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
|
||||
public function test_fails_when_snapshot_options_present_on_non_option_field_type(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 4])->assertSuccessful();
|
||||
$this->seedTemplateWithSnapshotRaw([
|
||||
'fields' => [[
|
||||
'id' => (string) Str::ulid(),
|
||||
|
||||
@@ -53,7 +53,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase
|
||||
// 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' => 16])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful();
|
||||
$this->assertFalse(Schema::hasTable('form_field_validation_rules'));
|
||||
$this->assertTrue(Schema::hasColumn('form_fields', 'validation_rules'));
|
||||
|
||||
@@ -114,7 +114,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' => 16])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful();
|
||||
|
||||
$fieldId = $this->seedFieldWithJson([
|
||||
'field_type' => 'TAG_PICKER',
|
||||
@@ -138,7 +138,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' => 16])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful();
|
||||
|
||||
$fieldId = $this->seedFieldWithJson([
|
||||
'field_type' => 'TEXT',
|
||||
@@ -165,7 +165,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' => 16])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful();
|
||||
|
||||
$this->seedFieldWithJson([
|
||||
'field_type' => 'TEXT',
|
||||
@@ -182,7 +182,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' => 16])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful();
|
||||
|
||||
$this->seedFieldWithJson([
|
||||
'field_type' => 'BOOLEAN',
|
||||
@@ -201,7 +201,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' => 16])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful();
|
||||
[$numberId] = $this->seedFields();
|
||||
|
||||
$this->artisan('migrate')->assertSuccessful();
|
||||
@@ -216,7 +216,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' => 16])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful();
|
||||
$this->assertTrue(Schema::hasColumn('form_fields', 'validation_rules'));
|
||||
|
||||
$field = DB::table('form_fields')->where('id', $numberId)->first();
|
||||
|
||||
Reference in New Issue
Block a user