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.']; }); } }