100 lines
6.5 KiB
PHP
100 lines
6.5 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
|
|
{
|
|
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, 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, 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.'];
|
|
});
|
|
}
|
|
}
|