feat(form-builder): add core services (schema, field, submission, value, field-access, locale, tag-sync, filter, webhook, anonymisation)
S2b Phase 1 per ARCH-FORM-BUILDER.md §20.2. Ten services + supporting
exceptions, jobs, and the organisations.default_locale column needed by
FormLocaleResolver. All services log via spatie/laravel-activitylog, write
operations are transactional, queued jobs are idempotent.
- FormSchemaService: CRUD, slug, version bump, duplicate, edit-lock,
public_token rotation (7-day grace window), typed-confirmation delete.
- FormFieldService: CRUD, reorder, insertFromLibrary, binding-change guard
(§6.5), conditional_logic + section cycle detection (§8, §4.8.1),
is_filterable toggle triggers BackfillFormValueIndexedJob (§7.2, §22.10).
- FormSubmissionService: createDraft with idempotency, saveDraft (auto-save),
submit with schema snapshot + signature hash computation (§9), review,
delegate/revoke, soft delete. Fires S1 domain events (§17.1).
- FormValueService: bulk upsert with FieldAccessService RBAC (§24.2),
Pattern A/C entity mirror writes (§6.1, §6.6) with cross-entity graceful
skip for person.user_id=null.
- FieldAccessService: canRead/canWrite/filterVisibleFields honouring
role_restrictions + subject-self (§18.3, §24.1).
- FormLocaleResolver: submitter → schema → org.default_locale → 'nl' (§16.2).
- FormTagSyncService: rebuildForPerson — replaces legacy TagSyncService
deleted in S2a (§31.10).
- FilterQueryBuilder: generic filter applier for entity_column / tags /
form_field sources (§7.4–§7.5).
- FormWebhookDispatcher + DeliverFormWebhookJob: HMAC-signed delivery with
SSRF protection, exponential backoff {1m,5m,30m,2h,8h}, max 5 attempts,
dead-letter on exhaustion (§17.5).
- FormSubmissionAnonymisationService: per-field anonymisation with separate
activity log entries (§13.3, §23.4).
MigrationRollbackTest: pin the S2a drop migration by filename so future
migrations don't shift the step offset.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -67,11 +67,13 @@ final class MigrationRollbackTest extends TestCase
|
||||
{
|
||||
Artisan::call('migrate:fresh');
|
||||
|
||||
// step=1 targets the most recent migration — the S2a drop —
|
||||
// whose down() is a hard failure.
|
||||
// Target the S2a drop migration by name so subsequent unrelated
|
||||
// migrations don't shift the step offset.
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('Legacy registration tables cannot be restored');
|
||||
|
||||
Artisan::call('migrate:rollback', ['--step' => 1]);
|
||||
$path = 'database/migrations/2026_04_20_100000_drop_remaining_legacy_registration_tables.php';
|
||||
$migration = require base_path($path);
|
||||
$migration->down();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user