varias alteracoes
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-06 14:50:12 +02:00
parent 4f35313d06
commit e65cb26901
333 changed files with 54656 additions and 2058 deletions
@@ -15,7 +15,7 @@ class ReferenceCatalogController extends Controller
public function index(Request $request, string $catalog): JsonResponse
{
abort_unless(in_array($catalog, self::CATALOGS, true), 404);
$catalog = $this->validatedCatalog($catalog);
$rules = [
'q' => ['nullable', 'string', 'max:100'],
@@ -71,7 +71,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, $row))->values();
$items = collect($page->items())->map(fn (object $row): array => $this->present($catalog, (array) $row))->values();
return response()->json([
'data' => $items,
@@ -156,32 +156,47 @@ class ReferenceCatalogController extends Controller
return $query;
}
/** @param list<string> $columns */
/** @param list<literal-string> $columns */
private function search(Builder $query, string $search, array $columns): void
{
$term = '%'.addcslashes($search, '%_\\').'%';
$query->where(function (Builder $nested) use ($columns, $term): void {
foreach ($columns as $index => $column) {
$method = $index === 0 ? 'whereRaw' : 'orWhereRaw';
$nested->{$method}($column.' ILIKE ?', [$term]);
if ($index === 0) {
$nested->whereRaw($column.' ILIKE ?', [$term]);
} else {
$nested->orWhereRaw($column.' ILIKE ?', [$term]);
}
}
});
}
/** @return array<string,mixed> */
private function present(string $catalog, object $row): array
/** @return 'authorities'|'jurisdictions'|'economic-activities' */
private function validatedCatalog(string $catalog): string
{
abort_unless(in_array($catalog, self::CATALOGS, true), 404);
return $catalog;
}
/**
* @param 'authorities'|'jurisdictions'|'economic-activities' $catalog
* @param array<string, mixed> $row
* @return array<string,mixed>
*/
private function present(string $catalog, array $row): array
{
return match ($catalog) {
'jurisdictions' => ['id' => $row->id, 'value' => $row->id, 'code' => $row->code, 'name' => $row->name,
'label' => $row->name, 'locale' => 'pt-MZ', 'parent_id' => $row->parent_id, 'type' => $row->location_type,
'source_code' => $row->source_code, 'version_no' => $row->version_no],
'economic-activities' => ['id' => $row->id, 'value' => $row->code, 'code' => $row->code, 'name' => $row->name,
'label' => $row->name, 'locale' => 'pt-MZ', 'parent_id' => $row->parent_id, 'sector_id' => $row->sector_id,
'version_no' => $row->version_no],
'authorities' => ['id' => $row->id, 'value' => $row->code, 'code' => $row->code, 'name' => $row->name,
'label' => $row->name, 'locale' => 'pt-MZ', 'authority_type' => $row->authority_type,
'organization_id' => $row->organization_id, 'jurisdiction_id' => $row->jurisdiction_id,
'jurisdiction_name' => $row->jurisdiction_name, 'version_no' => $row->version_no],
'jurisdictions' => ['id' => $row['id'], 'value' => $row['id'], 'code' => $row['code'], 'name' => $row['name'],
'label' => $row['name'], 'locale' => 'pt-MZ', 'parent_id' => $row['parent_id'], 'type' => $row['location_type'],
'source_code' => $row['source_code'], 'version_no' => $row['version_no']],
'economic-activities' => ['id' => $row['id'], 'value' => $row['code'], 'code' => $row['code'], 'name' => $row['name'],
'label' => $row['name'], 'locale' => 'pt-MZ', 'parent_id' => $row['parent_id'], 'sector_id' => $row['sector_id'],
'version_no' => $row['version_no']],
'authorities' => ['id' => $row['id'], 'value' => $row['code'], 'code' => $row['code'], 'name' => $row['name'],
'label' => $row['name'], 'locale' => 'pt-MZ', 'authority_type' => $row['authority_type'],
'organization_id' => $row['organization_id'], 'jurisdiction_id' => $row['jurisdiction_id'],
'jurisdiction_name' => $row['jurisdiction_name'], 'version_no' => $row['version_no']],
};
}
}
@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\StoreSimulationRequest;
use App\Http\Requests\UpdateSimulationScenarioRequest;
use App\Models\QuestionnaireVersion;
use App\Models\Simulation;
use App\Models\SimulationJob;
use App\Policies\SimulationPolicy;
use App\Services\SimulationExecution;
@@ -65,6 +66,56 @@ class SimulationController extends Controller
return response()->json(['data' => $simulation])->header('ETag', $execution->etag($simulation));
}
public function history(Request $request, SimulationExecution $execution): JsonResponse
{
$user = $request->user();
abort_unless($user !== null, 401);
abort_unless($user->hasVerifiedEmail(), 403, 'Verifique o correio electrónico para consultar o histórico.');
$perPage = min(50, max(1, $request->integer('per_page', 20)));
$page = max(1, $request->integer('page', 1));
$history = DB::transaction(function () use ($execution, $user, $perPage, $page) {
$execution->setContext($user->id, null);
return Simulation::query()
->select([
'simulations.id', 'simulations.organization_id', 'visible_organization.name as organization_name',
'simulations.status', 'simulations.version_no', 'simulations.created_at', 'simulations.updated_at',
])
->leftJoin('organizations as visible_organization', 'visible_organization.id', '=', 'simulations.organization_id')
->where(function ($scope) use ($user) {
$scope->where(fn ($personal) => $personal->whereNull('simulations.organization_id')->where('simulations.owner_user_id', $user->id))
->orWhere(fn ($organization) => $organization->whereNotNull('simulations.organization_id')->whereExists(
fn ($membership) => $membership->selectRaw('1')->from('memberships')
->whereColumn('memberships.organization_id', 'simulations.organization_id')
->where('memberships.user_id', $user->id)->whereNull('memberships.deleted_at'),
));
})
->withExists(['reports as has_ready_report' => fn ($query) => $query->where('status', 'ready')])
->orderByDesc('simulations.created_at')->orderByDesc('simulations.id')
->paginate($perPage, ['*'], 'page', $page);
});
return response()->json([
'data' => $history->getCollection()->map(fn (Simulation $simulation): array => [
'id' => $simulation->id,
'scope' => $simulation->organization_id === null ? 'personal' : 'organization',
'organization_name' => $simulation->getAttribute('organization_name'),
'status' => $simulation->status,
'version_no' => $simulation->version_no,
'created_at' => $simulation->created_at?->toISOString(),
'updated_at' => $simulation->updated_at?->toISOString(),
'has_ready_report' => (bool) $simulation->getAttribute('has_ready_report'),
]),
'meta' => [
'current_page' => $history->currentPage(),
'last_page' => $history->lastPage(),
'per_page' => $history->perPage(),
'total' => $history->total(),
],
])->header('Cache-Control', 'no-store, private');
}
public function updateScenario(UpdateSimulationScenarioRequest $request, string $id, string $scenarioId, SimulationExecution $execution): JsonResponse
{
abort_unless(Str::isUuid($id) && Str::isUuid($scenarioId), 404);
@@ -3,11 +3,16 @@
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\AttachSimulationToDraftRequest;
use App\Http\Requests\ClaimSimulationFormDraftRequest;
use App\Http\Requests\LinkSimulationDraftBusinessRequest;
use App\Http\Requests\StoreSimulationFormDraftRequest;
use App\Models\SimulationFormDraft;
use App\Policies\SimulationPolicy;
use App\Services\SimulationExecution;
use App\Services\SimulationFormDrafts;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class SimulationFormDraftController extends Controller
@@ -16,13 +21,65 @@ class SimulationFormDraftController extends Controller
{
$key = $request->header('Idempotency-Key');
abort_unless(is_string($key) && strlen($key) >= 16 && strlen($key) <= 128, 428, 'Idempotency-Key é obrigatório.');
$result = $drafts->create($request->validated(), $request, $key);
$result = $drafts->create($request->snapshots(), $request, $key);
$draft = $result['draft'];
return response()->json(['data' => ['id' => $draft->id, 'version_no' => $draft->version_no, 'replayed' => $result['replayed']]], $result['replayed'] ? 200 : 201)
return response()->json(['data' => ['id' => $draft->id, 'version_no' => $draft->version_no, 'replayed' => $result['replayed']], 'expires_at' => $draft->expires_at?->toISOString()], $result['replayed'] ? 200 : 201)
->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private');
}
public function index(Request $request, SimulationFormDrafts $drafts): JsonResponse
{
abort_unless($request->user() !== null, 401);
return response()->json(['data' => $drafts->ownedDrafts($request->user())])->header('Cache-Control', 'no-store, private');
}
public function claimPreview(Request $request, SimulationFormDrafts $drafts): JsonResponse
{
abort_unless($request->user() !== null, 401);
return response()->json(['data' => $drafts->claimPreview($request)])->header('Cache-Control', 'no-store, private');
}
public function claim(ClaimSimulationFormDraftRequest $request, SimulationFormDrafts $drafts): JsonResponse
{
$result = $drafts->claim($request, $request->validated('choice'), $request->validated('existing_draft_id'));
$draft = $result['draft'];
return response()->json(['data' => $drafts->present($draft, true), 'choice' => $result['choice'], 'replayed' => $result['replayed'] ?? false, 'expires_at' => $draft->expires_at?->toISOString()])
->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private');
}
public function linkableBusinesses(Request $request, SimulationFormDrafts $drafts): JsonResponse
{
abort_unless($request->user()?->hasVerifiedEmail(), 403);
return response()->json(['data' => $drafts->linkableBusinesses($request->user())])->header('Cache-Control', 'no-store, private');
}
public function businessLink(Request $request, string $id, SimulationFormDrafts $drafts): JsonResponse
{
abort_unless(Str::isUuid($id) && $request->user()?->hasVerifiedEmail(), 404);
return response()->json(['data' => $drafts->activeBusinessLink($request->user(), $id)])->header('Cache-Control', 'no-store, private');
}
public function linkBusiness(LinkSimulationDraftBusinessRequest $request, string $id, SimulationFormDrafts $drafts): JsonResponse
{
abort_unless(Str::isUuid($id), 404);
$result = $drafts->linkBusiness($request->user(), $id, $request->validated('business_id'));
return response()->json(['data' => $result], isset($result['replayed']) ? 200 : 201)->header('Cache-Control', 'no-store, private');
}
public function unlinkBusiness(Request $request, string $id, SimulationFormDrafts $drafts): JsonResponse
{
abort_unless(Str::isUuid($id) && $request->user()?->hasVerifiedEmail(), 404);
return response()->json(['data' => $drafts->unlinkBusiness($request->user(), $id)])->header('Cache-Control', 'no-store, private');
}
public function current(Request $request, SimulationFormDrafts $drafts): JsonResponse
{
$draft = $drafts->current($request);
@@ -30,7 +87,14 @@ class SimulationFormDraftController extends Controller
return response()->json(['data' => null], 200)->header('Cache-Control', 'no-store, private');
}
return response()->json(['data' => $drafts->present($draft)])->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private');
return response()->json(['data' => $drafts->present($draft, $request->user() !== null)])->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private');
}
public function forgetCurrent(Request $request, SimulationFormDrafts $drafts): JsonResponse
{
$drafts->forgetCurrent($request);
return response()->json([], 204)->header('Cache-Control', 'no-store, private');
}
public function show(Request $request, string $id, SimulationFormDrafts $drafts): JsonResponse
@@ -38,18 +102,82 @@ class SimulationFormDraftController extends Controller
abort_unless(Str::isUuid($id), 404);
$draft = $drafts->find($id, $request);
return response()->json(['data' => $drafts->present($draft)])->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private');
return response()->json(['data' => $drafts->present($draft, $request->user() !== null)])->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private');
}
public function update(StoreSimulationFormDraftRequest $request, string $id, SimulationFormDrafts $drafts): JsonResponse
{
abort_unless(Str::isUuid($id), 404);
$draft = $drafts->update($id, $request->validated(), $request);
$draft = $drafts->update($id, $request->snapshots(), $request);
return response()->json(['data' => ['id' => $draft->id, 'version_no' => $draft->version_no]])
return response()->json(['data' => ['id' => $draft->id, 'version_no' => $draft->version_no], 'expires_at' => $draft->expires_at?->toISOString()])
->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private');
}
public function renew(Request $request, string $id, SimulationFormDrafts $drafts): JsonResponse
{
abort_unless(Str::isUuid($id), 404);
$draft = $drafts->renew($id, $request);
return response()->json(['data' => ['id' => $draft->id, 'version_no' => $draft->version_no], 'expires_at' => $draft->expires_at?->toISOString()])
->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private');
}
public function attachSimulation(AttachSimulationToDraftRequest $request, string $id, SimulationFormDrafts $drafts, SimulationExecution $execution): JsonResponse
{
abort_unless(Str::isUuid($id), 404);
$validated = $request->validated();
$simulation = $execution->find($validated['simulation_id'], $request);
$tokenOwned = $simulation->anonymous_token_hash !== null
&& hash_equals($simulation->anonymous_token_hash, (string) $execution->tokenHash($request));
if ($request->user()) {
$policy = app(SimulationPolicy::class);
$canTransfer = $simulation->organization_id === null
? $policy->view($request->user(), $simulation)
: $policy->update($request->user(), $simulation);
abort_unless($canTransfer || $tokenOwned, 404);
} else {
abort_unless($tokenOwned, 404);
}
abort_unless($simulation->status === 'completed' && is_array($simulation->result_snapshot), 409, 'Só é possível reutilizar uma simulação concluída.');
$report = $simulation->reports->where('status', 'ready')->sortByDesc('created_at')->first();
abort_unless($report !== null && is_array($report->report_snapshot), 409, 'O relatório versionado da simulação ainda não está disponível.');
abort_unless(preg_match('/^[a-f0-9]{64}$/D', $report->report_sha256) === 1, 409,
'O relatório da simulação não tem um hash verificável.');
$canonicalSnapshots = DB::selectOne('select CAST(? AS jsonb)::text as report_canonical, CAST(? AS jsonb)::text as result_canonical', [
json_encode($report->report_snapshot, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE),
json_encode($simulation->result_snapshot, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE),
]);
abort_unless(hash_equals($report->report_sha256, hash('sha256', $canonicalSnapshots->report_canonical))
&& hash_equals($canonicalSnapshots->report_canonical, $canonicalSnapshots->result_canonical), 409,
'O relatório não corresponde ao resultado versionado da simulação.');
$inputSnapshot = array_intersect_key($simulation->input_snapshot, array_flip(['base', 'rate', 'reduction']));
$sourceSnapshot = [
'simulation_id' => $simulation->id,
'source_version_no' => $simulation->version_no,
'source_created_at' => $simulation->created_at?->toISOString(),
'source_updated_at' => $simulation->updated_at?->toISOString(),
'input_snapshot' => $inputSnapshot,
'calculation_snapshot' => $simulation->calculation_snapshot,
'result_snapshot' => $simulation->result_snapshot,
'result_sha256' => $report->report_sha256,
'report_id' => $report->id,
'report_version_no' => $report->version_no,
'report_sha256' => $report->report_sha256,
'report_snapshot' => $report->report_snapshot,
'consented_at' => now()->toISOString(),
];
$attached = $drafts->attachSimulation($id, $sourceSnapshot, $request);
$draft = $attached['draft'];
return response()->json([
'data' => $drafts->present($draft, $request->user() !== null),
'replayed' => $attached['replayed'],
'expires_at' => $draft->expires_at?->toISOString(),
])->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private');
}
public function destroy(Request $request, string $id, SimulationFormDrafts $drafts): JsonResponse
{
abort_unless(Str::isUuid($id), 404);
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\ResolveSimulationTaxParametersRequest;
use App\Services\SimulationTaxParameterResolver;
use Illuminate\Http\JsonResponse;
class SimulationTaxParameterController extends Controller
{
public function resolve(ResolveSimulationTaxParametersRequest $request, SimulationTaxParameterResolver $resolver): JsonResponse
{
$input = $request->validated();
$data = $resolver->resolvePublishedForPeriod(
$input['component'],
$input['period_start'],
$input['period_end'],
$input['context'] ?? [],
);
return response()->json([
'component' => $input['component'],
'period' => ['start' => $input['period_start'], 'end' => $input['period_end']],
'assumptions' => $input['context'] ?? [],
'parameters' => $data,
])->header('Cache-Control', 'no-store');
}
}