Files
Investidor-V2/app/Services/InterestSubmission.php
T
nala e65cb26901
tests / ci (push) Canceled after 0s
varias alteracoes
2026-10-06 14:50:12 +02:00

90 lines
4.4 KiB
PHP

<?php
namespace App\Services;
use App\Models\InterestRequest;
use Closure;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use SensitiveParameter;
final class InterestSubmission
{
/**
* @param array<string, mixed> $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(),
]);
}
}