Files
crewli/api/app/Exceptions/FormBuilder/IdentityMatchInvariantViolation.php
bert.hausmans f94b3fb329 feat(form-builder): exception hierarchy for binding-apply pipeline
Per RFC-WS-6 §Q3 v1.3 addition 2 (binding hierarchy) + §Q2 (invariant exception).

- Refactored FormBindingApplicatorException from concrete final to abstract
  base. Constructor (submissionId, message, previous?) preserves submissionId
  as a public readonly property so D2's outer-transaction handler can write
  it structurally to form_submission_action_failures.context JSON without
  regex-parsing the message. Replaced public-readonly reasonCode property
  with abstract reasonCode(): string method.
- Added 3 reason-coded subclasses:
  - FormBindingSchemaConfigException -> 'schema_config_error' (422)
  - FormBindingInfraException -> 'temporary_error' (503, NOT final because
    Timeout extends it)
  - FormBindingDataIntegrityException -> 'data_integrity_error' (422)
- Added FormBindingApplicatorTimeoutException extending FormBindingInfraException
  (timeout = temporary infra issue from user perspective; reasonCode inherited).
- Added IdentityMatchInvariantViolation as a sibling DomainException — NOT
  in the FormBindingApplicatorException hierarchy because it's thrown
  outside the binding-applicator pipeline.
- Migrated 3 existing throw sites in FormBindingApplicator::apply():
  - 'no_transaction' -> FormBindingInfraException (developer-error wants
    infra-triage workflow: GlitchTip alert + retry-after)
  - 'no_schema' -> FormBindingSchemaConfigException
  - 'unknown_purpose' -> FormBindingSchemaConfigException
- Updated FormBindingApplicatorIntegrationTest::test_no_transaction_guard_present
  to assert against the new throw shape (FormBindingInfraException + new
  message string) while preserving the test's intent (guard exists in source).

Wiring (deadline wrapper, classifier integration in listener catch +
retry-service recordFailure) lands in D2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 01:58:11 +02:00

37 lines
1.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Exceptions\FormBuilder;
use DomainException;
/**
* Thrown by TriggerPersonIdentityMatchOnFormSubmit when the post-ApplyBindings
* invariant breaks.
*
* Per RFC-WS-6 §Q2 + ARCH-BINDINGS §7.3:
* Post ApplyBindingsOnFormSubmit::handle for event_registration purpose:
* subject_type='person' AND subject_id IS NOT NULL,
* OR apply_status=ApplyStatus::FAILED.
* No third state exists. Violation is a structural defect.
*
* This is NOT a FormBindingApplicatorException — the listener that throws
* this does not run inside the FormBindingApplicator pipeline. It indicates
* the pipeline succeeded according to apply_status but produced incoherent
* subject state, which means a publish-guard gap or a race with manual
* data manipulation.
*
* Routed via Laravel queue worker → GlitchTip + form_submission_action_failures
* row (the catch + outer-transaction handler in TriggerPersonIdentityMatchOnFormSubmit
* mirrors ApplyBindingsOnFormSubmit's pattern). Wiring lands in D2.
*/
final class IdentityMatchInvariantViolation extends DomainException
{
// Plain DomainException subclass. No reasonCode — when this fires,
// 'unknown_error' (the classifier's default fallback) is the right
// response-shape because users cannot meaningfully act on it; admins
// triage via GlitchTip. D2's invariant-throw path writes its own
// context JSON to form_submission_action_failures.
}