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

78 lines
4.9 KiB
PHP

<?php
namespace App\Services;
use App\Models\Contact;
use App\Models\CrmTask;
use App\Models\InterestRequest;
use App\Models\Investor;
use App\Models\Lead;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class ConvertInterestToLead
{
/** @param array{investor_id?: string|null, legal_name?: string|null, investor_type?: string|null, owner_user_id: int, title: string, next_action: string, due_at: string} $data */
public function convert(InterestRequest $interest, array $data, int $actor): Lead
{
return DB::transaction(function () use ($interest, $data, $actor) {
$interest = InterestRequest::lockForUpdate()->findOrFail($interest->id);
$contactConsentIsCurrent = DB::table('consents')->where('interest_request_id', $interest->id)
->where('subject_ref', $interest->reference)->where('purpose', 'respond_to_interest')
->where('status', 'granted')->whereNotNull('granted_at')->where('granted_at', '<=', now())
->whereNull('withdrawn_at')->whereNull('deleted_at')->lockForUpdate()->exists();
if (! $interest->privacy_consent || ! $contactConsentIsCurrent) {
throw ValidationException::withMessages(['investor_id' => 'O pedido não tem consentimento de contacto activo.']);
}
$existing = Lead::withTrashed()->where('interest_request_id', $interest->id)->first();
if ($existing) {
if ($existing->trashed()) {
throw ValidationException::withMessages(['investor_id' => 'Este pedido já tem um lead arquivado. Reveja o registo existente.']);
}
return $existing;
}
if (! is_string($interest->email) || trim($interest->email) === '') {
throw ValidationException::withMessages(['investor_id' => 'Confirme um email antes de converter este pedido telefonico num contacto/lead CRM.']);
}
$email = strtolower(trim($interest->email));
$journeyInvestor = DB::table('investor_journeys')->where('interest_request_id', $interest->id)->whereNull('deleted_at')->value('investor_id');
if ($journeyInvestor && ($data['investor_id'] ?? null) !== $journeyInvestor) {
throw ValidationException::withMessages(['investor_id' => 'Seleccione o investidor associado à jornada original para preservar a identidade.']);
}
// Serialise intake by normalised email without merging unrelated identities.
DB::select('select pg_advisory_xact_lock(170925, hashtext(?))', [$email]);
$contacts = Contact::whereRaw('lower(trim(email)) = ?', [$email])->get();
if ($contacts->count() > 1) {
throw ValidationException::withMessages(['investor_id' => 'Existem contactos duplicados com este email. É necessária revisão antes da conversão.']);
}
$contact = $contacts->first();
$linkedInvestors = $contact ? DB::table('investor_contacts')->where('contact_id', $contact->id)->pluck('investor_id') : collect();
if (empty($data['investor_id']) && $linkedInvestors->isNotEmpty()) {
throw ValidationException::withMessages(['investor_id' => 'Este contacto já representa um investidor. Seleccione o registo existente.']);
}
$investor = ! empty($data['investor_id']) ? Investor::findOrFail($data['investor_id']) : new Investor;
if (! $investor->exists) {
if (empty($data['legal_name']) || empty($data['investor_type'])) {
throw ValidationException::withMessages(['legal_name' => 'Identifique o novo investidor e o seu tipo.']);
}
PortalAudit::save($investor, ['legal_name' => $data['legal_name'], 'investor_type' => $data['investor_type'], 'status' => 'prospect'], $actor);
}
if (! $contact) {
$contact = new Contact;
PortalAudit::save($contact, ['full_name' => $interest->full_name, 'email' => $email, 'phone' => $interest->phone], $actor);
}
DB::table('investor_contacts')->insertOrIgnore(['investor_id' => $investor->id, 'contact_id' => $contact->id, 'is_primary' => false]);
$lead = new Lead;
PortalAudit::save($lead, ['investor_id' => $investor->id, 'owner_user_id' => $data['owner_user_id'],
'interest_request_id' => $interest->id, 'title' => $data['title'], 'status' => 'new',
'first_source' => 'portal:'.$interest->reference, 'last_source' => 'portal:'.$interest->reference,
'source_system' => 'portal-intake', 'external_id' => $interest->reference], $actor);
PortalAudit::save(new CrmTask, ['lead_id' => $lead->id, 'assignee_user_id' => $data['owner_user_id'],
'title' => $data['next_action'], 'status' => 'open', 'due_at' => $data['due_at']], $actor);
return $lead;
});
}
}