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; }); } }