78 lines
4.9 KiB
PHP
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;
|
|
});
|
|
}
|
|
}
|