@@ -53,6 +53,7 @@ class InterestRequestController extends Controller
|
||||
'phone' => $validated['phone'] ?? null,
|
||||
'investor_type' => $validated['investorType'],
|
||||
'country' => $validated['country'],
|
||||
'metadata' => ['declared_origin_country_code' => $validated['declared_origin_country_code'] ?? null],
|
||||
'sector_interest' => $validated['sectorInterest'],
|
||||
'opportunity_reference' => $validated['opportunityReference'] ?? null,
|
||||
'investment_range' => $validated['investmentRange'] ?? null,
|
||||
|
||||
@@ -3,19 +3,25 @@
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\CountryCatalogue;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class ReferenceCatalogController extends Controller
|
||||
{
|
||||
private const CATALOGS = ['authorities', 'jurisdictions', 'economic-activities'];
|
||||
private const CATALOGS = ['authorities', 'jurisdictions', 'economic-activities', 'countries'];
|
||||
|
||||
public function index(Request $request, string $catalog): JsonResponse
|
||||
{
|
||||
$catalog = $this->validatedCatalog($catalog);
|
||||
if ($catalog === 'countries') {
|
||||
return $this->countries($request);
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'q' => ['nullable', 'string', 'max:100'],
|
||||
@@ -89,6 +95,21 @@ class ReferenceCatalogController extends Controller
|
||||
->header('ETag', 'W/"catalog-'.substr(hash('sha256', $catalog.'|'.$version->version_no.'|'.json_encode($data, JSON_THROW_ON_ERROR)), 0, 32).'"');
|
||||
}
|
||||
|
||||
private function countries(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate(['q' => 'nullable|string|max:100', 'page' => 'nullable|integer|min:1', 'per_page' => 'nullable|integer|min:1|max:100', 'parent_id' => 'prohibited', 'version' => 'prohibited']);
|
||||
$term = strtolower(Str::ascii(trim($data['q'] ?? '')));
|
||||
$items = collect(app(CountryCatalogue::class)->all())
|
||||
->filter(fn (array $country) => $term === '' || str_contains(strtolower(Str::ascii(implode(' ', $country))), $term))
|
||||
->values()->map(fn (array $country) => ['id' => $country['code'], 'value' => $country['code'], 'code' => $country['code'], 'name' => $country['name'], 'label' => $country['name']]);
|
||||
$page = (int) ($data['page'] ?? 1);
|
||||
$perPage = (int) ($data['per_page'] ?? 50);
|
||||
$result = new LengthAwarePaginator($items->forPage($page, $perPage)->values(), $items->count(), $perPage, $page, ['path' => $request->url(), 'query' => $request->query()]);
|
||||
|
||||
return response()->json([...$result->toArray(), 'meta' => ['catalog' => 'countries', 'source' => 'Local ISO 3166 catalogue', 'sha256' => hash_file('sha256', resource_path('data/iso3166.tab'))]])
|
||||
->header('Cache-Control', 'public, max-age=300');
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $data */
|
||||
private function authorities(string $versionId, array $data): Builder
|
||||
{
|
||||
@@ -171,7 +192,7 @@ class ReferenceCatalogController extends Controller
|
||||
});
|
||||
}
|
||||
|
||||
/** @return 'authorities'|'jurisdictions'|'economic-activities' */
|
||||
/** @return 'authorities'|'jurisdictions'|'economic-activities'|'countries' */
|
||||
private function validatedCatalog(string $catalog): string
|
||||
{
|
||||
abort_unless(in_array($catalog, self::CATALOGS, true), 404);
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
|
||||
use App\Models\InterestRequest;
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\Sector;
|
||||
use App\Services\InvestorPath;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
@@ -14,7 +15,16 @@ class DashboardController extends Controller
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
if (! $request->user()->can('manage-portal')) {
|
||||
return Inertia::render('dashboard', ['summary' => null]);
|
||||
$path = app(InvestorPath::class);
|
||||
$contexts = $path->contexts($request->user());
|
||||
$current = $contexts ? $path->view($request->user(), $contexts[0]['id']) : null;
|
||||
|
||||
return Inertia::render('dashboard', ['summary' => null, 'investorPath' => $path->available() ? [
|
||||
'contexts' => $contexts,
|
||||
'current' => $current ? ['id' => $current['case']['id'], 'next' => $current['next'],
|
||||
'pending' => count(array_filter($current['steps'], fn ($step) => ! in_array($step['state'], ['completed', 'approved', 'dispensed'], true))),
|
||||
'documents' => count($current['documents']), 'results' => count($current['results']), 'tasks' => count($current['tasks'])] : null,
|
||||
] : null]);
|
||||
}
|
||||
|
||||
$counts = InterestRequest::query()->selectRaw('status, count(*) as total')
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\ExistingBusiness;
|
||||
use App\Models\ExistingBusinessObject;
|
||||
use App\Models\Sector;
|
||||
use App\Services\BusinessFormSchema;
|
||||
use App\Services\CountryCatalogue;
|
||||
use App\Services\ExistingBusinessDrafts;
|
||||
use App\Services\ExistingBusinessIntake;
|
||||
use App\Services\ExistingBusinessObjects;
|
||||
@@ -15,9 +16,11 @@ use App\Services\Mne\BusinessRepository;
|
||||
use App\Services\Mne\CompatibilityShadow;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
@@ -35,7 +38,23 @@ class ExistingBusinessController extends Controller
|
||||
} else {
|
||||
$query->where('owner_user_id', $request->user()->id);
|
||||
}
|
||||
$props = ['records' => $query->orderByDesc('updated_at')->paginate(20)->through(fn ($row) => $this->record($row)),
|
||||
$filters = $request->validate(['country' => ['nullable', app(CountryCatalogue::class)->rule()], 'origin' => ['nullable', Rule::in([...array_column(app(CountryCatalogue::class)->all(), 'code'), 'unknown'])], 'page' => ['nullable', 'integer', 'min:1']]);
|
||||
$query->orderByDesc('updated_at');
|
||||
if (! empty($filters['country']) || ! empty($filters['origin'])) {
|
||||
// The private payload is encrypted: filter only the already-authorized collection.
|
||||
$matches = $query->get()->filter(function ($business) use ($filters) {
|
||||
$payload = $business->payload;
|
||||
$origin = $payload['declared_origin_country_code'] ?? null;
|
||||
$validOrigin = in_array($origin, array_column(app(CountryCatalogue::class)->all(), 'code'), true) ? $origin : 'unknown';
|
||||
|
||||
return (empty($filters['country']) || ($payload['country'] ?? null) === $filters['country']) && (empty($filters['origin']) || $validOrigin === $filters['origin']);
|
||||
})->values();
|
||||
$page = (int) ($filters['page'] ?? 1);
|
||||
$records = new LengthAwarePaginator($matches->forPage($page, 20)->values(), $matches->count(), 20, $page, ['path' => $request->url(), 'query' => $request->query()]);
|
||||
} else {
|
||||
$records = $query->paginate(20)->withQueryString();
|
||||
}
|
||||
$props = ['records' => $records->through(fn ($row) => $this->record($row)), 'filters' => $filters, 'countries' => app(CountryCatalogue::class)->all(),
|
||||
'management' => $management, 'modalities' => ExistingBusinessDrafts::MODALITIES];
|
||||
|
||||
return $request->expectsJson() ? response()->json($props) : Inertia::render('investment/businesses', $props);
|
||||
@@ -46,7 +65,7 @@ class ExistingBusinessController extends Controller
|
||||
$business = app(BusinessRepository::class)->find($id);
|
||||
Gate::authorize('view', $business);
|
||||
app(CompatibilityShadow::class)->schedule($id);
|
||||
$props = ['sectors' => Sector::where('is_active', true)->orderBy('name')->get(['id', 'name']), 'formSchema' => BusinessFormSchema::fields(), 'record' => $this->record($business), 'canEdit' => $request->user()->can('update', $business),
|
||||
$props = ['countries' => app(CountryCatalogue::class)->all(), 'sectors' => Sector::where('is_active', true)->orderBy('name')->get(['id', 'name']), 'formSchema' => BusinessFormSchema::fields(), 'record' => $this->record($business), 'canEdit' => $request->user()->can('update', $business),
|
||||
'owner' => $request->user()->can('owns', $business), 'canReview' => $request->user()->can('review', $business),
|
||||
'canTriage' => $request->user()->can('triage', $business),
|
||||
'canApproveLevelOne' => $request->user()->can('approveLevelOne', $business),
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Services\InvestmentZones;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
@@ -130,7 +131,7 @@ class InvestmentDiscoveryController extends Controller
|
||||
{
|
||||
$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 !== []);
|
||||
$opportunityOnlyFilters = array_filter(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)) {
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Models\InterestRequest;
|
||||
use App\Models\InvestmentSpecificNeed;
|
||||
use App\Models\InvestmentSubmission;
|
||||
use App\Models\Opportunity;
|
||||
use App\Services\CountryCatalogue;
|
||||
use App\Services\InterestSubmission;
|
||||
use App\Services\InvestmentCatalogue;
|
||||
use App\Services\InvestmentWorkspace;
|
||||
@@ -111,7 +112,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 = ['countries' => app(\App\Services\CountryCatalogue::class)->all(), 'submission' => $row->only(['id', 'payload', 'version', 'status', 'opportunity_id', 'dossier_id']), 'zone_origin' => $zoneOrigin,
|
||||
$data = ['countries' => app(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(),
|
||||
@@ -145,9 +146,9 @@ class InvestmentSubmissionController extends Controller
|
||||
$required = $submitting ? 'required' : 'nullable';
|
||||
$data = $request->validate(['version' => ['required', 'integer', 'min:1'],
|
||||
'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.residence_country_code' => ['nullable', 'string', app(CountryCatalogue::class)->rule()],
|
||||
'payload.incorporation_country_code' => ['nullable', 'string', app(CountryCatalogue::class)->rule()],
|
||||
'payload.declared_origin_country_code' => ['nullable', 'string', app(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' => [$submitting ? 'required_without:payload.declared_origin_country_code' : 'nullable', 'nullable', 'string', 'max:100'],
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\InvestmentZones;
|
||||
use App\Services\TerritoryGeometry;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -10,6 +11,17 @@ use Illuminate\Validation\ValidationException;
|
||||
|
||||
class InvestmentTerritoryController extends Controller
|
||||
{
|
||||
public function geometries(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate(['territory_id' => ['required', 'uuid']]);
|
||||
$management = $request->is('management/*');
|
||||
if ($management) {
|
||||
abort_unless($request->user()?->can('manage-portal'), 403);
|
||||
}
|
||||
|
||||
return response()->json(app(TerritoryGeometry::class)->context($data['territory_id'], $management));
|
||||
}
|
||||
|
||||
private const TABLES = ['opportunities' => 'opportunities', 'projects' => 'projects', 'clusters' => 'economic_clusters', 'corridors' => 'development_corridors', 'zones' => 'investment_zones'];
|
||||
|
||||
public function subjects(Request $request): JsonResponse
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\InvestmentSubmission;
|
||||
use App\Models\User;
|
||||
use App\Services\CountryCatalogue;
|
||||
use App\Services\InvestmentWorkspace;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -18,7 +19,7 @@ class InvestmentWorkspaceController extends Controller
|
||||
{
|
||||
public function profile(Request $request, InvestmentWorkspace $workspace): Response|JsonResponse
|
||||
{
|
||||
$data = ['countries' => app(\App\Services\CountryCatalogue::class)->all(), 'account' => $workspace->account($request->user()->id), 'companies' => DB::table('investment_company_declarations')
|
||||
$data = ['countries' => app(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);
|
||||
|
||||
@@ -32,7 +33,7 @@ class InvestmentWorkspaceController extends Controller
|
||||
{
|
||||
$data = $request->validate(['version' => ['required', 'integer', 'min:0'],
|
||||
'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.residence_country_code' => ['nullable', 'string', app(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']]);
|
||||
@@ -55,7 +56,7 @@ class InvestmentWorkspaceController extends Controller
|
||||
{
|
||||
$data = $request->validate(['version' => ['required', 'integer', 'min:0'],
|
||||
'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.incorporation_country_code' => ['nullable', 'string', app(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']]);
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\Ai\EvidenceRepository;
|
||||
use App\Services\Ai\InvestorGuidance;
|
||||
use App\Services\InvestorPath;
|
||||
use App\Services\VirtualDesk;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -18,7 +19,10 @@ class InvestorGuidanceController extends Controller
|
||||
$actor = $request->user();
|
||||
$organizations = DB::table('memberships')->where('user_id', $actor->id)->whereNull('deleted_at')->pluck('organization_id');
|
||||
$cases = DB::table('desk_cases')->whereNull('closed_at')->where(fn ($q) => $q->where(fn ($p) => $p->where('user_id', $actor->id)->whereNull('organization_id'))->orWhereIn('organization_id', $organizations))
|
||||
->orderByDesc('updated_at')->get(['id', 'title']);
|
||||
->orderByDesc('updated_at')->get(['id', 'title', 'journey_id'])->map(function ($case) {
|
||||
return ['id' => $case->id, 'title' => $case->title, 'path_supported' => app(InvestorPath::class)->available()
|
||||
&& $case->journey_id && DB::table('investor_journeys')->where('id', $case->journey_id)->whereNotNull('path_definition_id')->exists()];
|
||||
});
|
||||
|
||||
return response()->json(['cases' => $cases]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\InvestorPath;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class InvestorPathController extends Controller
|
||||
{
|
||||
public function index(Request $request, InvestorPath $path): JsonResponse|Response
|
||||
{
|
||||
abort_unless($path->available(), 404);
|
||||
$data = ['contexts' => $path->contexts($request->user())];
|
||||
|
||||
return $request->expectsJson() ? response()->json($data) : Inertia::render('investor-path/index', $data);
|
||||
}
|
||||
|
||||
public function create(Request $request, InvestorPath $path): JsonResponse|RedirectResponse
|
||||
{
|
||||
$id = $path->create($request->user(), $request->all());
|
||||
|
||||
return $request->expectsJson() ? response()->json(['id' => $id], 201) : redirect('/investimentos/'.$id)->with('success', 'Investimento guardado no seu dossier.');
|
||||
}
|
||||
|
||||
public function show(Request $request, string $id, InvestorPath $path): JsonResponse|Response
|
||||
{
|
||||
$data = $path->view($request->user(), $id);
|
||||
|
||||
return $request->expectsJson() ? response()->json($data) : Inertia::render('investor-path/show', $data);
|
||||
}
|
||||
|
||||
public function facts(Request $request, string $id, InvestorPath $path): JsonResponse|RedirectResponse
|
||||
{
|
||||
abort_unless($path->available(), 404);
|
||||
$path->facts($request->user(), $id, $request->all());
|
||||
|
||||
return $this->saved($request, 'Aplicabilidade registada; o histórico foi preservado.');
|
||||
}
|
||||
|
||||
public function stage(Request $request, string $id, string $code, InvestorPath $path): JsonResponse|RedirectResponse
|
||||
{
|
||||
abort_unless($path->available(), 404);
|
||||
$path->stage($request->user(), $id, $code, $request->all());
|
||||
|
||||
return $this->saved($request, 'Etapa e fundamento registados.');
|
||||
}
|
||||
|
||||
public function tool(Request $request, string $id, InvestorPath $path): JsonResponse
|
||||
{
|
||||
abort_unless($path->available(), 404);
|
||||
|
||||
return response()->json(['result' => $path->returnTool($request, $id, $request->all())]);
|
||||
}
|
||||
|
||||
public function task(Request $request, string $id, InvestorPath $path): JsonResponse|RedirectResponse
|
||||
{
|
||||
abort_unless($path->available(), 404);
|
||||
$task = $path->task($request->user(), $id, $request->all());
|
||||
|
||||
return $request->expectsJson() ? response()->json(['id' => $task], 201) : back()->with('success', 'Tarefa assumida no acompanhamento interno.');
|
||||
}
|
||||
|
||||
private function saved(Request $request, string $message): JsonResponse|RedirectResponse
|
||||
{
|
||||
return $request->expectsJson() ? response()->json(['message' => $message]) : back()->with('success', $message);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
|
||||
use App\Models\InterestRequest;
|
||||
use App\Models\Investor;
|
||||
use App\Models\InvestorJourney;
|
||||
use App\Services\CountryCatalogue;
|
||||
use App\Services\InterestSubmission;
|
||||
use App\Services\PortalAudit;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -23,10 +24,12 @@ class InvestorWorkspaceController extends Controller
|
||||
$interest = $journey->interest_request_id ? InterestRequest::find($journey->interest_request_id) : null;
|
||||
|
||||
return ['id' => $journey->id, 'dossier_id' => $dossier?->id, 'title' => $dossier->title ?? 'Ficha temporariamente indisponível',
|
||||
'status' => $journey->status, 'reference' => $interest?->reference, 'request_status' => $interest?->status];
|
||||
'status' => $journey->status, 'reference' => $interest?->reference, 'request_status' => $interest?->status,
|
||||
'country' => $interest?->country,
|
||||
'declared_origin' => app(CountryCatalogue::class)->describe($interest?->metadata['declared_origin_country_code'] ?? null)];
|
||||
});
|
||||
|
||||
return Inertia::render('portfolio/workspace', ['journeys' => $journeys]);
|
||||
return Inertia::render('portfolio/workspace', ['journeys' => $journeys, 'countries' => app(CountryCatalogue::class)->all()]);
|
||||
}
|
||||
|
||||
public function start(Request $request, string $id): RedirectResponse
|
||||
@@ -57,7 +60,8 @@ class InvestorWorkspaceController extends Controller
|
||||
|
||||
public function submit(Request $request, string $id, InterestSubmission $submission): RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['message' => ['required', 'string', 'min:20', 'max:5000'], 'country' => ['required', 'string', 'max:100'],
|
||||
$data = $request->validate(['message' => ['required', 'string', 'min:20', 'max:5000'], 'country' => ['required', 'string', 'max:100', app(CountryCatalogue::class)->legacyCountryRule()],
|
||||
'declared_origin_country_code' => ['nullable', app(CountryCatalogue::class)->rule()],
|
||||
'privacy_consent' => ['accepted'], 'role' => ['required', 'in:investir,financiar,fornecer,operar,parceria'],
|
||||
'horizon' => ['required', 'string', 'max:100']]);
|
||||
DB::transaction(function () use ($request, $id, $data, $submission) {
|
||||
@@ -74,6 +78,7 @@ class InvestorWorkspaceController extends Controller
|
||||
'sector_interest' => Str::limit($dossier->content['sector'] ?? $dossier->title, 150, ''), 'message' => $data['message'],
|
||||
'privacy_consent' => true, 'consented_at' => now(), 'status' => 'received', 'source_page' => '/carteira/'.$dossier->id,
|
||||
'metadata' => ['journey_id' => $journey->id, 'dossier_id' => $dossier->id, 'origin_dossier_id' => $journey->origin_dossier_id,
|
||||
'declared_origin_country_code' => $data['declared_origin_country_code'] ?? null,
|
||||
'project_id' => $dossier->project_id ?? $origin?->project_id,
|
||||
'cluster_id' => $dossier->cluster_id ?? $origin?->cluster_id, 'corridor_id' => $dossier->corridor_id ?? $origin?->corridor_id,
|
||||
'role' => $data['role'], 'horizon' => $data['horizon'], 'purpose' => 'Acompanhamento da manifestação de interesse'],
|
||||
|
||||
@@ -8,7 +8,9 @@ use App\Http\Requests\StoreLandAssistanceCaseRequest;
|
||||
use App\Jobs\ScanDocumentForMalware;
|
||||
use App\Models\InterestRequest;
|
||||
use App\Models\LandAssistanceCase;
|
||||
use App\Services\CountryCatalogue;
|
||||
use App\Services\InterestSubmission;
|
||||
use App\Services\InvestmentGeography;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -57,6 +59,7 @@ class LandAssistanceController extends Controller
|
||||
'user' => ['name' => $request->user()->name, 'email' => $request->user()->email],
|
||||
'disclaimer' => self::DISCLAIMER,
|
||||
'modelVersion' => 'land-assistance-v1',
|
||||
'countries' => app(CountryCatalogue::class)->all(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -70,6 +73,7 @@ class LandAssistanceController extends Controller
|
||||
'user' => ['name' => $request->user()->name, 'email' => $request->user()->email],
|
||||
'disclaimer' => self::DISCLAIMER,
|
||||
'modelVersion' => 'land-assistance-v1',
|
||||
'countries' => app(CountryCatalogue::class)->all(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -102,6 +106,7 @@ class LandAssistanceController extends Controller
|
||||
{
|
||||
$case = $this->owned($request, $id);
|
||||
$data = [
|
||||
...$this->presentation($case),
|
||||
'case' => $case->only(['id', 'reference', 'status', 'version', 'payload', 'updated_at']),
|
||||
'documents' => DB::table('land_assistance_documents as link')->join('documents as document', 'document.id', '=', 'link.document_id')
|
||||
->where('link.case_id', $id)->whereIn('document.status', ['active', 'quarantined'])->whereNull('document.deleted_at')
|
||||
@@ -162,6 +167,7 @@ class LandAssistanceController extends Controller
|
||||
'sectorInterest' => 'Terra e DUAT', 'message' => mb_substr($message, 0, 2000),
|
||||
'privacyConsent' => true, 'portal_user_id' => $request->user()->id,
|
||||
'land_case_id' => $case->id, 'land_case_reference' => $case->reference,
|
||||
'declared_origin_country_code' => $payload['declared_origin_country_code'] ?? null,
|
||||
], $case->crm_submission_key, fn () => InterestRequest::create([
|
||||
'reference' => 'AP-'.now()->year.'-'.Str::ulid(), 'full_name' => $payload['full_name'],
|
||||
'email' => $payload['email'], 'phone' => $payload['phone'] ?? null,
|
||||
@@ -169,7 +175,8 @@ class LandAssistanceController extends Controller
|
||||
'sector_interest' => 'Terra e DUAT', 'message' => mb_substr($message, 0, 2000),
|
||||
'privacy_consent' => true, 'consented_at' => now(), 'status' => 'received', 'source_page' => '/terra-duat',
|
||||
'metadata' => ['kind' => 'support', 'portal_user_id' => $request->user()->id,
|
||||
'land_case_id' => $case->id, 'land_case_reference' => $case->reference],
|
||||
'land_case_id' => $case->id, 'land_case_reference' => $case->reference,
|
||||
'declared_origin_country_code' => $payload['declared_origin_country_code'] ?? null],
|
||||
]));
|
||||
$case->forceFill([
|
||||
'payload' => $payload,
|
||||
@@ -304,6 +311,22 @@ class LandAssistanceController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function presentation(LandAssistanceCase $case, bool $management = false): array
|
||||
{
|
||||
$payload = $case->payload;
|
||||
$countries = app(CountryCatalogue::class);
|
||||
$country = $countries->describe($payload['country'] ?? null);
|
||||
$paths = [];
|
||||
foreach ($payload['parcels'] ?? [] as $parcel) {
|
||||
$id = $parcel['locality_id'] ?? $parcel['admin_post_id'] ?? $parcel['district_id'] ?? $parcel['province_id'] ?? null;
|
||||
$paths[] = app(InvestmentGeography::class)->path($id ?: null, $management);
|
||||
}
|
||||
|
||||
return ['countryName' => $country['code'] !== null ? $country['name'] : ($payload['country'] ?? 'Não informado'),
|
||||
'declaredOrigin' => $countries->describe($payload['declared_origin_country_code'] ?? null), 'territoryPaths' => $paths];
|
||||
}
|
||||
|
||||
public function manage(Request $request): Response
|
||||
{
|
||||
$cases = LandAssistanceCase::query()->latest('updated_at')->paginate(30)->through(fn (LandAssistanceCase $case): array => [
|
||||
@@ -321,6 +344,7 @@ class LandAssistanceController extends Controller
|
||||
$events = DB::table('land_assistance_events')->where('case_id', $id)->orderBy('created_at')->get(['action', 'version', 'metadata', 'created_at']);
|
||||
|
||||
return Inertia::render('land-assistance/case', [
|
||||
...$this->presentation($case, true),
|
||||
'case' => $case->only(['id', 'reference', 'status', 'version', 'payload', 'updated_at']),
|
||||
'documents' => DB::table('land_assistance_documents as link')->join('documents as document', 'document.id', '=', 'link.document_id')
|
||||
->where('link.case_id', $id)->whereIn('document.status', ['active', 'quarantined'])->whereNull('document.deleted_at')->get(['link.id', 'link.category', 'document.file_name', 'document.status']),
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\Sector;
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Services\OpportunityManagement;
|
||||
use App\Services\OpportunityManagementInstallation;
|
||||
use App\Services\OpportunityMaturity;
|
||||
use App\Services\OpportunityResources;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -67,10 +68,11 @@ class OpportunityManagementController extends Controller
|
||||
return app(PortalManagementController::class)->edit('opportunities', $id);
|
||||
}
|
||||
$record = $id ? Opportunity::withTrashed()->findOrFail($id) : null;
|
||||
$values = $record ? ($record->management_draft ? json_decode($record->management_draft, true) : $record->only(OpportunityManagement::FIELDS)) : [];
|
||||
$values = $record ? $service->values($record) : [];
|
||||
$classification = $id ? DB::table('audit_events')->where('source_system', 'opportunity-origin-review')->where('after_data->opportunity_id', $id)->orderByDesc('occurred_at')->orderByDesc('id')->value('after_data') : null;
|
||||
|
||||
return Inertia::render('management/opportunity-draft', ['resourcesInstalled' => app(OpportunityResources::class)->installed(), 'record' => $record, 'values' => $values, 'etag' => $record ? $service->etag($record) : null, 'readOnly' => str_ends_with($request->path(), '/view'), 'closedVersion' => $record ? $service->closed($record) : false, 'classification' => $classification ? json_decode($classification, true)['classification'] : 'undetermined', 'sectors' => Sector::where('is_active', true)->orderBy('name')->get(['id', 'name']), 'categories' => OpportunityManagement::CATEGORIES,
|
||||
'maturityCatalogue' => ['version' => OpportunityMaturity::VERSION, 'stages' => OpportunityMaturity::STAGES, 'legacyVersion' => OpportunityMaturity::LEGACY, 'legacyLabels' => OpportunityMaturity::LEGACY_LABELS],
|
||||
'territoryPath' => app(InvestmentGeography::class)->path($values['primary_territory_id'] ?? null, true),
|
||||
'history' => $id ? DB::table('audit_events')->where('source_system', 'opportunity-management')->where('after_data->record_id', $id)->orderByDesc('occurred_at')->get(['id', 'action', 'actor_user_id', 'occurred_at']) : []]);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class OpportunityResourceController extends Controller
|
||||
$image->derivativePreviews = [];
|
||||
foreach ($image->payload['derivatives'] ?? [] as $derived) {
|
||||
if (DB::table('documents')->where('id', $derived['document_id'])->where('status', 'active')->where('malware_scan_status', 'clean')->whereColumn('sha256', 'malware_scanned_sha256')->exists()) {
|
||||
$image->derivativePreviews[] = [...$derived, 'url' => URL::temporarySignedRoute('opportunity-image-preview', now()->addMinutes(10), ['id' => $id, 'resourceId' => $image->id, 'documentId' => $derived['document_id']])];
|
||||
$image->derivativePreviews[] = [...$derived, 'url' => URL::temporarySignedRoute('management.opportunity-image-preview', now()->addMinutes(10), ['id' => $id, 'resourceId' => $image->id, 'documentId' => $derived['document_id']])];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ use App\Models\InvestmentSubmission;
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\Sector;
|
||||
use App\Models\User;
|
||||
use App\Services\CountryCatalogue;
|
||||
use App\Services\DeclaredOriginStatistics;
|
||||
use App\Services\InterestWorkflow;
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Services\InvestmentWorkspace;
|
||||
@@ -56,10 +58,22 @@ class PortalManagementController extends Controller
|
||||
'q' => ['nullable', 'string', 'max:150'],
|
||||
'status' => ['nullable', Rule::in(self::STATES)],
|
||||
'follow_up' => ['nullable', Rule::in(['overdue', 'upcoming', 'unassigned'])],
|
||||
'country' => ['nullable', app(CountryCatalogue::class)->rule()],
|
||||
'origin' => ['nullable', Rule::in([...array_column(app(CountryCatalogue::class)->all(), 'code'), 'unknown'])],
|
||||
]);
|
||||
$query = InterestRequest::query()->with('assignee:id,name')->select([
|
||||
'id', 'reference', 'full_name', 'email', 'sector_interest', 'existing_business_id', 'modality', 'status', 'assigned_to', 'next_action', 'next_action_due_at', 'created_at',
|
||||
'id', 'reference', 'full_name', 'email', 'country', 'metadata', 'sector_interest', 'existing_business_id', 'modality', 'status', 'assigned_to', 'next_action', 'next_action_due_at', 'created_at',
|
||||
]);
|
||||
if ($country = $filters['country'] ?? null) {
|
||||
$query->where('country', $country);
|
||||
}
|
||||
if ($origin = $filters['origin'] ?? null) {
|
||||
if ($origin === 'unknown') {
|
||||
$query->where(fn ($q) => $q->whereNull('metadata->declared_origin_country_code')->orWhereNotIn('metadata->declared_origin_country_code', array_column(app(CountryCatalogue::class)->all(), 'code')));
|
||||
} else {
|
||||
$query->where('metadata->declared_origin_country_code', $origin);
|
||||
}
|
||||
}
|
||||
if ($term = $filters['q'] ?? null) {
|
||||
$query->where(fn ($q) => $q->whereLike('reference', "%{$term}%")
|
||||
->orWhereLike('full_name', "%{$term}%")->orWhereLike('email', "%{$term}%"));
|
||||
@@ -78,8 +92,18 @@ class PortalManagementController extends Controller
|
||||
}
|
||||
|
||||
return Inertia::render('management/interests', [
|
||||
'requests' => $query->orderByDesc('id')->paginate(20)->withQueryString(),
|
||||
'requests' => $query->orderByDesc('id')->paginate(20)->withQueryString()->through(function ($row) {
|
||||
$result = $row->toArray();
|
||||
$result['declaredOrigin'] = app(CountryCatalogue::class)->describe($row->metadata['declared_origin_country_code'] ?? null);
|
||||
$country = app(CountryCatalogue::class)->describe($row->country);
|
||||
$result['countryName'] = $country['code'] !== null ? $country['name'] : $row->country;
|
||||
unset($result['metadata']);
|
||||
|
||||
return $result;
|
||||
}),
|
||||
'filters' => $filters,
|
||||
'countries' => app(CountryCatalogue::class)->all(),
|
||||
'originStatistics' => app(DeclaredOriginStatistics::class)->forManager($request->user(), $filters['origin'] ?? null),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -97,6 +121,7 @@ class PortalManagementController extends Controller
|
||||
->orderBy('created_at')->get(['purpose', 'policy_version', 'status', 'granted_at', 'withdrawn_at']);
|
||||
|
||||
return Inertia::render('management/interest', [
|
||||
'declaredOrigin' => app(CountryCatalogue::class)->describe($interest->metadata['declared_origin_country_code'] ?? null),
|
||||
'interest' => $interest->only([
|
||||
'id', 'reference', 'full_name', 'email', 'phone', 'investor_type', 'country',
|
||||
'sector_interest', 'opportunity_reference', 'investment_range', 'message',
|
||||
|
||||
@@ -5,9 +5,11 @@ namespace App\Http\Controllers;
|
||||
use App\Jobs\ScanDocumentForMalware;
|
||||
use App\Models\InterestRequest;
|
||||
use App\Models\Sector;
|
||||
use App\Services\CountryCatalogue;
|
||||
use App\Services\IdempotencyConflict;
|
||||
use App\Services\InterestSubmission;
|
||||
use App\Services\Malware\MalwareScanner;
|
||||
use App\Services\VerifiedDocumentBinary;
|
||||
use App\Services\VirtualDesk;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\QueryException;
|
||||
@@ -37,7 +39,7 @@ class PortalServicesController extends Controller
|
||||
$essentialResources = collect();
|
||||
}
|
||||
|
||||
return Inertia::render('PortalInvestidor', ['essentialResources' => $essentialResources]);
|
||||
return Inertia::render('PortalInvestidor', ['essentialResources' => $essentialResources, 'countries' => app(CountryCatalogue::class)->all()]);
|
||||
}
|
||||
|
||||
private function resources(): Builder
|
||||
@@ -64,7 +66,7 @@ class PortalServicesController extends Controller
|
||||
'stage' => ['nullable', 'array', 'max:10'], 'stage.*' => ['string', 'max:80'],
|
||||
'module' => ['nullable', 'array', 'max:10'], 'module.*' => ['string', 'max:80'],
|
||||
]);
|
||||
$props = ['section' => $page, 'filters' => $filters];
|
||||
$props = ['section' => $page, 'filters' => $filters, 'countries' => app(CountryCatalogue::class)->all()];
|
||||
if ($page === 'sectors') {
|
||||
$props['sectors'] = Sector::where('is_active', true)->withCount(['opportunities' => fn ($q) => $q->published()])->orderBy('sort_order')->get(['id', 'name', 'slug', 'short_description']);
|
||||
}
|
||||
@@ -222,7 +224,8 @@ class PortalServicesController extends Controller
|
||||
$data = $request->validate([
|
||||
'kind' => ['required', 'in:support,investor,company,partnership'],
|
||||
'fullName' => ['required', 'string', 'min:3', 'max:150'], 'email' => ['required', 'email:rfc', 'max:150'],
|
||||
'phone' => ['nullable', 'string', 'max:30'], 'country' => ['required', 'string', 'max:100'],
|
||||
'phone' => ['nullable', 'string', 'max:30'], 'country' => ['required', 'string', 'max:100', app(CountryCatalogue::class)->legacyCountryRule()],
|
||||
'declared_origin_country_code' => ['nullable', app(CountryCatalogue::class)->rule()],
|
||||
'company' => ['nullable', 'required_if:kind,company', 'string', 'max:150'],
|
||||
'sectorInterest' => ['required', 'string', 'max:150'], 'message' => ['required', 'string', 'min:20', 'max:2000'],
|
||||
'privacyConsent' => ['required', 'accepted'], 'submission_key' => ['required', 'uuid'],
|
||||
@@ -240,7 +243,8 @@ class PortalServicesController extends Controller
|
||||
'sector_interest' => $data['sectorInterest'], 'message' => $data['message'],
|
||||
'privacy_consent' => true, 'consented_at' => now(), 'status' => 'received', 'source_page' => '/apoio',
|
||||
'metadata' => ['kind' => $data['kind'], 'company' => $data['company'] ?? null,
|
||||
'portal_user_id' => $data['portal_user_id'], 'partnership_id' => $data['partnership_id'] ?? null],
|
||||
'portal_user_id' => $data['portal_user_id'], 'partnership_id' => $data['partnership_id'] ?? null,
|
||||
'declared_origin_country_code' => $data['declared_origin_country_code'] ?? null],
|
||||
]));
|
||||
} catch (IdempotencyConflict) {
|
||||
abort(409, 'Este envio já foi usado com dados diferentes. Recarregue o formulário.');
|
||||
@@ -260,17 +264,12 @@ class PortalServicesController extends Controller
|
||||
return back()->with('success', 'Proposta recebida para publicação. Acompanhe no Balcão Virtual.');
|
||||
}
|
||||
|
||||
public function download(int $id): BinaryFileResponse
|
||||
public function download(int $id): BinaryFileResponse|\Illuminate\Http\Response
|
||||
{
|
||||
$resource = $this->resources()->where('r.id', $id)->first(['d.storage_key', 'd.file_name', 'd.sha256']);
|
||||
$resource = $this->resources()->where('r.id', $id)->first(['d.*']);
|
||||
abort_unless($resource !== null, 404);
|
||||
$root = realpath(Storage::disk('local')->path(''));
|
||||
$path = realpath(Storage::disk('local')->path($resource->storage_key));
|
||||
abort_unless($root && $path && str_starts_with(strtolower($path), strtolower($root.DIRECTORY_SEPARATOR)) && is_file($path), 404);
|
||||
$checksum = hash_file('sha256', $path);
|
||||
abort_unless($checksum !== false && hash_equals($resource->sha256, $checksum), 404);
|
||||
|
||||
return response()->download($path, basename($resource->file_name), ['X-Content-Type-Options' => 'nosniff', 'Cache-Control' => 'private, no-store']);
|
||||
return app(VerifiedDocumentBinary::class)->respond($resource);
|
||||
}
|
||||
|
||||
public function showDocument(Request $request, int $id): Response
|
||||
@@ -306,42 +305,21 @@ class PortalServicesController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function viewDocument(int $id): BinaryFileResponse
|
||||
public function viewDocument(int $id): BinaryFileResponse|\Illuminate\Http\Response
|
||||
{
|
||||
$document = $this->resources()->where('r.id', $id)->first([
|
||||
'd.storage_key', 'd.file_name', 'd.sha256', 'd.media_type',
|
||||
]);
|
||||
$document = $this->resources()->where('r.id', $id)->first(['d.*']);
|
||||
abort_unless($document && in_array($document->media_type, ['application/pdf', 'image/jpeg', 'image/png', 'image/webp'], true), 404);
|
||||
$root = realpath(Storage::disk('local')->path(''));
|
||||
$path = realpath(Storage::disk('local')->path($document->storage_key));
|
||||
abort_unless($root && $path && str_starts_with(strtolower($path), strtolower($root.DIRECTORY_SEPARATOR)) && is_file($path), 404);
|
||||
$checksum = hash_file('sha256', $path);
|
||||
abort_unless($checksum !== false && hash_equals($document->sha256, $checksum), 404);
|
||||
|
||||
return response()->file($path, [
|
||||
'Content-Type' => $document->media_type,
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
'Content-Security-Policy' => "sandbox; default-src 'none'; style-src 'unsafe-inline'",
|
||||
'Cache-Control' => 'private, no-store',
|
||||
])->setContentDisposition('inline', basename($document->file_name));
|
||||
return app(VerifiedDocumentBinary::class)->respond($document, true);
|
||||
}
|
||||
|
||||
public function downloadForManager(string $id): BinaryFileResponse
|
||||
public function downloadForManager(string $id): BinaryFileResponse|\Illuminate\Http\Response
|
||||
{
|
||||
$document = DB::table('documents')->where('id', $id)->whereNull('deleted_at')->first([
|
||||
'storage_key', 'file_name', 'sha256', 'status', 'malware_scan_status', 'malware_scanned_sha256',
|
||||
]);
|
||||
$document = DB::table('documents')->where('id', $id)->whereNull('deleted_at')->first();
|
||||
abort_unless($document && $document->status === 'active' && $document->malware_scan_status === 'clean'
|
||||
&& hash_equals(trim($document->sha256), trim((string) $document->malware_scanned_sha256)), 404);
|
||||
$root = realpath(Storage::disk('local')->path(''));
|
||||
$path = realpath(Storage::disk('local')->path($document->storage_key));
|
||||
abort_unless($root && $path && str_starts_with(strtolower($path), strtolower($root.DIRECTORY_SEPARATOR)) && is_file($path), 404);
|
||||
$checksum = hash_file('sha256', $path);
|
||||
abort_unless($checksum !== false && hash_equals($document->sha256, $checksum), 404);
|
||||
|
||||
return response()->download($path, basename($document->file_name), [
|
||||
'X-Content-Type-Options' => 'nosniff', 'Cache-Control' => 'private, no-store',
|
||||
]);
|
||||
return app(VerifiedDocumentBinary::class)->respond($document);
|
||||
}
|
||||
|
||||
public function manage(MalwareScanner $scanner): Response
|
||||
@@ -437,8 +415,8 @@ class PortalServicesController extends Controller
|
||||
$id = DB::table('portal_resources')->insertGetId([...$fields, 'approved_by' => null, 'published' => false, 'created_at' => now(), 'updated_at' => now()]);
|
||||
$this->syncResourceLinks($id, $data['associations'] ?? []);
|
||||
$this->recordResourceRevision($id, 'created', $request->user()->id, 1);
|
||||
DB::table('documents')->where('id', $document->id)->update(['issuer' => $data['issuer'] ?: $document->issuer,
|
||||
'issued_on' => $data['document_date'] ?: $document->issued_on, 'rights_status' => $data['rights_status'],
|
||||
DB::table('documents')->where('id', $document->id)->update(['issuer' => ($data['issuer'] ?? null) ?: $document->issuer,
|
||||
'issued_on' => ($data['document_date'] ?? null) ?: $document->issued_on, 'rights_status' => $data['rights_status'],
|
||||
'data_classification' => $data['data_classification'] ?? $document->data_classification, 'updated_at' => now()]);
|
||||
$this->audit($request, $duplicate ? 'resource_registered_possible_duplicate' : 'resource_registered', 'portal_resources', $id);
|
||||
|
||||
@@ -475,8 +453,8 @@ class PortalServicesController extends Controller
|
||||
DB::table('portal_resource_links')->where('resource_id', $id)->delete();
|
||||
$this->syncResourceLinks($id, $data['associations'] ?? []);
|
||||
$this->recordResourceRevision($id, 'metadata_updated', $request->user()->id, $expectedVersion + 1);
|
||||
DB::table('documents')->where('id', $document->id)->update(['issuer' => $data['issuer'] ?: $document->issuer,
|
||||
'issued_on' => $data['document_date'] ?: $document->issued_on, 'rights_status' => $data['rights_status'],
|
||||
DB::table('documents')->where('id', $document->id)->update(['issuer' => ($data['issuer'] ?? null) ?: $document->issuer,
|
||||
'issued_on' => ($data['document_date'] ?? null) ?: $document->issued_on, 'rights_status' => $data['rights_status'],
|
||||
'data_classification' => $data['data_classification'] ?? $document->data_classification, 'updated_at' => now()]);
|
||||
$this->audit($request, 'resource_metadata_updated', 'portal_resources', $id);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Resources\PublicBusinessOpportunityResource;
|
||||
use App\Models\ExistingBusiness;
|
||||
use App\Services\CountryCatalogue;
|
||||
use App\Services\ExistingBusinessDrafts;
|
||||
use App\Services\InvestmentGeography;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -84,6 +85,7 @@ class PublicBusinessOpportunityController extends Controller
|
||||
return Inertia::render('investment/business-interest', [
|
||||
'record' => (new PublicBusinessOpportunityResource($record))->resolve($request),
|
||||
'modalities' => ExistingBusinessDrafts::MODALITIES,
|
||||
'countries' => app(CountryCatalogue::class)->all(),
|
||||
'returnUrl' => $returnFilters === [] ? '/negocios' : '/negocios?'.http_build_query($returnFilters),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\Sector;
|
||||
use App\Services\CountryCatalogue;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
@@ -32,13 +33,13 @@ class PublicPortalController extends Controller
|
||||
|
||||
public function interest(?string $slug = null): Response
|
||||
{
|
||||
return $slug ? $this->detail($slug, 'interest') : Inertia::render('PortalInvestidor', ['initialView' => 'interest']);
|
||||
return $slug ? $this->detail($slug, 'interest') : Inertia::render('PortalInvestidor', ['initialView' => 'interest', 'countries' => app(CountryCatalogue::class)->all()]);
|
||||
}
|
||||
|
||||
private function detail(string $slug, string $view): Response
|
||||
{
|
||||
$opportunity = Opportunity::with('sector:id,name,slug')->published()->where('slug', $slug)->firstOrFail();
|
||||
|
||||
return Inertia::render('PortalInvestidor', ['initialView' => $view, 'initialOpportunity' => $opportunity]);
|
||||
return Inertia::render('PortalInvestidor', ['initialView' => $view, 'initialOpportunity' => $opportunity, 'countries' => app(CountryCatalogue::class)->all()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\TerritoryGeometryImport;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class TerritoryGeometryImportController extends Controller
|
||||
{
|
||||
public function preview(Request $request, TerritoryGeometryImport $service)
|
||||
{
|
||||
$data = $request->validate(['level' => 'nullable|integer|in:1,2,3']);
|
||||
$plan = $service->preview($request->user(), (int) ($data['level'] ?? 1));
|
||||
|
||||
return $request->expectsJson() ? response()->json($plan) : Inertia::render('management/territory-geometry-import', ['plan' => $plan]);
|
||||
}
|
||||
|
||||
public function apply(Request $request, TerritoryGeometryImport $service)
|
||||
{
|
||||
return response()->json($service->apply($request->user(), $request->all()));
|
||||
}
|
||||
|
||||
public function reverse(Request $request, string $batch, TerritoryGeometryImport $service)
|
||||
{
|
||||
return response()->json($service->reverse($request->user(), $batch, $request->all()));
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,13 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\ScanDocumentForMalware;
|
||||
use App\Models\InvestmentSubmission;
|
||||
use App\Models\User;
|
||||
use App\Services\Ai\InvestorGuidance;
|
||||
use App\Services\CountryCatalogue;
|
||||
use App\Services\InstitutionalLinkMonitor;
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Services\VerifiedDocumentBinary;
|
||||
use App\Services\VirtualDesk;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -48,7 +51,7 @@ 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' => \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)]),
|
||||
'submissions' => 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')
|
||||
@@ -72,7 +75,11 @@ class VirtualDeskController extends Controller
|
||||
$case = $desk->case($request->user(), $id);
|
||||
$owner = $desk->owns($request->user(), $case, false);
|
||||
$this->auditRead($request, 'desk_cases', $id);
|
||||
$data = ['case' => [...(array) $case, 'payload' => Arr::except($desk->decode($case->payload), $owner ? ['_creation_hash'] : ['_creation_hash', 'profile', 'company', 'investment', 'name', 'email']), 'workflow' => json_decode($case->workflow, true)],
|
||||
$hidden = $owner ? ['_creation_hash'] : ['_creation_hash', 'profile', 'company', 'investment', 'name', 'email'];
|
||||
if ((int) $case->user_id !== $request->user()->id) {
|
||||
$hidden[] = 'investor_path';
|
||||
}
|
||||
$data = ['case' => [...(array) $case, 'payload' => Arr::except($desk->decode($case->payload), $hidden), 'workflow' => json_decode($case->workflow, true)],
|
||||
'legacyRequest' => ($case->interest_request_id ?? null) ? DB::table('interest_requests as r')->leftJoin('users as u', 'u.id', '=', 'r.assigned_to')->where('r.id', $case->interest_request_id)->whereNull('r.deleted_at')
|
||||
->when(! $request->user()->can('manage-portal'), fn ($q) => $q->where('r.metadata->portal_user_id', $request->user()->id))
|
||||
->first(['r.id', 'r.reference', 'r.status', 'r.next_action', 'r.next_action_due_at', 'u.name as responsible_name']) : null,
|
||||
@@ -122,6 +129,13 @@ class VirtualDeskController extends Controller
|
||||
$row = $desk->request($request->user(), $id);
|
||||
$case = $desk->case($request->user(), $row->case_id);
|
||||
$payload = $desk->decode($row->payload);
|
||||
$definition = $desk->definition($row);
|
||||
$territoryPaths = [];
|
||||
foreach ($definition['fields'] as $key => $field) {
|
||||
if ($field['type'] === 'territory') {
|
||||
$territoryPaths[$key] = app(InvestmentGeography::class)->path($payload[$key] ?? null);
|
||||
}
|
||||
}
|
||||
$messageKinds = [];
|
||||
foreach (DB::table('desk_events')->where('request_id', $id)->where('action', 'message_added')->get() as $event) {
|
||||
$original = $desk->decode($event->payload);
|
||||
@@ -130,8 +144,8 @@ class VirtualDeskController extends Controller
|
||||
}
|
||||
}
|
||||
$this->auditRead($request, 'desk_requests', $id);
|
||||
$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),
|
||||
$data = ['countries' => app(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' => $definition, 'territoryPaths' => $territoryPaths,
|
||||
'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,
|
||||
'fieldsEditable' => in_array($row->status, ['draft', 'returned'], true), 'states' => config('virtual-desk.request_states'), 'transitions' => config('virtual-desk.transitions.'.$row->status, []),
|
||||
@@ -274,7 +288,7 @@ class VirtualDeskController extends Controller
|
||||
return $request->expectsJson() ? response()->json(['ok' => true]) : back()->with('success', 'Documento privado associado; consulte o estado da análise.');
|
||||
}
|
||||
|
||||
public function download(Request $request, string $id, string $document, VirtualDesk $desk): BinaryFileResponse
|
||||
public function download(Request $request, string $id, string $document, VirtualDesk $desk): BinaryFileResponse|\Illuminate\Http\Response
|
||||
{
|
||||
$desk->case($request->user(), $id);
|
||||
$doc = DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')->where('l.case_id', $id)->where('l.id', $document)->whereNull('d.deleted_at')->first(['d.*', 'l.request_id']);
|
||||
@@ -282,12 +296,10 @@ class VirtualDeskController extends Controller
|
||||
$desk->request($request->user(), $doc->request_id);
|
||||
}
|
||||
abort_unless($doc && $doc->status === 'active' && $doc->malware_scan_status === 'clean' && trim($doc->sha256) === $doc->malware_scanned_sha256, 404);
|
||||
$path = realpath(Storage::disk('local')->path($doc->storage_key));
|
||||
$root = realpath(Storage::disk('local')->path(''));
|
||||
abort_unless($path && $root && str_starts_with($path, $root.DIRECTORY_SEPARATOR) && is_file($path) && is_string($actualHash = hash_file('sha256', $path)) && hash_equals(trim($doc->sha256), $actualHash), 404);
|
||||
$response = app(VerifiedDocumentBinary::class)->respond($doc);
|
||||
$this->auditRead($request, 'documents', $doc->id, 'document_downloaded');
|
||||
|
||||
return response()->download($path, $doc->file_name, ['Cache-Control' => 'private, no-store']);
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function notification(Request $request, int $id): RedirectResponse
|
||||
|
||||
Reference in New Issue
Block a user