Files
crewli/api/app/Exceptions/FormBuilder/PersonProvisioningException.php
bert.hausmans d257d64925 feat(form-builder): add PersonProvisioner with race-safe firstOrCreate (WS-6)
PersonProvisioner reads bindings from schema_snapshot (RFC Q6) and
provisions Persons via lockForUpdate + firstOrCreate (RFC Q8).
Person is event-scoped (Person::$organisationScopeColumn = 'event_id'),
so the lookup matches by (email, event_id) — cross-event submissions
never collide.

Throws PersonProvisioningException on misconfiguration (failsafe —
publish guards should prevent these at config time): no_transaction,
no_event, no_identity_key, identity_key_missing_value, no_crowd_type.

Snapshot enrichment: FormFieldBindingService::toApplicatorShape +
FormSubmissionService snapshot now adds a 'bindings' (plural) key with
binding id, merge_strategy, trust_level, is_identity_key. Singular
'binding' key kept for legacy webhook / GDPR readers.

Includes RFC V4 state-injection concurrency test asserting recovery
semantics under lockForUpdate windows.

Refs: RFC-WS-6.md §3 (Q6, Q8), §4 (V4)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:43:12 +02:00

28 lines
874 B
PHP

<?php
declare(strict_types=1);
namespace App\Exceptions\FormBuilder;
use RuntimeException;
/**
* RFC-WS-6 §3 (Q8) — failure during PersonProvisioner operation.
*
* Reason codes (informal contract):
* - 'no_transaction' called outside DB::transaction
* - 'no_identity_key' schema has no is_identity_key=true binding for person
* - 'no_event' submission missing event_id (Person scope is event_id)
* - 'identity_key_missing_value' identity-key form_value is absent or null
*/
final class PersonProvisioningException extends RuntimeException
{
public function __construct(
public readonly string $reasonCode,
public readonly string $submissionId,
?string $message = null,
) {
parent::__construct($message ?? "Person provisioning failed: {$reasonCode} (submission {$submissionId})");
}
}