From fa06c0f9f3eb7bfff5a1682d285bd999a8eb33fb Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 02:58:09 +0200 Subject: [PATCH] feat(form-builder): add apply_status=COMPLETED gate to SyncTagPickerSelectionsOnSubmit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per ARCH-BINDINGS §5.6 v1.2. The queued tag-sync listener now skips unless apply_status === COMPLETED. PARTIAL and FAILED both fall through to the early-return — rebuilding user_organisation_tags against a Person whose tag-binding may have been the binding that failed would propagate partial state into derived data. Logs at info level when skipped (form-builder.queued-listener.skipped_apply_failed) for triage visibility. The fresh() reload is required because the inner-txn commit happens between dispatch and worker pickup. ApplyBindingsOnFormSectionSubmitted (the other queued listener under app/Listeners/FormBuilder/) listens to FormSubmissionSectionSubmitted, a different event — the §5.6 gate is specifically about FormSubmissionSubmitted's post-apply-status state, so the section-level listener is intentionally left without this gate. Co-Authored-By: Claude Opus 4.7 --- .../SyncTagPickerSelectionsOnSubmit.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/api/app/Listeners/FormBuilder/SyncTagPickerSelectionsOnSubmit.php b/api/app/Listeners/FormBuilder/SyncTagPickerSelectionsOnSubmit.php index d8504bab..62a3dd2e 100644 --- a/api/app/Listeners/FormBuilder/SyncTagPickerSelectionsOnSubmit.php +++ b/api/app/Listeners/FormBuilder/SyncTagPickerSelectionsOnSubmit.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Listeners\FormBuilder; +use App\Enums\FormBuilder\ApplyStatus; use App\Enums\FormBuilder\FormFieldType; use App\Enums\FormBuilder\FormPurpose; use App\Events\FormBuilder\FormSubmissionSubmitted; @@ -24,6 +25,11 @@ use Illuminate\Support\Facades\Log; * person's linked user. No-ops when person.user_id is null (deferred * sync runs on PersonIdentityService::confirmMatch). * + * Gating-invariant first statement per ARCH-BINDINGS §5.6: skip unless + * apply_status is COMPLETED. PARTIAL and FAILED both fall through — + * rebuilding tags against a Person whose tag-binding may have been the + * binding that failed would propagate partial state into derived data. + * * Failure mode: log at error level; never throw. Event propagation must * reach sibling listeners (§31.1 identity, §31.3 shifts, §31.8 crowd lists). */ @@ -43,11 +49,25 @@ final class SyncTagPickerSelectionsOnSubmit implements ShouldQueue public function handle(FormSubmissionSubmitted $event): void { try { + // Gating-invariant first statement per ARCH-BINDINGS §5.6. + // The fresh() reload is required because the inner-txn commit + // happens between dispatch and worker pickup; the in-memory + // event submission may carry pre-commit state. $submission = $event->submission->fresh(['schema']); if ($submission === null) { return; } + if ($submission->apply_status !== ApplyStatus::COMPLETED) { + Log::info('form-builder.queued-listener.skipped_apply_failed', [ + 'listener' => self::class, + 'submission_id' => (string) $submission->id, + 'apply_status' => $submission->apply_status?->value, + ]); + + return; + } + $schema = $submission->schema; if ($schema === null) { return;