Files
Investidor-V2/app/Services/OpportunityContacts.php
T

104 lines
6.6 KiB
PHP

<?php
namespace App\Services;
use App\Models\Contact;
use App\Models\Investor;
use App\Models\Opportunity;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
/** Reuse the CRM master without manufacturing an interest, lead or consent. */
class OpportunityContacts
{
/**
* @param array<array-key, mixed> $input
* @return array<array-key, mixed>
*/
public function save(User $actor, int $id, array $input): array
{
$access = app(PortalProfileAccess::class);
$record = Opportunity::withTrashed()->findOrFail($id);
$access->authorize($actor, 'edit', $record);
abort_unless($access->mfa($actor), 403, 'Confirme a autenticação multifator nesta sessão.');
// Validate the persisted form of names/email, rather than padding.
// Non-string input stays with the validator and cannot cause a TypeError.
if (is_string($input['full_name'] ?? null)) {
$input['full_name'] = trim(preg_replace('/\s+/u', ' ', $input['full_name']));
}
if (is_string($input['email'] ?? null)) {
$input['email'] = strtolower(trim($input['email']));
}
$data = Validator::make($input, [
'etag' => 'required|string|size:64',
'full_name' => 'required|string|min:2|max:150',
'email' => 'required|email:rfc|max:150',
'phone' => ['required', 'regex:/^\+[1-9]\d{7,14}$/'],
'investor_id' => 'nullable|uuid',
'investor_type' => ['nullable', Rule::in(['domestic', 'foreign', 'institutional', 'individual'])],
])->validate();
return DB::transaction(function () use ($actor, $id, $data, $access) {
$record = Opportunity::withTrashed()->whereKey($id)->lockForUpdate()->firstOrFail();
$access->authorize($actor, 'edit', $record);
abort_unless(hash_equals(app(OpportunityManagement::class)->etag($record), $data['etag']), 409, 'A oportunidade mudou. Recupere a versão atual antes de cadastrar o contacto.');
DB::select('SELECT pg_advisory_xact_lock(170925, hashtext(?))', [$data['email']]);
$matches = Contact::withTrashed()->whereRaw('lower(trim(email)) = ?', [$data['email']])->lockForUpdate()->get();
$scoped = function (Contact $contact) use ($record): bool {
return $record->organization_id
? DB::table('investor_contacts as ic')->join('investors as i', 'i.id', '=', 'ic.investor_id')
->where('ic.contact_id', $contact->id)->where('i.organization_id', $record->organization_id)->whereNull('i.deleted_at')->exists()
: $contact->source_system === 'portal-user-contact' && $contact->external_id === 'user:'.$record->owner_user_id;
};
if ($matches->isNotEmpty()) {
$contact = $matches->first(fn ($row) => ! $row->trashed() && $scoped($row));
if ($matches->count() !== 1 || ! $contact || $contact->full_name !== $data['full_name'] || $contact->phone !== $data['phone']) {
throw ValidationException::withMessages(['email' => 'Não foi possível reutilizar este contacto neste âmbito. Escolha um contacto autorizado existente ou reconcilie os dados no cadastro mestre; nenhum registo foi alterado.']);
}
return ['contact' => $contact->only(['id', 'full_name']), 'reused' => true, 'message' => 'Contacto existente recuperado, sem duplicação. Guarde a seleção no rascunho.'];
}
$investor = null;
if ($record->organization_id) {
DB::select('SELECT pg_advisory_xact_lock(170926, hashtext(?))', [$record->organization_id]);
$investors = Investor::where('organization_id', $record->organization_id)->whereIn('status', ['prospect', 'active'])->lockForUpdate()->get();
if (! empty($data['investor_id'])) {
$investor = $investors->firstWhere('id', $data['investor_id']);
abort_unless($investor !== null, 403, 'O cadastro selecionado não pertence ao promotor desta oportunidade.');
} elseif ($investors->count() === 1) {
$investor = $investors->first();
} elseif ($investors->count() > 1) {
throw ValidationException::withMessages(['investor_id' => 'Selecione o cadastro mestre do promotor; não será criada outra entidade.']);
} else {
if (empty($data['investor_type'])) {
throw ValidationException::withMessages(['investor_type' => 'Indique a classificação do promotor no cadastro existente.']);
}
$organization = DB::table('organizations')->where('id', $record->organization_id)->whereNull('deleted_at')->first();
abort_unless($organization !== null, 422, 'Organização indisponível.');
$investor = new Investor;
PortalAudit::save($investor, ['organization_id' => $organization->id, 'legal_name' => $organization->name,
'investor_type' => $data['investor_type'], 'status' => 'prospect', 'source_system' => 'portal-promoter-master',
'external_id' => 'organization:'.$organization->id], $actor->id);
}
} else {
abort_unless(empty($data['investor_id']) && empty($data['investor_type']), 422);
}
$contact = new Contact;
PortalAudit::save($contact, ['full_name' => $data['full_name'], 'email' => $data['email'], 'phone' => $data['phone'],
'source_system' => $record->organization_id ? 'portal-promoter-contact' : 'portal-user-contact',
'external_id' => $record->organization_id ? null : 'user:'.$record->owner_user_id,
'data_classification' => 'internal'], $actor->id);
if ($investor) {
DB::table('investor_contacts')->insertOrIgnore(['investor_id' => $investor->id, 'contact_id' => $contact->id, 'is_primary' => false]);
}
$access->audit($actor, 'opportunity_contact_registered', ['record_id' => $id, 'contact_id' => $contact->id,
'organization_id' => $record->organization_id, 'investor_id' => $investor?->id, 'automatically_published' => false]);
return ['contact' => $contact->only(['id', 'full_name']), 'reused' => false, 'message' => 'Contacto guardado no cadastro existente. Guarde a seleção no rascunho; não foi publicado.'];
});
}
}