$payload Validated business fields only. * @param Closure(): InterestRequest $create */ public function receive(#[SensitiveParameter] array $payload, #[SensitiveParameter] ?string $key, Closure $create): InterestRequest { $canonical = array_replace(['phone' => null, 'opportunityReference' => null, 'existingBusinessId' => null, 'modality' => null, 'investmentRange' => null, 'shareWithProponent' => false], $payload); $canonical['email'] = isset($canonical['email']) && trim((string) $canonical['email']) !== '' ? strtolower(trim((string) $canonical['email'])) : null; $canonical['fullName'] = trim($canonical['fullName']); $canonical['message'] = trim($canonical['message']); $canonical['privacyConsent'] = true; // FormRequest requires acceptance. ksort($canonical); $payloadHash = hash_hmac('sha256', json_encode($canonical, JSON_THROW_ON_ERROR), config()->string('app.key')); $keyHash = $key === null ? null : hash('sha256', strtolower($key)); return DB::transaction(function () use ($keyHash, $payloadHash, $create, $canonical): InterestRequest { if ($keyHash !== null) { // Serialise concurrent submissions of this key across all web workers. DB::select('select pg_advisory_xact_lock(250925, hashtext(?))', [$keyHash]); $receipt = DB::table('interest_submission_receipts')->where('key_hash', $keyHash)->first(); if ($receipt) { if (! hash_equals($receipt->payload_hash, $payloadHash)) { throw new IdempotencyConflict; } return InterestRequest::withTrashed()->where('id', $receipt->interest_request_id)->firstOrFail(); } } $interest = $create(); $integrationId = (string) Str::uuid(); $interest->forceFill(['integration_id' => $integrationId])->save(); $this->recordConsent($interest, $integrationId, 'respond_to_interest', config('existing-business.consent_policy_version')); if ($canonical['shareWithProponent'] === true || $canonical['shareWithProponent'] === 1 || $canonical['shareWithProponent'] === '1') { $this->recordConsent($interest, $integrationId, 'share_contact_with_proponent', config('existing-business.sharing_policy_version')); } DB::table('outbox_events')->insert([ 'source_system' => 'portal-intake', 'external_id' => 'ExpressionOfInterestSubmitted:'.$integrationId, 'aggregate_type' => 'interest_request', 'aggregate_id' => $integrationId, 'event_type' => 'ExpressionOfInterestSubmitted', 'data_classification' => 'confidential', 'correlation_id' => $integrationId, 'status' => 'pending', 'payload' => json_encode(['schema_version' => 1, 'interest_id' => $integrationId], JSON_THROW_ON_ERROR), ]); if ($keyHash !== null) { DB::table('interest_submission_receipts')->insert([ 'key_hash' => $keyHash, 'payload_hash' => $payloadHash, 'interest_request_id' => $interest->id, 'created_at' => now(), ]); } return $interest; }); } private function recordConsent(InterestRequest $interest, string $correlationId, string $purpose, string $policyVersion): void { DB::table('consents')->insert([ 'id' => (string) Str::uuid(), 'source_system' => 'portal-intake', 'external_id' => 'InterestConsent:'.$interest->integration_id.':'.$purpose, 'version_no' => 1, 'data_classification' => 'confidential', 'correlation_id' => $correlationId, 'subject_ref' => $interest->reference, 'purpose' => $purpose, 'status' => 'granted', 'granted_at' => now(), 'interest_request_id' => $interest->id, 'policy_version' => $policyVersion, 'created_at' => now(), 'updated_at' => now(), ]); } }