oportunidades 3
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-09 15:00:37 +02:00
parent 9d6176f55b
commit e662230df7
32 changed files with 865 additions and 37 deletions
+7 -2
View File
@@ -9,6 +9,8 @@ use App\Models\Investor;
use App\Models\Lead;
use App\Models\User;
use App\Services\ConvertInterestToLead;
use App\Services\CountryCatalogue;
use App\Services\DeclaredOriginStatistics;
use App\Services\PortalAudit;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\RedirectResponse;
@@ -77,7 +79,8 @@ class CrmController extends Controller
{
$filters = $request->validate(['q' => ['nullable', 'string', 'max:150'],
'status' => ['nullable', Rule::in(['new', 'qualified', 'disqualified', 'converted'])],
'owner_user_id' => ['nullable', 'integer', 'exists:users,id']]);
'owner_user_id' => ['nullable', 'integer', 'exists:users,id'],
'origin' => ['nullable', Rule::in(['unknown', ...array_column(app(CountryCatalogue::class)->all(), 'code')])]]);
$query = Lead::with(['investor:id,legal_name', 'owner:id,name'])
->withCount(['tasks as overdue_tasks_count' => fn ($q) => $q->whereIn('status', ['open', 'in_progress'])->where('due_at', '<', now())]);
if ($term = $filters['q'] ?? null) {
@@ -92,7 +95,9 @@ class CrmController extends Controller
}
return Inertia::render('crm/leads', ['leads' => $query->latest()->orderBy('id')->paginate(20)->withQueryString(),
'filters' => $filters, 'owners' => $this->owners()]);
'filters' => $filters, 'owners' => $this->owners(),
'countries' => app(CountryCatalogue::class)->all(),
'originStatistics' => app(DeclaredOriginStatistics::class)->forManager($request->user(), $filters['origin'] ?? null)]);
}
public function prepare(InterestRequest $interest): Response|RedirectResponse
@@ -128,7 +128,24 @@ class InvestmentDiscoveryController extends Controller
public function map(Request $request, InvestmentCatalogue $catalogue): JsonResponse
{
return response()->json($catalogue->map($catalogue->filters($request)));
$filters = $catalogue->filters($request);
$context = $this->territorial($filters)->map(fn ($d) => ['id' => 'dossier:'.$d->id, 'title' => $d->title, 'kind' => $d->cluster_id ? 'Cluster' : ($d->corridor_id ? 'Corredor' : 'Projeto âncora'), 'url' => '/carteira/'.$d->id]);
$opportunityOnlyFilters = array_filter(\Illuminate\Support\Arr::only($filters, ['education', 'sector', 'facets', 'project_id', 'cluster_id', 'corridor_id', 'min_amount', 'max_amount', 'currency', 'before', 'maturity_level']), fn ($value) => $value !== null && $value !== '' && $value !== false && $value !== []);
if ($opportunityOnlyFilters === [] && ! in_array($filters['dimension'] ?? null, ['cluster', 'corridor'], true)) {
$zoneFilters = $filters;
if (in_array($filters['dimension'] ?? null, ['zee', 'zfi'], true)) {
$zoneFilters['category'] = $filters['dimension'];
}
$zones = app(InvestmentZones::class)->search($zoneFilters);
foreach (['zee_id', 'zfi_id', 'park_id'] as $key) {
if (! empty($filters[$key])) {
$zones->where('id', $filters[$key]);
}
}
$context = $context->concat($zones->orderBy('name')->limit(30)->get(['id', 'name', 'category'])->map(fn ($z) => ['id' => 'zone:'.$z->id, 'title' => $z->name, 'kind' => ['zee' => 'ZEE', 'zfi' => 'ZFI', 'industrial_park' => 'Parque industrial'][$z->category] ?? $z->category, 'url' => '/zonas/'.$z->id]));
}
return response()->json([...$catalogue->map($filters), 'territorial_context' => $context->values(), 'context_limit_per_collection' => 30]);
}
public function compare(Request $request, InvestmentCatalogue $catalogue): JsonResponse
@@ -49,7 +49,7 @@ class InvestmentSubmissionController extends Controller
$company = DB::table('investment_company_declarations')->where('id', $data['company_id'])->where('user_id', $request->user()->id)->first();
abort_unless($company !== null, 404);
$companyData = json_decode(Crypt::decryptString($company->payload), true, 512, JSON_THROW_ON_ERROR);
$payload = [...$payload, 'organisation' => $companyData['name'], 'representative_role' => $companyData['role'], 'country' => $companyData['country']];
$payload = [...$payload, 'organisation' => $companyData['name'], 'representative_role' => $companyData['role'], 'incorporation_country_code' => $companyData['incorporation_country_code'] ?? null];
}
unset($data['company_id']);
if (! empty($data['opportunity_id'])) {
@@ -111,7 +111,7 @@ class InvestmentSubmissionController extends Controller
$target = $row->opportunity_id ? Opportunity::published()->find($row->opportunity_id) : null;
$dossier = $row->dossier_id ? InvestmentPortfolioController::visible()->find($row->dossier_id) : null;
$zoneOrigin = DB::table('investment_submission_zones')->where('submission_id', $id)->first(['zone_id', 'zone_version', 'intent']);
$data = ['submission' => $row->only(['id', 'payload', 'version', 'status', 'opportunity_id', 'dossier_id']), 'zone_origin' => $zoneOrigin,
$data = ['countries' => app(\App\Services\CountryCatalogue::class)->all(), 'submission' => $row->only(['id', 'payload', 'version', 'status', 'opportunity_id', 'dossier_id']), 'zone_origin' => $zoneOrigin,
'target' => ($target ? $target->title : null) ?? ($dossier ? $dossier->title : null), 'references' => [
...$catalogue->references(), 'options' => DB::table('investment_reference_options')->orderBy('label')->get()],
'needs' => InvestmentSpecificNeed::where('submission_id', $id)->get(),
@@ -144,10 +144,13 @@ class InvestmentSubmissionController extends Controller
{
$required = $submitting ? 'required' : 'nullable';
$data = $request->validate(['version' => ['required', 'integer', 'min:1'],
'payload' => ['required', 'array:full_name,email,phone,country,organisation,representative_role,experience,investment_capacity,profile_id,sector,objective,participation_id,partnership_id,estimated_amount,currency,horizon,location,cluster_ids,corridor_ids,project_ids,contact_consent,other_details'],
'payload' => ['required', 'array:full_name,email,phone,country,residence_country_code,incorporation_country_code,declared_origin_country_code,organisation,representative_role,experience,investment_capacity,profile_id,sector,objective,participation_id,partnership_id,estimated_amount,currency,horizon,location,cluster_ids,corridor_ids,project_ids,contact_consent,other_details'],
'payload.residence_country_code' => ['nullable', 'string', app(\App\Services\CountryCatalogue::class)->rule()],
'payload.incorporation_country_code' => ['nullable', 'string', app(\App\Services\CountryCatalogue::class)->rule()],
'payload.declared_origin_country_code' => ['nullable', 'string', app(\App\Services\CountryCatalogue::class)->rule()],
'payload.other_details' => ['nullable', 'string', 'max:5000'],
'payload.full_name' => [$required, 'string', ...($submitting ? ['min:3'] : []), 'max:150'], 'payload.email' => [$required, $submitting ? 'email:rfc' : 'string', 'max:150'],
'payload.phone' => ['nullable', 'string', 'max:30'], 'payload.country' => [$required, 'string', 'max:100'],
'payload.phone' => ['nullable', 'string', 'max:30'], 'payload.country' => [$submitting ? 'required_without:payload.declared_origin_country_code' : 'nullable', 'nullable', 'string', 'max:100'],
'payload.organisation' => ['nullable', 'string', 'max:200'], 'payload.representative_role' => ['nullable', 'string', 'max:150'],
'payload.experience' => ['nullable', 'string', 'max:2000'], 'payload.investment_capacity' => ['nullable', 'string', 'max:1000'],
'payload.profile_id' => [$required, 'integer'], 'payload.sector' => [$required, 'string', 'max:150'],
@@ -257,10 +260,13 @@ class InvestmentSubmissionController extends Controller
$interest = $intake->receive(['email' => $p['email'], 'fullName' => $p['full_name'], 'message' => $p['objective'], 'submission_id' => $id], $id,
fn () => InterestRequest::create(['reference' => 'IR-'.now()->year.'-'.Str::ulid(), 'opportunity_id' => $opportunity?->id,
'opportunity_reference' => $opportunity?->reference, 'full_name' => $p['full_name'], 'email' => $p['email'], 'phone' => $p['phone'] ?? null,
'country' => $p['country'], 'investor_type' => empty($p['organisation']) ? 'individual' : 'empresa',
'country' => $p['country'] ?? 'Não informado', 'investor_type' => empty($p['organisation']) ? 'individual' : 'empresa',
'sector_interest' => $p['sector'], 'message' => $p['objective'], 'privacy_consent' => true, 'consented_at' => now(),
'status' => 'received', 'source_page' => '/investidor/manifestacoes/'.$id,
'metadata' => ['submission_id' => $id, 'portal_user_id' => $request->user()->id, 'dossier_id' => $row->dossier_id,
'declared_origin_country_code' => $p['declared_origin_country_code'] ?? null,
'residence_country_code' => $p['residence_country_code'] ?? null,
'incorporation_country_code' => $p['incorporation_country_code'] ?? null,
...($zoneOrigin ? ['zone_origin' => (array) $zoneOrigin] : [])]]));
$row->update(['payload' => $p, 'version' => $row->version + 1, 'interest_request_id' => $interest->id, 'status' => 'submitted']);
InvestmentSpecificNeed::where('submission_id', $id)->delete();
@@ -18,7 +18,7 @@ class InvestmentWorkspaceController extends Controller
{
public function profile(Request $request, InvestmentWorkspace $workspace): Response|JsonResponse
{
$data = ['account' => $workspace->account($request->user()->id), 'companies' => DB::table('investment_company_declarations')
$data = ['countries' => app(\App\Services\CountryCatalogue::class)->all(), 'account' => $workspace->account($request->user()->id), 'companies' => DB::table('investment_company_declarations')
->where('user_id', $request->user()->id)->orderByDesc('created_at')->get(['id', 'version', 'payload'])->map(function ($company) {
$company->payload = json_decode(Crypt::decryptString($company->payload), true, 512, JSON_THROW_ON_ERROR);
@@ -31,7 +31,8 @@ class InvestmentWorkspaceController extends Controller
public function saveProfile(Request $request): RedirectResponse|JsonResponse
{
$data = $request->validate(['version' => ['required', 'integer', 'min:0'],
'payload' => ['required', 'array:phone,country,sector,experience,investment_capacity'],
'payload' => ['required', 'array:phone,country,residence_country_code,sector,experience,investment_capacity'],
'payload.residence_country_code' => ['nullable', 'string', app(\App\Services\CountryCatalogue::class)->rule()],
'payload.phone' => ['nullable', 'string', 'max:30'], 'payload.country' => ['nullable', 'string', 'max:100'],
'payload.sector' => ['nullable', 'string', 'max:150'], 'payload.experience' => ['nullable', 'string', 'max:2000'],
'payload.investment_capacity' => ['nullable', 'string', 'max:1000']]);
@@ -53,8 +54,9 @@ class InvestmentWorkspaceController extends Controller
public function company(Request $request, ?string $id = null): RedirectResponse|JsonResponse
{
$data = $request->validate(['version' => ['required', 'integer', 'min:0'],
'payload' => ['required', 'array:name,country,registration,sector,role,description'],
'payload.name' => ['required', 'string', 'min:2', 'max:200'], 'payload.country' => ['required', 'string', 'max:100'],
'payload' => ['required', 'array:name,country,incorporation_country_code,registration,sector,role,description'],
'payload.incorporation_country_code' => ['nullable', 'string', app(\App\Services\CountryCatalogue::class)->rule()],
'payload.name' => ['required', 'string', 'min:2', 'max:200'], 'payload.country' => ['required_without:payload.incorporation_country_code', 'nullable', 'string', 'max:100'],
'payload.registration' => ['nullable', 'string', 'max:100'], 'payload.sector' => ['nullable', 'string', 'max:150'],
'payload.role' => ['required', 'string', 'max:150'], 'payload.description' => ['nullable', 'string', 'max:2000']]);
$result = DB::transaction(function () use ($request, $id, $data) {
@@ -23,7 +23,8 @@ class OpportunityResourceController extends Controller
{
public function installation(Request $request, OpportunityResourceInstallation $service): JsonResponse|Response
{
$preview = [...$service->preview(), 'actionUrl' => '/management/opportunity-resources-install'];
$preview = [...$service->preview(), 'imageProcessingAvailable' => app(OpportunityImageProcessor::class)->available(), 'actionUrl' => '/management/opportunity-resources-install'];
$preview['changes'] = [...$preview['changes'], 'Código reconciliado: catálogo ISO comum; residência e constituição distintas; origem declarada guardada no payload existente e estatísticas internas autorizadas. Não converte países históricos nem usa IP.', 'Seletores territoriais reutilizados com pesquisa; filtros contextuais do catálogo e fichas territoriais consultáveis no percurso do mapa. Zero geometrias não permite desenhar estruturas sem dados.', 'Não reinstala a gestão básica nem acrescenta associações territoriais. Revisão transversal, cadastro completo em todos os campos e percursos educativos continuam parciais; consultar a matriz por módulo.'];
return $request->expectsJson() ? response()->json($preview) : Inertia::render('management/opportunity-install', $preview);
}
@@ -41,6 +42,7 @@ class OpportunityResourceController extends Controller
$opportunity = Opportunity::withTrashed()->findOrFail($id);
abort_unless($service->installed(), 503, 'Recursos ainda não instalados; consulte o plano autenticado.');
$resources = [];
Inertia::share('imageProcessingAvailable', app(OpportunityImageProcessor::class)->available());
foreach (OpportunityResources::KINDS as $kind) {
$resources[$kind] = $service->listing($id, $kind);
}
@@ -48,8 +48,8 @@ class VirtualDeskController extends Controller
$data = ['cases' => $cases, 'queue' => $queue, 'notifications' => DB::table('desk_notifications')->where('user_id', $actor->id)->orderByDesc('id')->limit(100)->get(),
'legacyRequests' => DB::table('interest_requests')->where('metadata->portal_user_id', $actor->id)->whereNull('deleted_at')->orderByDesc('created_at')->get(['id', 'reference', 'status', 'created_at']),
'companies' => DB::table('investment_company_declarations')->where('user_id', $actor->id)->get(['id', 'payload'])->map(fn ($c) => ['id' => $c->id, 'name' => $desk->decode($c->payload)['name'] ?? 'Declaração empresarial']),
'submissions' => DB::table('investment_submissions')->where('user_id', $actor->id)->get(['id', 'status']),
'journeys' => DB::table('investor_journeys')->where('user_id', $actor->id)->whereNull('deleted_at')->get(['id', 'dossier_id', 'status']),
'submissions' => \App\Models\InvestmentSubmission::where('user_id', $actor->id)->get()->map(fn ($s) => ['id' => $s->id, 'status' => $s->status, 'name' => ($s->opportunity_id ? DB::table('opportunities')->where('id', $s->opportunity_id)->value('title') : null) ?? ($s->dossier_id ? DB::table('investment_dossiers')->where('id', $s->dossier_id)->value('title') : null) ?? ($s->payload['organisation'] ?? null)]),
'journeys' => DB::table('investor_journeys as j')->leftJoin('investment_dossiers as d', 'd.id', '=', 'j.dossier_id')->where('j.user_id', $actor->id)->whereNull('j.deleted_at')->get(['j.id', 'j.dossier_id', 'j.status', 'd.title as name']),
'simulationDrafts' => DB::table('simulation_form_drafts')->where('owner_user_id', $actor->id)->whereNull('deleted_at')->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))->get(['id', 'version_no']),
'guidanceTasks' => app(InvestorGuidance::class)->installed() ? DB::table('tasks as t')->join('desk_cases as c', 'c.id', '=', 't.desk_case_id')
->whereNull('t.deleted_at')->when(! $actor->can('manage-portal'), fn ($q) => $q->where(fn ($p) => $p->where('t.assignee_user_id', $actor->id)->orWhere(fn ($s) => $s->where('c.user_id', $actor->id)->whereNull('c.organization_id'))->orWhereIn('c.organization_id', $organizations)))
@@ -130,7 +130,7 @@ class VirtualDeskController extends Controller
}
}
$this->auditRead($request, 'desk_requests', $id);
$data = ['request' => [...(array) $row, 'payload' => $payload, 'submitted_snapshot' => $desk->decode($row->submitted_snapshot), 'editable_fields' => json_decode($row->editable_fields ?? '[]', true)],
$data = ['countries' => app(\App\Services\CountryCatalogue::class)->all(), 'request' => [...(array) $row, 'payload' => $payload, 'submitted_snapshot' => $desk->decode($row->submitted_snapshot), 'editable_fields' => json_decode($row->editable_fields ?? '[]', true)],
'case' => ['id' => $case->id, 'title' => $case->title], 'caseVersion' => $case->version, 'definition' => $desk->definition($row),
'serviceSource' => json_decode(DB::table('desk_service_versions')->where('id', $row->service_version_id)->value('source'), true),
'owner' => $desk->owns($request->user(), $case), 'manager' => $request->user()->can('manage-portal'), 'analyst' => (int) $row->analyst_id === $request->user()->id,