fix: complete InvesteMoz validation and error corrections
This commit is contained in:
@@ -4,7 +4,7 @@ namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\Sector;
|
||||
use App\Services\SectorOrganization;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
@@ -21,7 +21,7 @@ class HomeController extends Controller
|
||||
$locale = $validated['locale'] ?? 'pt';
|
||||
|
||||
$data = Cache::remember("portal_home_sector_organization_v1_{$locale}", now()->addMinutes(10), function () {
|
||||
$sectors = \App\Services\SectorOrganization::catalog();
|
||||
$sectors = SectorOrganization::catalog();
|
||||
|
||||
$featured = Opportunity::with('sector:id,name,slug')
|
||||
->published()->where('is_featured', true)
|
||||
|
||||
@@ -8,11 +8,13 @@ use App\Http\Requests\StoreInterestRequestRequest;
|
||||
use App\Jobs\NotifyInterestReceived;
|
||||
use App\Models\ExistingBusiness;
|
||||
use App\Models\InterestRequest;
|
||||
use App\Models\InvestmentSubmission;
|
||||
use App\Models\Opportunity;
|
||||
use App\Services\IdempotencyConflict;
|
||||
use App\Services\InterestSubmission;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
@@ -26,7 +28,7 @@ class InterestRequestController extends Controller
|
||||
$validated = [...$request->validated(), '_actor_id' => $request->user()->id];
|
||||
$key = $request->header('Idempotency-Key');
|
||||
abort_unless(is_string($key), 422, 'Identificador do envio em falta.');
|
||||
Validator::make(['idempotency_key' => $key], ['idempotency_key' => [$key === null ? 'nullable' : 'required', 'uuid']])->validate();
|
||||
Validator::make(['idempotency_key' => $key], ['idempotency_key' => ['required', 'uuid']])->validate();
|
||||
|
||||
try {
|
||||
$interestRequest = $submission->receive($validated, $key, function () use ($request, $validated) {
|
||||
@@ -78,19 +80,20 @@ class InterestRequestController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$row = \App\Models\InvestmentSubmission::create([
|
||||
$row = InvestmentSubmission::create([
|
||||
'user_id' => $request->user()->id, 'opportunity_id' => $opportunity?->id,
|
||||
'interest_request_id' => $interest->id, 'status' => 'submitted', 'version' => 1,
|
||||
'payload' => ['simple_participation' => true, 'opportunity_title' => $opportunity?->title ?? $business?->title, 'opportunity_reference' => $opportunity?->reference, 'phone' => $validated['phone'] ?? null, 'declared_origin_country_code' => $validated['declared_origin_country_code'] ?? null, 'full_name' => $validated['fullName'], 'email' => $validated['email'],
|
||||
'payload' => ['simple_participation' => true, 'opportunity_title' => $opportunity->title ?? $business?->public_title, 'opportunity_reference' => $opportunity?->reference, 'phone' => $validated['phone'] ?? null, 'declared_origin_country_code' => $validated['declared_origin_country_code'] ?? null, 'full_name' => $validated['fullName'], 'email' => $validated['email'],
|
||||
'objective' => $validated['message'], 'sector' => $validated['sectorInterest'],
|
||||
'country' => $validated['country'], 'existing_business_id' => $business?->id,
|
||||
'modality' => $validated['modality'] ?? null],
|
||||
]);
|
||||
\Illuminate\Support\Facades\DB::table('investment_submission_events')->insert([
|
||||
DB::table('investment_submission_events')->insert([
|
||||
'submission_id' => $row->id, 'actor_id' => $request->user()->id,
|
||||
'action' => 'submitted', 'version' => 1, 'created_at' => now(),
|
||||
]);
|
||||
$interest->forceFill(['metadata' => [...$interest->metadata, 'submission_id' => $row->id]])->save();
|
||||
|
||||
return $interest;
|
||||
});
|
||||
|
||||
@@ -99,7 +102,8 @@ class InterestRequestController extends Controller
|
||||
'message' => 'Manifestação de interesse registada com sucesso.',
|
||||
'data' => [
|
||||
'reference' => $interestRequest->reference,
|
||||
'status' => $interestRequest->status,
|
||||
// The POST acknowledgement is stable on replay; current workflow state is in tracking.
|
||||
'status' => InterestStatus::Received,
|
||||
'tracking_url' => '/investidor/manifestacoes',
|
||||
],
|
||||
], 201);
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Sector;
|
||||
use App\Services\CountryCatalogue;
|
||||
use App\Services\EconomicReferenceCatalog;
|
||||
use App\Services\SectorOrganization;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -44,7 +47,9 @@ class ReferenceCatalogController extends Controller
|
||||
|
||||
$cae = $catalog === 'economic-activities' && ($data['origin'] ?? 'portal') === 'cae';
|
||||
$versionQuery = DB::table('reference_catalog_versions')->where('catalog_code', $cae ? 'economic-activities-cae' : $catalog)->where('locale', 'pt-MZ');
|
||||
if ($cae && ! app(\App\Services\EconomicReferenceCatalog::class)->available()) { $versionQuery->whereRaw('false'); }
|
||||
if ($cae && ! app(EconomicReferenceCatalog::class)->available()) {
|
||||
$versionQuery->whereRaw('false');
|
||||
}
|
||||
if (isset($data['version'])) {
|
||||
$versionQuery->where('version_no', $data['version'])->whereIn('status', ['published', 'retired']);
|
||||
} else {
|
||||
@@ -82,7 +87,7 @@ class ReferenceCatalogController extends Controller
|
||||
$page = $query->orderBy('code')->orderBy('id')->paginate($data['per_page'] ?? 20)->withQueryString();
|
||||
$items = collect($page->items())->map(fn (object $row): array => $this->present($catalog, (array) $row))->values();
|
||||
if ($cae) {
|
||||
$paths = app(\App\Services\EconomicReferenceCatalog::class)->activities()->keyBy('id');
|
||||
$paths = app(EconomicReferenceCatalog::class)->activities()->keyBy('id');
|
||||
$items = $items->map(fn ($row) => [...$row, 'level' => $paths[$row['id']]->level, 'path' => $paths[$row['id']]->path, 'source_version' => $paths[$row['id']]->source_version, 'edition' => 'not_determined', 'divergence' => '600/579 documentalmente aberta']);
|
||||
}
|
||||
|
||||
@@ -169,15 +174,17 @@ class ReferenceCatalogController extends Controller
|
||||
->where('valid_from', '<=', today()->toDateString())
|
||||
->where(fn (Builder $builder) => $builder->whereNull('valid_to')->orWhere('valid_to', '>', today()->toDateString()))
|
||||
->select(['id', 'code', 'label_pt_mz as name', 'parent_id', 'sector_id', 'version_no']);
|
||||
if (empty($data['version'])) $query->where('id', '!=', \App\Services\SectorOrganization::LEGACY_ACTIVITY_ID);
|
||||
if (empty($data['version'])) {
|
||||
$query->where('id', '!=', SectorOrganization::LEGACY_ACTIVITY_ID);
|
||||
}
|
||||
if (array_key_exists('parent_id', $data) && $data['parent_id'] !== null) {
|
||||
$query->where('parent_id', $data['parent_id']);
|
||||
} else {
|
||||
$query->whereNull('parent_id');
|
||||
}
|
||||
if (! empty($data['sector_id'])) {
|
||||
$sector = \App\Models\Sector::find($data['sector_id']);
|
||||
$query->whereIn('sector_id', $sector ? \App\Models\Sector::whereIn('slug', \App\Services\SectorOrganization::slugs($sector->slug))->pluck('id') : []);
|
||||
$sector = Sector::whereKey($data['sector_id'])->first();
|
||||
$query->whereIn('sector_id', $sector ? Sector::whereIn('slug', SectorOrganization::slugs($sector->slug))->pluck('id') : []);
|
||||
}
|
||||
if (! empty($data['q'])) {
|
||||
$this->search($query, $data['q'], ['code', 'label_pt_mz']);
|
||||
|
||||
@@ -4,13 +4,14 @@ namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Sector;
|
||||
use App\Services\SectorOrganization;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class SectorController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$sectors = \App\Services\SectorOrganization::catalog()->map(fn ($s) => [
|
||||
$sectors = SectorOrganization::catalog()->map(fn ($s) => [
|
||||
'id' => $s->id, 'name' => $s->name, 'slug' => $s->slug,
|
||||
'shortDescription' => $s->short_description, 'description' => $s->description,
|
||||
'icon' => $s->icon, 'opportunitiesCount' => $s->opportunities_count,
|
||||
@@ -25,7 +26,8 @@ class SectorController extends Controller
|
||||
$sector = Sector::with(['opportunities' => fn ($q) => $q->published()])
|
||||
->where('slug', $slug)->firstOrFail();
|
||||
|
||||
$sector->setRelation('opportunities', \App\Services\SectorOrganization::opportunities($sector)->get());
|
||||
return response()->json(['success' => true, 'data' => $sector, 'canonical_url' => \App\Services\SectorOrganization::href($slug), 'specialization_of' => $slug === 'fintech' ? 'mercado-financeiro' : null]);
|
||||
$sector->setRelation('opportunities', SectorOrganization::opportunities($sector)->get());
|
||||
|
||||
return response()->json(['success' => true, 'data' => $sector, 'canonical_url' => SectorOrganization::href($slug), 'specialization_of' => $slug === 'fintech' ? 'mercado-financeiro' : null]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,17 @@ namespace App\Http\Controllers\Api\V1;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\ScanDocumentForMalware;
|
||||
use App\Services\SimulationFormDrafts;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use stdClass;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
class SimulationOfferDocumentController extends Controller
|
||||
{
|
||||
public function store(Request $request, string $id, SimulationFormDrafts $drafts)
|
||||
public function store(Request $request, string $id, SimulationFormDrafts $drafts): JsonResponse
|
||||
{
|
||||
abort_unless(Str::isUuid($id), 404);
|
||||
$drafts->find($id, $request);
|
||||
@@ -20,6 +23,7 @@ class SimulationOfferDocumentController extends Controller
|
||||
$request->validate(['file' => ['required', 'file', 'mimetypes:application/pdf,text/plain,image/png,image/jpeg', 'max:10240']]);
|
||||
$file = $request->file('file');
|
||||
$hash = hash_file('sha256', $file->getRealPath());
|
||||
abort_unless(is_string($hash), 422, 'Não foi possível verificar o conteúdo do anexo.');
|
||||
$existing = DB::table('documents')->where('source_system', 'simulation-fiscal-offer')->where('external_id', $id)->where('sha256', $hash)->whereNull('deleted_at')->first();
|
||||
if ($existing) {
|
||||
return response()->json(['data' => $this->metadata($id, $existing)], 200)->header('Cache-Control', 'private, no-store');
|
||||
@@ -37,32 +41,37 @@ class SimulationOfferDocumentController extends Controller
|
||||
throw $e;
|
||||
}
|
||||
ScanDocumentForMalware::dispatch($documentId)->afterCommit();
|
||||
|
||||
return response()->json(['data' => $this->metadata($id, DB::table('documents')->where('id', $documentId)->first())], 201)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function index(Request $request, string $id, SimulationFormDrafts $drafts)
|
||||
public function index(Request $request, string $id, SimulationFormDrafts $drafts): JsonResponse
|
||||
{
|
||||
abort_unless(Str::isUuid($id), 404);
|
||||
$drafts->find($id, $request);
|
||||
$documents = DB::table('documents')->where('source_system', 'simulation-fiscal-offer')->where('external_id', $id)->whereNull('deleted_at')->get();
|
||||
|
||||
return response()->json(['data' => $documents->map(fn ($doc) => $this->metadata($id, $doc))])->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function download(Request $request, string $id, string $document, SimulationFormDrafts $drafts)
|
||||
public function download(Request $request, string $id, string $document, SimulationFormDrafts $drafts): BinaryFileResponse
|
||||
{
|
||||
abort_unless(Str::isUuid($id) && Str::isUuid($document), 404);
|
||||
$drafts->find($id, $request);
|
||||
$doc = DB::table('documents')->where('id', $document)->where('source_system', 'simulation-fiscal-offer')->where('external_id', $id)->whereNull('deleted_at')->first();
|
||||
abort_unless($doc, 404);
|
||||
abort_unless($doc !== null, 404);
|
||||
abort_unless($doc->status === 'active' && $doc->malware_scan_status === 'clean' && hash_equals(trim($doc->sha256), (string) $doc->malware_scanned_sha256), 409, 'Anexo conservado; descarga disponível após análise antimalware concluída.');
|
||||
$root = realpath(Storage::disk('local')->path('simulation-offers/'.$id));
|
||||
$path = realpath(Storage::disk('local')->path($doc->storage_key));
|
||||
abort_unless($root && $path && str_starts_with(strtolower($path), strtolower($root.DIRECTORY_SEPARATOR)) && is_file($path), 404);
|
||||
abort_unless(hash_equals(trim($doc->sha256), hash_file('sha256', $path)), 409, 'Conteúdo do anexo diverge do hash registado.');
|
||||
$hash = hash_file('sha256', $path);
|
||||
abort_unless(is_string($hash) && hash_equals(trim($doc->sha256), $hash), 409, 'Conteúdo do anexo diverge do hash registado.');
|
||||
|
||||
return response()->download($path, basename($doc->file_name), ['Cache-Control' => 'private, no-store', 'X-Content-Type-Options' => 'nosniff']);
|
||||
}
|
||||
|
||||
private function metadata(string $id, object $doc): array
|
||||
/** @return array{id: string, draftId: string, name: string, sha256: string, scan: string, url: string} */
|
||||
private function metadata(string $id, stdClass $doc): array
|
||||
{
|
||||
return ['id' => $doc->id, 'draftId' => $id, 'name' => $doc->file_name, 'sha256' => trim($doc->sha256),
|
||||
'scan' => $doc->malware_scan_status, 'url' => '/api/v1/simulation-drafts/'.$id.'/offer-documents/'.$doc->id];
|
||||
|
||||
Reference in New Issue
Block a user