From 528a2597ed40665e357ac9267da5117bb76d5042 Mon Sep 17 00:00:00 2001
From: nala
Date: Sat, 14 Nov 2026 18:08:46 +0200
Subject: [PATCH] feat(offers): complete phase five operational controls in QA
---
app/Console/Commands/ExpirePrivateOffers.php | 37 ++
.../OfferPublicCatalogueController.php | 25 +-
.../PortalEditorialWorkflowController.php | 1 +
.../PrivateOfferOperationsController.php | 51 ++
.../PrivateOfferPreparationController.php | 12 +-
app/Services/OfferAvailability.php | 80 +++
app/Services/OfferPublicCatalogue.php | 18 +-
app/Services/OpportunityProfileWorkflow.php | 10 +-
app/Services/PrivateOfferConfidentiality.php | 1 +
app/Services/PrivateOfferOperations.php | 296 ++++++++++
app/Services/PrivateOfferPreparation.php | 6 +-
config/private_offers.php | 4 +
.../unification/traceability-and-manual.md | 14 +
.../v2/auditoria-portal/matriz-requisitos.csv | 2 +-
.../ofertas-fase5-fecho-qa-20261114.md | 110 ++++
.../build-final.log | 332 +++++++++++
.../focused.log | 1 +
.../focused.xml | 17 +
.../global-format.log | 73 +++
.../qa-result.json | 170 ++++++
.../regression.log | 1 +
.../regression.xml | 74 +++
resources/js/components/offer-operations.tsx | 550 ++++++++++++++++++
resources/js/components/public-offer-card.tsx | 6 +
resources/js/lib/public-offer.ts | 1 +
.../pages/management/editorial-workflow.tsx | 3 +
.../js/pages/offers/preparation-index.tsx | 42 +-
resources/js/pages/offers/preparation.tsx | 18 +
resources/js/pages/offers/public-detail.tsx | 6 +
resources/js/pages/offers/report.tsx | 80 +++
routes/console.php | 3 +
routes/web.php | 7 +
tests/Feature/PrivateOfferOperationsTest.php | 351 +++++++++++
33 files changed, 2383 insertions(+), 19 deletions(-)
create mode 100644 app/Console/Commands/ExpirePrivateOffers.php
create mode 100644 app/Http/Controllers/PrivateOfferOperationsController.php
create mode 100644 app/Services/OfferAvailability.php
create mode 100644 app/Services/PrivateOfferOperations.php
create mode 100644 docs/investmoz/v2/auditoria-portal/ofertas-fase5-fecho-qa-20261114.md
create mode 100644 outputs/offers-private-phase5-20261114/build-final.log
create mode 100644 outputs/offers-private-phase5-20261114/focused.log
create mode 100644 outputs/offers-private-phase5-20261114/focused.xml
create mode 100644 outputs/offers-private-phase5-20261114/global-format.log
create mode 100644 outputs/offers-private-phase5-20261114/qa-result.json
create mode 100644 outputs/offers-private-phase5-20261114/regression.log
create mode 100644 outputs/offers-private-phase5-20261114/regression.xml
create mode 100644 resources/js/components/offer-operations.tsx
create mode 100644 resources/js/pages/offers/report.tsx
create mode 100644 tests/Feature/PrivateOfferOperationsTest.php
diff --git a/app/Console/Commands/ExpirePrivateOffers.php b/app/Console/Commands/ExpirePrivateOffers.php
new file mode 100644
index 00000000..562383f2
--- /dev/null
+++ b/app/Console/Commands/ExpirePrivateOffers.php
@@ -0,0 +1,37 @@
+whereNotNull('workflow_context->private_offer_preparation')->orderBy('id')->chunkById(100, function ($records) use (&$count) {
+ foreach ($records as $record) {
+ if (app(OfferAvailability::class)->record($record)['state'] !== 'expired') {
+ continue;
+ }
+ if ($this->option('confirm')) {
+ if (app(PrivateOfferOperations::class)->expire($record->id)) {
+ $count++;
+ }
+ } else {
+ $count++;
+ }
+ }
+ });
+ $this->info(($this->option('confirm') ? 'Expirações registadas: ' : 'Expiradas, sem escrita: ').$count);
+
+ return self::SUCCESS;
+ }
+}
diff --git a/app/Http/Controllers/OfferPublicCatalogueController.php b/app/Http/Controllers/OfferPublicCatalogueController.php
index 01d4ef9b..008c77c8 100644
--- a/app/Http/Controllers/OfferPublicCatalogueController.php
+++ b/app/Http/Controllers/OfferPublicCatalogueController.php
@@ -8,7 +8,7 @@ use App\Services\OpportunityManagement;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
-use Inertia\Response;
+use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
final class OfferPublicCatalogueController extends Controller
@@ -17,30 +17,30 @@ final class OfferPublicCatalogueController extends Controller
{
$filters = $catalogue->filters($request);
- return Inertia::render('offers/catalogue', [...$catalogue->page($request, $filters), 'filters' => $filters,
+ return $this->uncached(Inertia::render('offers/catalogue', [...$catalogue->page($request, $filters), 'filters' => $filters,
'mapData' => in_array($filters['view'] ?? '', ['map', 'combined'], true) ? $catalogue->map($filters) : null,
'territorySelection' => app(InvestmentGeography::class)->selection($filters),
- 'references' => ['categories' => OpportunityManagement::CATEGORIES, 'options' => [], 'clusters' => [], 'corridors' => [], 'projects' => [], 'locations' => [], 'sectors' => []]]);
+ 'references' => ['categories' => OpportunityManagement::CATEGORIES, 'options' => [], 'clusters' => [], 'corridors' => [], 'projects' => [], 'locations' => [], 'sectors' => []]])->toResponse($request));
}
- public function show(int $id, OfferPublicCatalogue $catalogue): Response
+ public function show(Request $request, int $id, OfferPublicCatalogue $catalogue): Response
{
- return Inertia::render('offers/public-detail', ['offer' => $catalogue->detail($id)]);
+ return $this->uncached(Inertia::render('offers/public-detail', ['offer' => $catalogue->detail($id)])->toResponse($request));
}
public function search(Request $request, OfferPublicCatalogue $catalogue): JsonResponse
{
- return response()->json($catalogue->page($request, $catalogue->filters($request)));
+ return response()->json($catalogue->page($request, $catalogue->filters($request)))->header('Cache-Control', 'private, no-store');
}
public function detail(int $id, OfferPublicCatalogue $catalogue): JsonResponse
{
- return response()->json(['data' => $catalogue->detail($id)]);
+ return response()->json(['data' => $catalogue->detail($id)])->header('Cache-Control', 'private, no-store');
}
public function map(Request $request, OfferPublicCatalogue $catalogue): JsonResponse
{
- return response()->json($catalogue->map($catalogue->filters($request)));
+ return response()->json($catalogue->map($catalogue->filters($request)))->header('Cache-Control', 'private, no-store');
}
public function export(Request $request, OfferPublicCatalogue $catalogue): StreamedResponse
@@ -60,6 +60,13 @@ final class OfferPublicCatalogueController extends Controller
fputcsv($stream, array_map(fn ($value) => preg_match('/^[\s]*[=+@-]/u', (string) $value) ? "'".$value : $value, $fields), ';', '"', '');
}
fclose($stream);
- }, 'ofertas.csv', ['Content-Type' => 'text/csv; charset=UTF-8', 'X-Export-Limit' => '1000', 'X-Results-Truncated' => $result['truncated'] ? 'true' : 'false']);
+ }, 'ofertas.csv', ['Content-Type' => 'text/csv; charset=UTF-8', 'Cache-Control' => 'private, no-store', 'X-Export-Limit' => '1000', 'X-Results-Truncated' => $result['truncated'] ? 'true' : 'false']);
+ }
+
+ private function uncached(Response $response): Response
+ {
+ $response->headers->set('Cache-Control', 'private, no-store');
+
+ return $response;
}
}
diff --git a/app/Http/Controllers/PortalEditorialWorkflowController.php b/app/Http/Controllers/PortalEditorialWorkflowController.php
index 6dc5dcd1..656f28ea 100644
--- a/app/Http/Controllers/PortalEditorialWorkflowController.php
+++ b/app/Http/Controllers/PortalEditorialWorkflowController.php
@@ -107,6 +107,7 @@ final class PortalEditorialWorkflowController extends Controller
$destinations = app(ContentManagement::class)->access($actor);
$data = ['items' => $items, 'filters' => $filters, 'notices' => $workflow->notices($actor), 'resourceAccess' => $access->full($actor) || $access->editorial($actor), 'opportunitiesUrl' => $destinations['opportunities']['index'] ?? null,
'offersUrl' => isset($destinations['opportunities']['index']) && app(PortalProfileAccess::class)->available() ? '/minhas-oportunidades/ofertas' : null,
+ 'offerOperationsUrl' => ! app(PortalProfileAccess::class)->has($actor, 'auditor') && (app(PortalProfileAccess::class)->has($actor, 'validator') || app(PortalProfileAccess::class)->has($actor, 'publisher')) ? '/minhas-oportunidades/ofertas?operational=1' : null,
'opportunityPending' => $workflow->opportunityPending($actor), 'clustersUrl' => $destinations['clusters']['index'] ?? null,
'portfolioUrl' => $destinations['portfolio']['index'] ?? null, 'portfolioPending' => $workflow->portfolioPending($actor), 'demographyUrl' => $destinations['demography']['index'] ?? null, 'demographyPending' => $workflow->demographyPending($actor),
'territorialUrl' => app(TerritorialRegistry::class)->editorialAvailable() && $actor->can('manage-portal') ? '/management/territorial/editorial' : null,
diff --git a/app/Http/Controllers/PrivateOfferOperationsController.php b/app/Http/Controllers/PrivateOfferOperationsController.php
new file mode 100644
index 00000000..80a08c72
--- /dev/null
+++ b/app/Http/Controllers/PrivateOfferOperationsController.php
@@ -0,0 +1,51 @@
+json($operations->presentation($request->user(), $id));
+ }
+
+ public function owner(Request $request, int $id, PrivateOfferOperations $operations): Response
+ {
+ $operations->ownerAction($request->user(), $id, $request->all());
+
+ return response()->json($operations->presentation($request->user(), $id));
+ }
+
+ public function reportForm(Request $request, int $id, PrivateOfferOperations $operations): Response
+ {
+ $record = Opportunity::findOrFail($id);
+ abort_unless($record->isPrivateOfferPreparation() && $operations->canReport($request->user(), $record), 404);
+
+ return Inertia::render('offers/report', ['offerId' => $id])->toResponse($request);
+ }
+
+ public function report(Request $request, int $id, PrivateOfferOperations $operations): Response
+ {
+ return response()->json($operations->report($request->user(), $id, $request->all()), 201);
+ }
+
+ public function handleReport(Request $request, int $id, string $reportId, PrivateOfferOperations $operations): Response
+ {
+ $operations->handleReport($request->user(), $id, $reportId, $request->all());
+
+ return response()->json($operations->presentation($request->user(), $id));
+ }
+
+ public function highlight(Request $request, int $id, PrivateOfferOperations $operations): Response
+ {
+ $operations->highlight($request->user(), $id, $request->all());
+
+ return response()->json($operations->presentation($request->user(), $id));
+ }
+}
diff --git a/app/Http/Controllers/PrivateOfferPreparationController.php b/app/Http/Controllers/PrivateOfferPreparationController.php
index 3e010977..c76dbd33 100644
--- a/app/Http/Controllers/PrivateOfferPreparationController.php
+++ b/app/Http/Controllers/PrivateOfferPreparationController.php
@@ -3,12 +3,14 @@
namespace App\Http\Controllers;
use App\Models\Opportunity;
+use App\Services\OfferAvailability;
use App\Services\OpportunityCanonicalContract;
use App\Services\OpportunityManagement;
use App\Services\OpportunityProfileWorkflow;
use App\Services\OpportunityResources;
use App\Services\PortalProfileAccess;
use App\Services\PrivateOfferConfidentiality;
+use App\Services\PrivateOfferOperations;
use App\Services\PrivateOfferPreparation;
use App\Services\VerifiedDocumentBinary;
use Illuminate\Http\JsonResponse;
@@ -86,14 +88,20 @@ final class PrivateOfferPreparationController extends Controller
public function index(Request $request, PortalProfileAccess $access): Response
{
- $filters = $request->validate(['state' => 'nullable|in:draft,submitted,in_review,needs_information,rejected,approved']);
- $records = $access->scope(Opportunity::query(), $request->user())->whereNotNull('workflow_context->private_offer_preparation')
+ $filters = $request->validate(['state' => 'nullable|in:draft,submitted,in_review,needs_information,rejected,approved', 'operational' => 'nullable|boolean', 'pending_reports' => 'nullable|boolean']);
+ $query = ($filters['operational'] ?? false) ? app(PrivateOfferOperations::class)->staffQuery($request->user()) : $access->scope(Opportunity::query(), $request->user());
+ if ($filters['pending_reports'] ?? false) {
+ $query = app(PrivateOfferOperations::class)->staffQuery($request->user());
+ $query->whereRaw("jsonb_path_exists(COALESCE(workflow_context->'offer_reports','{}'::jsonb), '$.* ? (@.status == \"open\" || @.status == \"in_review\")')");
+ }
+ $records = $query->whereNotNull('workflow_context->private_offer_preparation')
->when($filters['state'] ?? null, fn ($q, $state) => $q->where('workflow_context->private_offer_preparation->state', $state))
->orderByDesc('id')->paginate(20)->withQueryString();
return Inertia::render('offers/preparation-index', ['records' => $records->through(fn ($r) => [
'id' => $r->id, 'title' => app(OpportunityManagement::class)->values($r)['title'] ?? null, 'reference' => $r->reference,
'state' => $access->state($r),
+ 'availability' => app(OfferAvailability::class)->record($r)['state'],
]), 'filters' => $filters, 'canCreate' => count($access->navigation($request->user())['contexts']) > 0]);
}
diff --git a/app/Services/OfferAvailability.php b/app/Services/OfferAvailability.php
new file mode 100644
index 00000000..2f1eb073
--- /dev/null
+++ b/app/Services/OfferAvailability.php
@@ -0,0 +1,80 @@
+ 'unavailable', 'expires_at' => null, 'policy_pending' => true];
+ }
+ $policy = is_array($operation) ? ($operation['validity_policies'][$key] ?? $operation['validity'] ?? null) : null;
+ $pending = ! is_array($policy) || ($policy['days'] ?? null) === null;
+ $result = ['state' => 'available', 'expires_at' => null, 'policy_pending' => $pending];
+ if ($operation !== null && ! is_array($operation)) {
+ return [...$result, 'state' => 'unavailable'];
+ }
+ $state = $operation['status'] ?? 'available';
+ if (! is_int($operation['cycle'] ?? 0) || ($operation['cycle'] ?? 0) < 0) {
+ return [...$result, 'state' => 'unavailable'];
+ }
+ if (! in_array($state, ['available', 'withdrawn', 'expired', 'revalidation'], true)) {
+ return [...$result, 'state' => 'unavailable'];
+ }
+ if ($state !== 'available') {
+ return [...$result, 'state' => $state];
+ }
+ $days = $policy['days'] ?? null;
+ if (($operation['cycle'] ?? 0) > 0 && ($publication['offer_cycle'] ?? null) !== $operation['cycle']) {
+ return $result;
+ }
+ if ($days === null || ! is_array($publication) || ($publication['publication']['state'] ?? null) !== 'published') {
+ return $result;
+ }
+ if (! is_int($days) || $days < 1 || $days > 36500) {
+ return [...$result, 'state' => 'unavailable'];
+ }
+ try {
+ $at = $publication['publication']['at'] ?? null;
+ if (! is_string($at) || ! preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D', $at)) {
+ return [...$result, 'state' => 'unavailable'];
+ }
+ $start = CarbonImmutable::parse($at);
+ $end = $start->addDays($days)->utc();
+ } catch (\Throwable) {
+ return [...$result, 'state' => 'unavailable'];
+ }
+
+ return [...$result, 'expires_at' => $end->toIso8601String(), 'state' => $end->lte(CarbonImmutable::now()) ? 'expired' : 'available'];
+ }
+
+ /** @return array{state:string,expires_at:?string,policy_pending:bool} */
+ public function record(Opportunity $record): array
+ {
+ return $this->view($record->workflow_context['offer_operation'] ?? null, $record->workflow_context['offer_publication'] ?? null);
+ }
+
+ public function publicAllowed(mixed $operation, mixed $publication): bool
+ {
+ if ($this->view($operation, $publication)['state'] !== 'available') {
+ return false;
+ }
+ $cycle = $operation['cycle'] ?? 0;
+ if (! is_int($cycle) || $cycle < 0) {
+ return false;
+ }
+
+ // A new validation must never resurrect the previous public snapshot.
+ return $cycle === 0 || (is_array($publication) && ($publication['offer_cycle'] ?? null) === $cycle
+ && is_string($operation['activated_hash'] ?? null) && ($publication['editorial_hash'] ?? null) === $operation['activated_hash']);
+ }
+}
diff --git a/app/Services/OfferPublicCatalogue.php b/app/Services/OfferPublicCatalogue.php
index 85a1b18d..867c5f4b 100644
--- a/app/Services/OfferPublicCatalogue.php
+++ b/app/Services/OfferPublicCatalogue.php
@@ -120,7 +120,7 @@ final class OfferPublicCatalogue
return ['records' => [], 'truncated' => false];
}
$rows = Opportunity::query()->whereNotNull('workflow_context->private_offer_preparation')->whereNotNull('workflow_context->offer_publication')
- ->orderBy('id')->limit(1001)->get(['id', DB::raw("workflow_context->'offer_publication' AS public_snapshot")]);
+ ->orderBy('id')->limit(1001)->get(['id', DB::raw("workflow_context->'offer_publication' AS public_snapshot"), DB::raw("workflow_context->'offer_operation' AS operation_state")]);
$records = [];
foreach ($rows->take(1000) as $row) {
if (! is_string($row->getAttribute('public_snapshot'))) {
@@ -128,13 +128,21 @@ final class OfferPublicCatalogue
}
try {
$publication = json_decode($row->getAttribute('public_snapshot'), true, 512, JSON_THROW_ON_ERROR);
+ $operation = json_decode($row->getAttribute('operation_state') ?? 'null', true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $error) {
continue;
}
+ if (! app(OfferAvailability::class)->publicAllowed($operation, $publication)) {
+ continue;
+ }
$p = $this->project($row->id, $publication);
if (! $p) {
continue;
}
+ $expires = app(OfferAvailability::class)->view($operation, $publication)['expires_at'];
+ if ($expires !== null) {
+ $p['expires_at'] = $expires;
+ }
if (! empty($filters['q']) && ! str_contains(mb_strtolower($p['title'].' '.$p['summary']), mb_strtolower($filters['q']))) {
continue;
}
@@ -184,17 +192,23 @@ final class OfferPublicCatalogue
public function detail(int $id): array
{
abort_unless($this->fixtureReadAllowed(), 404);
- $row = Opportunity::query()->whereKey($id)->whereNotNull('workflow_context->private_offer_preparation')->first(['id', DB::raw("workflow_context->'offer_publication' AS public_snapshot")]);
+ $row = Opportunity::query()->whereKey($id)->whereNotNull('workflow_context->private_offer_preparation')->first(['id', DB::raw("workflow_context->'offer_publication' AS public_snapshot"), DB::raw("workflow_context->'offer_operation' AS operation_state")]);
if (! $row || ! is_string($row->getAttribute('public_snapshot'))) {
abort(404);
}
try {
$publication = json_decode($row->getAttribute('public_snapshot'), true, 512, JSON_THROW_ON_ERROR);
+ $operation = json_decode($row->getAttribute('operation_state') ?? 'null', true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $error) {
abort(404);
}
+ abort_unless(app(OfferAvailability::class)->publicAllowed($operation, $publication), 404);
$record = $this->project($id, $publication);
abort_unless($record !== null, 404);
+ $expires = app(OfferAvailability::class)->view($operation, $publication)['expires_at'];
+ if ($expires !== null) {
+ $record['expires_at'] = $expires;
+ }
return $record;
}
diff --git a/app/Services/OpportunityProfileWorkflow.php b/app/Services/OpportunityProfileWorkflow.php
index cc1b9d9b..deddf692 100644
--- a/app/Services/OpportunityProfileWorkflow.php
+++ b/app/Services/OpportunityProfileWorkflow.php
@@ -116,7 +116,9 @@ class OpportunityProfileWorkflow
if (in_array($state, ['draft', 'needs_information'], true)) {
$actions[] = 'submit';
}
- if (in_array($state, ['rejected', 'approved'], true)) {
+ if (in_array($state, ['rejected', 'approved'], true) || (isset($record->workflow_context['submission']) && ($record->workflow_context['offer_operation']['status'] ?? null) === 'revalidation'
+ && ($record->workflow_context['offer_operation']['requested_by'] ?? null) === $actor->id && (int) $record->owner_user_id === $actor->id
+ && in_array($state, ['draft', 'needs_information', 'submitted', 'in_review', 'information_pending'], true))) {
$actions[] = 'request_revision';
}
}
@@ -232,7 +234,8 @@ class OpportunityProfileWorkflow
'review_gate' => ['submitted', 'in_review', 'information_pending', 'approved'],
'publish' => ['approved'], 'suspend' => [$state],
'assign_publication' => ['approved'],
- 'request_revision' => $offer ? ['approved', 'rejected'] : ['approved', 'published', 'suspended'],
+ 'request_revision' => $offer ? (($context['offer_operation']['status'] ?? null) === 'revalidation'
+ ? ['draft', 'needs_information', 'submitted', 'in_review', 'information_pending', 'approved', 'rejected'] : ['approved', 'rejected']) : ['approved', 'published', 'suspended'],
'archive' => ['draft', 'submitted', 'in_review', 'information_pending', 'needs_information', 'approved', 'rejected', 'published', 'suspended'],
'request_archive' => ['draft', 'submitted', 'in_review', 'information_pending', 'needs_information', 'approved', 'published', 'suspended'],
'appeal', 'appeal_uphold', 'appeal_reopen' => ['rejected'],
@@ -378,6 +381,9 @@ class OpportunityProfileWorkflow
&& $this->responsibility($reviewer, $record, preg_match('/legal|registry|authority|confidentiality|license/', $gate) === 1 ? 'legal_reviewer' : 'technical_reviewer'), 403, 'Revisor revogado, sem responsabilidade ou com conflito.');
}
$context['approval'] = [...$decision, 'hash' => $snapshot['hash'], 'gates' => $context['reviews'] ?? []];
+ if ($offer) {
+ $context['offer_operation'] = app(PrivateOfferOperations::class)->afterApproval($context);
+ }
$this->tasks->complete($context['review_task_id'] ?? null, $data['reason']);
$record->workflow_context = $context;
if (! $offer) {
diff --git a/app/Services/PrivateOfferConfidentiality.php b/app/Services/PrivateOfferConfidentiality.php
index a20f02cf..8f795ade 100644
--- a/app/Services/PrivateOfferConfidentiality.php
+++ b/app/Services/PrivateOfferConfidentiality.php
@@ -61,6 +61,7 @@ final class PrivateOfferConfidentiality
private function approvedHash(Opportunity $record): string
{
+ abort_unless(app(OfferAvailability::class)->record($record)['state'] === 'available', 409, 'A oferta não está disponível.');
$hash = $record->workflow_context['approval']['hash'] ?? null;
abort_unless(is_string($hash) && app(PortalProfileAccess::class)->state($record) === 'approved', 409, 'Esta versão não tem aprovação editorial actual.');
try {
diff --git a/app/Services/PrivateOfferOperations.php b/app/Services/PrivateOfferOperations.php
new file mode 100644
index 00000000..19bdbbe3
--- /dev/null
+++ b/app/Services/PrivateOfferOperations.php
@@ -0,0 +1,296 @@
+ $context
+ * @return array */
+ public function afterApproval(array $context): array
+ {
+ $days = config('private_offers.validity_days');
+ $reference = config('private_offers.validity_policy_reference');
+ if ($days !== null && (! is_int($days) || $days < 1 || $days > 36500 || ! is_string($reference) || trim($reference) === '')) {
+ throw ValidationException::withMessages(['validity' => 'Configuração de validade incompleta. Não foi aplicado um prazo presumido.']);
+ }
+ $operation = $context['offer_operation'] ?? ['status' => 'available', 'cycle' => 0];
+ $hash = $context['approval']['hash'];
+ $policy = ['days' => $days, 'policy_reference' => $reference, 'approval_hash' => $hash];
+ $operation['validity_policies'][$hash] ??= $policy;
+ $operation['validity'] ??= $operation['validity_policies'][$hash];
+ $operation['last_approval_hash'] = $hash;
+
+ return $operation;
+ }
+
+ private function offer(int $id, bool $lock = false): Opportunity
+ {
+ $record = Opportunity::query()->when($lock, fn ($q) => $q->lockForUpdate())->findOrFail($id);
+ abort_unless($record->isPrivateOfferPreparation(), 404);
+
+ return $record;
+ }
+
+ public function publisher(User $actor, Opportunity $record): bool
+ {
+ $access = app(PortalProfileAccess::class);
+
+ return $access->canView($actor, $record) && $access->mfa($actor) && ! $access->has($actor, 'auditor') && ! $access->conflict($actor, $record)
+ && $access->scoped($actor, 'publisher', $record->organization_id, $record->opportunity_category) && $access->operational($actor, 'publish', $record->id);
+ }
+
+ /** @return Builder */
+ public function staffQuery(User $actor): Builder
+ {
+ $access = app(PortalProfileAccess::class);
+ abort_unless($access->mfa($actor) && ! $access->has($actor, 'auditor'), 403);
+ $grants = [...$access->grants($actor, 'validator'), ...$access->grants($actor, 'publisher')];
+ abort_unless($grants !== [], 403);
+
+ return $access->scope(Opportunity::query(), $actor)->whereNotNull('workflow_context->private_offer_preparation')
+ ->where('owner_user_id', '!=', $actor->id)->whereRaw("NOT COALESCE(workflow_context->'contributors','[]'::jsonb) @> ?::jsonb", [json_encode([$actor->id])])
+ ->where(fn ($q) => $q->whereNull('organization_id')->orWhereNotIn('organization_id', DB::table('memberships')->where('user_id', $actor->id)->whereNull('deleted_at')->select('organization_id')))
+ ->where(function ($q) use ($grants) {
+ foreach ($grants as $grant) {
+ $q->orWhere(function ($scope) use ($grant) {
+ $scope->whereIn('opportunity_category', json_decode($grant->opportunity_types, true, 512, JSON_THROW_ON_ERROR));
+ if ($grant->organization_id !== null) {
+ $scope->where('organization_id', $grant->organization_id);
+ }
+ });
+ }
+ });
+ }
+
+ /** @return array */
+ public function presentation(User $actor, int $id): array
+ {
+ $record = app(PrivateOfferPreparation::class)->record($actor, $id);
+ $reviewer = app(PrivateOfferConfidentiality::class)->internal($actor, $record);
+ $publisher = $this->publisher($actor, $record);
+ $reports = [];
+ if ($reviewer) {
+ foreach ($record->workflow_context['offer_reports'] ?? [] as $report) {
+ $reports[] = [...$report, 'reason' => Crypt::decryptString($report['reason']), 'resolution_reason' => isset($report['resolution_reason']) ? Crypt::decryptString($report['resolution_reason']) : null,
+ 'source_snapshot' => isset($report['source_snapshot']) ? json_decode(Crypt::decryptString($report['source_snapshot']), true, 512, JSON_THROW_ON_ERROR) : null];
+ }
+ }
+ $highlight = $record->workflow_context['offer_highlight'] ?? null;
+
+ return ['id' => $id, 'etag' => app(OpportunityManagement::class)->etag($record),
+ 'availability' => app(OfferAvailability::class)->record($record), 'cycle' => $record->workflow_context['offer_operation']['cycle'] ?? 0,
+ 'owner' => app(PrivateOfferConfidentiality::class)->seller($actor, $record), 'reviewer' => $reviewer, 'publisher' => $publisher,
+ 'editorial_state' => app(PortalProfileAccess::class)->state($record), 'reports' => $reports,
+ 'highlight' => $publisher && $highlight ? [...$highlight, 'note' => Crypt::decryptString($highlight['note'])] : null,
+ 'commercial_activation' => false,
+ 'history' => array_map(fn ($event) => array_intersect_key($event, array_flip(['id', 'action', 'actor_id', 'at', 'cycle'])), $record->workflow_context['offer_operation_history'] ?? [])];
+ }
+
+ /** @param array $context
+ * @param array $metadata */
+ private function save(Opportunity $record, ?User $actor, array $context, string $action, array $metadata = []): void
+ {
+ $event = ['id' => (string) Str::uuid(), 'action' => $action, 'actor_id' => $actor?->id, 'at' => now()->toIso8601String(),
+ 'cycle' => $context['offer_operation']['cycle'] ?? 0, ...$metadata];
+ $context['offer_operation_history'][] = $event;
+ $record->workflow_context = $context;
+ $record->management_version++;
+ $record->save();
+ DB::table('audit_events')->insert(['source_system' => 'private-offers', 'entity_type' => 'opportunity', 'actor_user_id' => $actor?->id,
+ 'action' => $action, 'data_classification' => 'restricted', 'correlation_id' => (string) Str::uuid(),
+ 'after_data' => json_encode(['record_id' => $record->id, 'record_version' => $record->management_version, ...$event], JSON_THROW_ON_ERROR)]);
+ }
+
+ /** @param array $context
+ * @return array */
+ private function withdraw(array $context, string $origin): array
+ {
+ $context['offer_operation'] = [...($context['offer_operation'] ?? ['cycle' => 0]), 'status' => 'withdrawn', 'withdrawn_at' => now()->toIso8601String(), 'origin' => $origin];
+
+ return $context;
+ }
+
+ /** @param array $input */
+ public function ownerAction(User $actor, int $id, array $input): Opportunity
+ {
+ $data = validator($input, ['action' => 'required|in:withdraw,request_reactivation,reactivate', 'confirm' => 'required|accepted',
+ 'etag' => 'required_unless:action,withdraw|string|size:64', 'reason' => 'nullable|string|max:1000'])->validate();
+
+ return DB::transaction(function () use ($actor, $id, $data) {
+ $record = $this->offer($id, true);
+ abort_unless(app(PrivateOfferConfidentiality::class)->seller($actor, $record), 403);
+ $context = $record->workflow_context;
+ $state = app(OfferAvailability::class)->record($record)['state'];
+ if ($data['action'] === 'withdraw') {
+ if (($context['offer_operation']['status'] ?? null) === 'withdrawn') {
+ return $record;
+ }
+ $this->save($record, $actor, $this->withdraw($context, 'owner'), 'offer_withdrawn', ['reason' => Crypt::encryptString($data['reason'] ?? '')]);
+
+ return $record;
+ }
+ abort_unless(hash_equals(app(OpportunityManagement::class)->etag($record), $data['etag']), 409);
+ if ($data['action'] === 'request_reactivation') {
+ abort_unless(in_array($state, ['withdrawn', 'expired'], true), 409, 'A oferta não está retirada ou expirada.');
+ $editorialState = app(PortalProfileAccess::class)->state($record);
+ abort_unless(isset($context['submission']) || in_array($editorialState, ['draft', 'needs_information'], true), 409,
+ 'A preparação anterior ainda precisa da vinculação editorial existente antes de abrir nova revisão.');
+ $context['offer_operation'] = [...($context['offer_operation'] ?? []), 'status' => 'revalidation', 'cycle' => ($context['offer_operation']['cycle'] ?? 0) + 1,
+ 'requested_by' => $actor->id, 'requested_at' => now()->toIso8601String()];
+ // Reopen the same editorial engine, retaining old submissions and decisions.
+ $record->workflow_context = $context;
+ $record->save();
+ if (isset($context['submission'])) {
+ $record = app(OpportunityProfileWorkflow::class)->transition($actor, $id, ['etag' => app(OpportunityManagement::class)->etag($record),
+ 'action' => 'request_revision', 'reason' => 'Pedido explícito de nova validação para reactivar a oferta. '.($data['reason'] ?? '')]);
+ }
+ $this->save($record, $actor, $record->workflow_context, 'offer_reactivation_requested');
+
+ return $record;
+ }
+ abort_unless($state === 'revalidation' && app(PortalProfileAccess::class)->state($record) === 'approved', 409, 'É necessária nova validação editorial.');
+ $snapshot = app(PrivateOfferPreparation::class)->snapshot($record);
+ abort_unless(($snapshot['payload']['operational_cycle'] ?? null) === $context['offer_operation']['cycle']
+ && hash_equals($context['approval']['hash'] ?? '', $snapshot['hash'])
+ && ($context['submission']['payload']['operational_cycle'] ?? null) === $context['offer_operation']['cycle'], 409);
+ $context['offer_operation']['status'] = 'available';
+ $context['offer_operation']['activated_hash'] = $snapshot['hash'];
+ $this->save($record, $actor, $context, 'offer_reactivated_privately');
+
+ return $record;
+ });
+ }
+
+ public function expire(int $id): bool
+ {
+ return DB::transaction(function () use ($id) {
+ $record = $this->offer($id, true);
+ $availability = app(OfferAvailability::class)->record($record);
+ if ($availability['state'] !== 'expired' || ($record->workflow_context['offer_operation']['status'] ?? null) === 'expired') {
+ return false;
+ }
+ $context = $record->workflow_context;
+ $context['offer_operation'] = [...($context['offer_operation'] ?? []), 'status' => 'expired', 'expired_at' => $availability['expires_at']];
+ $this->save($record, null, $context, 'offer_expired', ['expires_at' => $availability['expires_at']]);
+
+ return true;
+ });
+ }
+
+ public function canReport(User $actor, Opportunity $record): bool
+ {
+ if (app(PortalProfileAccess::class)->canView($actor, $record)) {
+ return true;
+ }
+ try {
+ app(OfferPublicCatalogue::class)->detail($record->id);
+
+ return true;
+ } catch (HttpExceptionInterface $error) {
+ return false;
+ }
+ }
+
+ /** @param array $input
+ * @return array */
+ public function report(User $actor, int $id, array $input): array
+ {
+ $data = validator($input, ['client_key' => 'required|uuid', 'reason' => 'required|string|min:20|max:2000', 'confirm' => 'required|accepted'])->validate();
+
+ return DB::transaction(function () use ($actor, $id, $data) {
+ $record = $this->offer($id, true);
+ $context = $record->workflow_context;
+ $key = $data['client_key'];
+ $hash = hash('sha256', $data['reason']);
+ if (isset($context['offer_reports'][$key])) {
+ $previous = $context['offer_reports'][$key];
+ abort_unless($previous['reporter_id'] === $actor->id, 404);
+ abort_unless($previous['command_hash'] === $hash, 409);
+
+ return ['id' => $key, 'replayed' => true];
+ }
+ abort_unless($this->canReport($actor, $record), 404);
+ try {
+ $source = app(OfferPublicCatalogue::class)->detail($id);
+ } catch (HttpExceptionInterface $error) {
+ $source = Arr::only(app(OpportunityManagement::class)->values($record), ['title', 'description', 'opportunity_category', 'private_offer']);
+ }
+ $task = app(OpportunityWorkflowTasks::class)->ensure($record, 'offer_report_'.$key, 'Denúncia da Oferta '.$record->reference);
+ $context['offer_reports'][$key] = ['id' => $key, 'reporter_id' => $actor->id, 'at' => now()->toIso8601String(), 'command_hash' => $hash,
+ 'reason' => Crypt::encryptString($data['reason']), 'version' => 1, 'status' => 'open', 'task_id' => $task,
+ 'source_snapshot' => Crypt::encryptString(json_encode($source, JSON_THROW_ON_ERROR)),
+ 'source_record_version' => $record->management_version, 'source_approval_hash' => $context['approval']['hash'] ?? null,
+ 'source_public_hash' => $context['offer_publication']['publication']['hash'] ?? null];
+ $this->save($record, $actor, $context, 'offer_reported', ['report_id' => $key]);
+
+ return ['id' => $key, 'replayed' => false];
+ });
+ }
+
+ /** @param array $input */
+ public function handleReport(User $actor, int $id, string $reportId, array $input): void
+ {
+ $data = validator($input, ['etag' => 'required|string|size:64', 'version' => 'required|integer|min:1', 'action' => 'required|in:start_review,resolve_no_withdrawal,resolve_withdrawal',
+ 'reason' => 'required|string|min:20|max:2000', 'confirm' => 'required|accepted'])->validate();
+ DB::transaction(function () use ($actor, $id, $reportId, $data) {
+ $record = $this->offer($id, true);
+ abort_unless(app(PrivateOfferConfidentiality::class)->internal($actor, $record), 403);
+ abort_unless(hash_equals(app(OpportunityManagement::class)->etag($record), $data['etag']), 409);
+ $context = $record->workflow_context;
+ $report = $context['offer_reports'][$reportId] ?? null;
+ abort_unless($report !== null, 404);
+ abort_unless($report['version'] === (int) $data['version'] && in_array($report['status'], ['open', 'in_review'], true), 409);
+ if ($data['action'] === 'start_review') {
+ abort_unless($report['status'] === 'open', 409);
+ app(OpportunityWorkflowTasks::class)->assign($record, $report['task_id'], $actor);
+ $report['status'] = 'in_review';
+ } else {
+ abort_unless($report['status'] === 'in_review' && $report['reviewer_id'] === $actor->id, 409, 'A denúncia deve ser tratada pelo revisor que a iniciou.');
+ app(OpportunityWorkflowTasks::class)->complete($report['task_id'], 'Denúncia tratada; decisão no histórico privado.');
+ $report['status'] = 'resolved';
+ $report['resolution'] = $data['action'];
+ if ($data['action'] === 'resolve_withdrawal') {
+ $context = $this->withdraw($context, 'moderation');
+ }
+ }
+ $report['reviewer_id'] = $actor->id;
+ $report['version']++;
+ $report['updated_at'] = now()->toIso8601String();
+ $report['resolution_reason'] = Crypt::encryptString($data['reason']);
+ $report['history'][] = ['action' => $data['action'], 'actor_id' => $actor->id, 'at' => now()->toIso8601String(), 'reason' => Crypt::encryptString($data['reason'])];
+ $context['offer_reports'][$reportId] = $report;
+ $this->save($record, $actor, $context, 'offer_report_'.$data['action'], ['report_id' => $reportId]);
+ });
+ }
+
+ /** @param array $input */
+ public function highlight(User $actor, int $id, array $input): void
+ {
+ $data = validator($input, ['etag' => 'required|string|size:64', 'version' => 'required|integer|min:0', 'status' => 'required|in:draft,archived',
+ 'label' => 'required|string|max:150', 'starts_at' => 'nullable|date', 'ends_at' => 'nullable|required_with:starts_at|date|after:starts_at',
+ 'note' => 'required|string|min:20|max:1000', 'confirm' => 'required|accepted'])->validate();
+ DB::transaction(function () use ($actor, $id, $data) {
+ $record = $this->offer($id, true);
+ abort_unless($this->publisher($actor, $record), 403);
+ $context = $record->workflow_context;
+ $before = $context['offer_highlight'] ?? null;
+ abort_unless(hash_equals(app(OpportunityManagement::class)->etag($record), $data['etag']) && (int) $data['version'] === ($before['version'] ?? 0), 409);
+ $plan = ['version' => (int) $data['version'] + 1, 'status' => $data['status'], 'label' => $data['label'], 'starts_at' => $data['starts_at'] ?? null,
+ 'ends_at' => $data['ends_at'] ?? null, 'note' => Crypt::encryptString($data['note']), 'actor_id' => $actor->id, 'at' => now()->toIso8601String()];
+ $context['offer_highlight_history'][] = $before;
+ $context['offer_highlight'] = $plan;
+ $this->save($record, $actor, $context, 'offer_highlight_prepared', ['plan_version' => $plan['version'], 'status' => $plan['status']]);
+ });
+ }
+}
diff --git a/app/Services/PrivateOfferPreparation.php b/app/Services/PrivateOfferPreparation.php
index 931a9553..7d25da32 100644
--- a/app/Services/PrivateOfferPreparation.php
+++ b/app/Services/PrivateOfferPreparation.php
@@ -154,6 +154,10 @@ final class PrivateOfferPreparation
if ($extended) {
$payload['resources'] = $resources;
}
+ $cycle = $record->workflow_context['offer_operation']['cycle'] ?? 0;
+ if ($cycle > 0) {
+ $payload['operational_cycle'] = $cycle;
+ }
$normalize = function (mixed $value) use (&$normalize): mixed {
if (is_object($value)) {
$value = get_object_vars($value);
@@ -169,7 +173,7 @@ final class PrivateOfferPreparation
};
$payload = $normalize($payload);
- $contract = $extended ? 'private-offer-editorial-v3' : 'private-offer-editorial-v2';
+ $contract = $cycle > 0 ? 'private-offer-editorial-v4' : ($extended ? 'private-offer-editorial-v3' : 'private-offer-editorial-v2');
return ['payload' => $payload, 'hash' => hash('sha256', json_encode(['contract' => $contract, 'payload' => $payload], JSON_THROW_ON_ERROR)),
'record_version' => $record->management_version, 'contract_version' => $contract, 'resources' => $resources];
diff --git a/config/private_offers.php b/config/private_offers.php
index 4b52c37d..505f7083 100644
--- a/config/private_offers.php
+++ b/config/private_offers.php
@@ -3,4 +3,8 @@
return [
// No legal text or operational NDA is supplied/approved. Never infer it from an upload.
'nda' => null,
+ // No definitive publication lifetime was decided. Freeze an explicit policy on approval.
+ 'validity_days' => null,
+ 'validity_policy_reference' => null,
+ 'expiry_scheduler_enabled' => false,
];
diff --git a/docs/investmoz/unification/traceability-and-manual.md b/docs/investmoz/unification/traceability-and-manual.md
index 74f95514..359fac95 100644
--- a/docs/investmoz/unification/traceability-and-manual.md
+++ b/docs/investmoz/unification/traceability-and-manual.md
@@ -10185,3 +10185,17 @@ Controlo empresarial exige aceitação do exemplar exacto do NDA e autorização
Os cinco testes territoriais foram investigados: dependiam de Inhambane/turismo previamente importados numa base vazia. O fixture cria apenas os mínimos sintéticos quando faltam, com guarda testing/loopback/*_test:55441 e transacção de rollback. Os seis casos passaram; nenhum cadastro operacional foi alterado. Os snapshots F4 vinculam particularidades/recursos ao contrato v3; Ofertas comuns anteriores conservam v2.
Estado: implementado em QA, sem instalação, publicação efectiva, pagamentos, destaque comercial, migrações ou alterações em mne. Fase 5 não iniciada; módulo integral/Procuras parciais. Pendências: exemplar/versão/aprovação do NDA, identificação jurídica dos comprovativos admitidos, visual desktop/móvel e email real anteriores. GD ausente: derivadas do processador nativo continuam bloqueadas, sem simular processamento. Formatação global mantém 61 ficheiros exteriores ao delta. Relatório: docs/investmoz/v2/auditoria-portal/ofertas-fase4-entrega-qa-20261114.md. Recibo: outputs/offers-private-phase4-20261114/qa-result.json; contém resultados e hashes finais, sem somar execuções sobrepostas. Próximo passo: apreciação QA e disponibilização do NDA aprovado, mantendo o bloqueio de publicação.
+
+## 14/11/2026 — Ofertas Fase 5, fecho operacional em QA
+
+Base aceite com ressalvas e431de8b preservada, tal como 7691cab3, 91c18573, 1f2990e4 e plano6e302db6. F5 autorizada exclusivamente em QA. O dono pode retirar disponibilidade imediatamente, sem aprovação editorial ou bloqueio por etag antigo. Retirada/expiração excluem leituras públicas e fecham dados protegidos por NDA. Conteúdos, identificadores, originais e decisões não são apagados.
+
+Reactivar exige pedido no mesmo cadastro, novo ciclo operacional e nova submissão/revisão/aprovação no motor existente; só depois o dono repõe disponibilidade privada. O snapshot v4 inclui o ciclo, preservando v2/v3 anteriores. Uma publicação antiga não reaparece e grants NDA antigos não reabrem por causa dessa reactivação. A reposição de disponibilidade não publica.
+
+Validade independente do NDA: validity_days e validity_policy_reference inicialmente nulos, sem prazo definitivo presumido. Política congelada por hash; início na publicação da versão; configuração ou aprovação privada posterior não prolongam o exemplar público anterior. Expiração verificada em cada leitura, sem depender do agendador. API/cartão apresentam data apenas quando conhecida; CSV conserva o formato anterior e a mesma exclusão. Comando investmoz:offers-expire confere sem escrita; --confirm materializa com auditoria/idempotência. Agendamento permanece desligado.
+
+Denúncias fundamentadas ficam cifradas com origem, actor e versão, ligadas a tarefas CRM e ao Dashboard Colaborativo existentes. A entrada Ofertas — acompanhamento usa a mesma listagem privada e filtro de denúncias pendentes. Validador no âmbito, com MFA e sem conflito, analisa e conclui com fundamento, com ou sem retirada. Etags e versão da denúncia protegem a decisão; o proprietário não recebe os dados originais do denunciante. Retomas devolvem o recibo sem outra tarefa. Planos administrativos de destaque têm versão/histórico e só rascunho/arquivo, sob Publicador autorizado; não alteram is_featured, cobram ou executam pagamentos.
+
+Verificação: percurso novo inicialmente com 12 testes/151 asserções aprovados; regressão integrada final com 59 testes/780 asserções aprovados, com resultados no recibo outputs/offers-private-phase5-20261114/qa-result.json. Grupos sobrepostos não devem ser somados. Relatório técnico e matriz de aceitação das cinco fases: docs/investmoz/v2/auditoria-portal/ofertas-fase5-fecho-qa-20261114.md. A matriz RF-MN-003 existente foi actualizada, sem novo inventário geral.
+
+Estado: cinco fases preparadas em QA; F1-F4 aceites tecnicamente com ressalvas, F5 para apreciação; nenhuma instalação ou publicação efectiva. Mantêm-se pendências NDA/exemplar/versão/aprovação, tipos jurídicos de comprovativos, prazo final, avisos jurídicos, visual desktop/móvel, GD/WebP e email real F2. Formatação global mantém61ficheiros preexistentes fora do delta. NDA mantém configuração nula; sem migrações, alterações em mne, dados operacionais, pagamentos ou promoção comercial. Procuras/restante portal não encerrados por este lote. Próximo passo: apreciação QA e decisões documentais; instalação/activação/publicação aguardam autorização expressa.
diff --git a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv
index 357391e4..cfcf8b23 100644
--- a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv
+++ b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv
@@ -163,7 +163,7 @@ RF-MIN-005,"Controlar cessão, mudança de controlo e aprovações.",upgrade v4
RF-MIN-006,Pesquisar e corresponder oportunidades mineiras.,upgrade v4 SHA f0b9815…; p. 3275; requirements-audit.csv,oportunidades,/api/v1/business-opportunities; /api/v1/business-opportunities/{slug}; /investidor/terra-duat; /investidor/terra-duat/novo; /investidor/terra-duat/{id}; /investidor/terra-duat/{id}/actualizacoes; /investidor/terra-duat/{id}/documentos; /investidor/terra-duat/{id}/documentos/{attachment}; /investidor/terra-duat/{id}/editar; /investidor/terra-duat/{id}/imprimir; /investidor/terra-duat/{id}/solicitar-apoio; /management/opportunities,"OpportunityManagement, OpportunityCanonicalContract/Response, OpportunityReviewMateriality, PortalProfileAccess, OpportunityResources. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.",outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,Sem evidência suficiente,Reconciliar contrato/cláusula e provas por categoria; completar apenas falha/variante executável identificada. Response normativo não é completo. Critério individual original preservado; cobertura atual ainda não individualizada.,"030000→040000 já instalados; contratos/resources existentes, sessão/MFA, ClamD e worker para ficheiros; gates das seis categorias ON. Variantes modulares/offers continuam separadas. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.","RF-MIN-006: Filtros incluem recurso, fase, título, localização, valor, modalidade e infra-estrutura; recomendação é explicável e revista por gestor.",P2,RF-MIN-006: Relatório 25/09 conservado; estado histórico não promove conclusão,RF-MIN-006,1,"[{""id"":""RF-MIN-006"",""pedido"":""Pesquisar e corresponder oportunidades mineiras."",""fontes"":[""upgrade v4 SHA f0b9815…; p. 3275; requirements-audit.csv""],""aceitacao"":""Filtros incluem recurso, fase, título, localização, valor, modalidade e infra-estrutura; recomendação é explicável e revista por gestor."",""estado"":""parcial"",""variantes"":""Relatório 25/09 conservado; estado histórico não promove conclusão"",""modulo"":""oportunidades""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,public.opportunities: 6 registos; public.opportunity_images: tabela ausente; public.opportunity_documents: tabela ausente; public.opportunity_location_drafts: tabela ausente,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.",Reconciliar contrato/cláusula e provas por categoria; completar apenas falha/variante executável identificada. Response normativo não é completo. Critério individual original preservado; cobertura atual ainda não individualizada.,"OpportunityManagement, OpportunityCanonicalContract/Response, OpportunityReviewMateriality, PortalProfileAccess, OpportunityResources","030000→040000 já instalados; contratos/resources existentes, sessão/MFA, ClamD e worker para ficheiros; gates das seis categorias ON. Variantes modulares/offers continuam separadas.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: oportunidades. Esforço de código adicional só após lacuna reproduzida.,outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,oportunidades,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",app/Services/OpportunityManagement.php; app/Services/OpportunityResources.php; resources/js/pages/management/opportunity-draft.tsx — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"OpportunityManagementTest, OpportunityResourcesTest, OpportunityOriginReviewTest; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.",Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,,
RF-MN-001,Registar utilizadores singulares e colectivos com verificação de contactos.,upgrade v4 SHA f0b9815…; p. 2957; requirements-audit.csv | portal do investidor\upgrade do portal de investimentos.docx; SHA 471012abb0f56377956f4ab822fe3186e2561bee0471508a5d807091f2e0154f; w:p 573 | portal do investidor\Portal V4\upgrade do portal de investimentos v6 programacao.docx; SHA e1169a7743b13ff6c950d3ffd4672a2afea32e81da84885e11154b7d8de9241e; w:p 3409 | portal do investidor\Portal V4\upgrade do portal de investimentos v1.docx; SHA f70ce3075523c6278e00dce15b9c7acf2b690679e16959ae5f7d007e754cfc6e; w:p 580 | portal do investidor\Portal V4\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 2214 | portal do investidor\Portal V4\upgrade do portal de investimentos v5 programacao.docx; SHA edece80c0519537244f8a8b86456af0aa92de1c526f791bb0ee9b85af7a7e8b2; w:p 3409,CRM/solicitações,//; /Simulador; /api/v1/documents/recommendations; /api/v1/investment/submissions; /api/v1/investment/submissions/{id}; /api/v1/investment/submissions/{id}/attachments; /api/v1/investment/submissions/{id}/attachments/{attachment}; /api/v1/investment/submissions/{id}/submit; /apoio; /balcao-virtual; /como-investir; /documentos,"investors/contacts/leads/tasks, OpportunityContacts, acompanhamento interno existente. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.",outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,Sem evidência suficiente,"Converter sem duplicar, tarefa/responsável/próxima ação recuperáveis; corrigir apenas falha comprovada. Critério individual original preservado; cobertura atual ainda não individualizada.",Relações mestre/contexto e autorizações; não depende de CRM externo. C131 sóQA. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,RF-MN-001: Conta só é activada após confirmação do correio electrónico ou telefone; tentativas e falhas ficam registadas. | MASTER-471012ab-0051: Registar utilizadores singulares e colectivos com verificação de contactos. | MASTER-e1169a77-0201: Registar utilizadores singulares e colectivos com verificação de contactos. | MASTER-f70ce307-0057: Registar utilizadores singulares e colectivos com verificação de contactos. | MASTER-51b8f0ad-0137: Registar utilizadores singulares e colectivos com verificação de contactos. | MASTER-edece80c-0201: Registar utilizadores singulares e colectivos com verificação de contactos.,P2,RF-MN-001: Relatório 25/09 conservado; estado histórico não promove conclusão | MASTER-471012ab-0051: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-e1169a77-0201: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-f70ce307-0057: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-51b8f0ad-0137: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-edece80c-0201: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,RF-MN-001 | MASTER-471012ab-0051 | MASTER-e1169a77-0201 | MASTER-f70ce307-0057 | MASTER-51b8f0ad-0137 | MASTER-edece80c-0201,6,"[{""id"":""RF-MN-001"",""pedido"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""fontes"":[""upgrade v4 SHA f0b9815…; p. 2957; requirements-audit.csv""],""aceitacao"":""Conta só é activada após confirmação do correio electrónico ou telefone; tentativas e falhas ficam registadas."",""estado"":""parcial"",""variantes"":""Relatório 25/09 conservado; estado histórico não promove conclusão"",""modulo"":""CRM/solicitações""},{""id"":""MASTER-471012ab-0051"",""pedido"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""fontes"":[""portal do investidor\\upgrade do portal de investimentos.docx; SHA 471012abb0f56377956f4ab822fe3186e2561bee0471508a5d807091f2e0154f; w:p 573""],""aceitacao"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""},{""id"":""MASTER-e1169a77-0201"",""pedido"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v6 programacao.docx; SHA e1169a7743b13ff6c950d3ffd4672a2afea32e81da84885e11154b7d8de9241e; w:p 3409""],""aceitacao"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""},{""id"":""MASTER-f70ce307-0057"",""pedido"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v1.docx; SHA f70ce3075523c6278e00dce15b9c7acf2b690679e16959ae5f7d007e754cfc6e; w:p 580""],""aceitacao"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""},{""id"":""MASTER-51b8f0ad-0137"",""pedido"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 2214""],""aceitacao"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""},{""id"":""MASTER-edece80c-0201"",""pedido"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v5 programacao.docx; SHA edece80c0519537244f8a8b86456af0aa92de1c526f791bb0ee9b85af7a7e8b2; w:p 3409""],""aceitacao"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,public.interest_requests: 4 registos; public.leads: 0 registos; public.investors: 3 registos,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.","Converter sem duplicar, tarefa/responsável/próxima ação recuperáveis; corrigir apenas falha comprovada. Critério individual original preservado; cobertura atual ainda não individualizada.","investors/contacts/leads/tasks, OpportunityContacts, acompanhamento interno existente",Relações mestre/contexto e autorizações; não depende de CRM externo. C131 sóQA.,Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: acompanhamento. Esforço de código adicional só após lacuna reproduzida.,outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,acompanhamento,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",app/Http/Controllers/CrmController.php; app/Services/DeclaredOriginStatistics.php — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"InterestSubmissionTest, InvestmentWorkspaceTest, PortalOriginCompatibilityTest; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.",Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,,
RF-MN-002,"Gerir perfis, representantes, funções e organizações.",upgrade v4 SHA f0b9815…; p. 2961; requirements-audit.csv,governação/outros,/api/v1/investment/admin/interests/{interest}/status; /management/history; /management/interests; /management/interests/{interest}; /management/interests/{interest}/consent/contact; /management/interests/{interest}/consent/proponent; /management/opportunities; /management/opportunities/create; /management/opportunities/drafts; /management/opportunities/{id}/draft; /management/opportunities/{id}/edit; /management/opportunities/{id}/lifecycle,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.","outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual. | 02/11/2026: profile-json preserva erros por campo e reautenticacao em falhas HTML/419 e rejeita JSON invalido; contexto de investimento rejeita falso sucesso HTTP 200 e liberta keepalive, sem repetir escritas. 26 testes focados, TypeScript e build aprovados. Prova local parcial; sem dados/permissoes/publicacao. | Retoma 02/11/2026: profileJson GET independente de cookie CSRF malformado; POST recusa antes do transporte com mensagem de recuperacao. Preservadas validacao e credenciais. 14 testes focados aprovados, incluindo separadores de cookie sem espaco; sem mudanca de direitos.",Sem evidência suficiente,Localizar obrigação verificável e consumidores/contrato atuais antes de estimar implementação ou declarar conclusão. Critério individual original preservado; cobertura atual ainda não individualizada.,"Ler secção/versão correspondente; distinguir requisito funcional, decisão e exemplo. Fontes oficiais já aceites. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.",RF-MN-002: Uma organização pode ter vários utilizadores; cada acção identifica o utilizador e a função.,P2,RF-MN-002: Relatório 25/09 conservado; estado histórico não promove conclusão,RF-MN-002,1,"[{""id"":""RF-MN-002"",""pedido"":""Gerir perfis, representantes, funções e organizações."",""fontes"":[""upgrade v4 SHA f0b9815…; p. 2961; requirements-audit.csv""],""aceitacao"":""Uma organização pode ter vários utilizadores; cada acção identifica o utilizador e a função."",""estado"":""parcial"",""variantes"":""Relatório 25/09 conservado; estado histórico não promove conclusão"",""modulo"":""governação/outros""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,Requisito transversal; consultar estado-ativo-after.json,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.",Localizar obrigação verificável e consumidores/contrato atuais antes de estimar implementação ou declarar conclusão. Critério individual original preservado; cobertura atual ainda não individualizada.,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer","Ler secção/versão correspondente; distinguir requisito funcional, decisão e exemplo. Fontes oficiais já aceites.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: governação e demais cláusulas. Esforço de código adicional só após lacuna reproduzida.,outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,governação e demais cláusulas,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",AGENTS.md — componentes do domínio encontrados; correspondência integral deste requisito não presumida,; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.,Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,,
-RF-MN-003,"Criar, guardar, editar e submeter ofertas e procuras.",upgrade v4 SHA f0b9815…; p. 2965; requirements-audit.csv | portal do investidor\upgrade do portal de investimentos.docx; SHA 471012abb0f56377956f4ab822fe3186e2561bee0471508a5d807091f2e0154f; w:p 581 | portal do investidor\Portal V4\upgrade do portal de investimentos v6 programacao.docx; SHA e1169a7743b13ff6c950d3ffd4672a2afea32e81da84885e11154b7d8de9241e; w:p 3417 | portal do investidor\Portal V4\upgrade do portal de investimentos v1.docx; SHA f70ce3075523c6278e00dce15b9c7acf2b690679e16959ae5f7d007e754cfc6e; w:p 588 | portal do investidor\Portal V4\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 2222 | portal do investidor\Portal V4\upgrade do portal de investimentos v5 programacao.docx; SHA edece80c0519537244f8a8b86456af0aa92de1c526f791bb0ee9b85af7a7e8b2; w:p 3417,governação/outros,/api/v1/investment/admin/interests/{interest}/status; /management/history; /management/interests; /management/interests/{interest}; /management/interests/{interest}/consent/contact; /management/interests/{interest}/consent/proponent; /management/opportunities; /management/opportunities/create; /management/opportunities/drafts; /management/opportunities/{id}/draft; /management/opportunities/{id}/edit; /management/opportunities/{id}/lifecycle; /ofertas; /ofertas/{id}; /api/v1/offers; /api/v1/offers/{id}; /api/v1/offers/map; /api/v1/offers/export; /minhas-oportunidades/ofertas/{id}/recursos; /minhas-oportunidades/ofertas/{id}/confidencialidade; /minhas-oportunidades/ofertas/{id}/confidencial; /minhas-oportunidades/ofertas/{id}/nda,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual. | 14/11/2026 - Ofertas F1: oportunidades como cadastro principal; criar, guardar, recuperar, editar e submeter em privado; autorizacao existente, CRM e localizacao reutilizados; sem menu, publicacao ou instalacao. | 14/11/2026 F2 autorizada: circuito editorial privado no workflow e Dashboard existentes; revisao/atribuicao/rejeicao/correccao/reenvio/aprovacao interna; decisoes por snapshot/hash/ciclo, tarefas, auditoria e notificacoes. Sem publicacao ou instalacao. | 14/11/2026 F3 autorizada: consulta read-only da projeccao publica separada, filtros por categoria/localizacao/preco/moeda, grelha/lista/mapa administrativo e CSV. Barreira efectiva fechada; apenas amostras sinteticas em QA isolado podem ser consumidas por injeccao de teste. | 14/11/2026 F4 autorizada, campos propostos confirmados: particularidades opcionais de empresas/imoveis no mesmo rascunho; seis fotografias activas com edicao/retirada/restauro; originais cifrados e scan existente; comprovativos exclusivamente para validadores internos com ambito/MFA. NDA e autorizacao expressa do vendedor vinculados a versao/hash, revogacao e auditoria; exemplar juridico real pendente, configuracao nula. Sem publicacao ou instalacao.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.","outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual. | QA: PrivateOfferPreparationTest.php; regressao 35 testes/548 assercoes e passagem final focada 16/172 (grupos sobrepostos, nao somar). outputs/offers-private-phase1-20261114/qa-result.json. | F2 QA: 35 testes/499 assercoes; final focado 11/160; complemento contribuidores 1/14 e HTML compilado 1/10 aprovados, grupos sobrepostos. tests/Feature/PrivateOfferEditorialTest.php; outputs/offers-private-phase2-20261114/qa-result.json. | F3 QA: tests/Feature/OfferPublicCatalogueTest.php; outputs/offers-public-phase3-20261114/qa-result.json; sem instalacao ou publicacao real. | F4: tests/Feature/PrivateOfferConfidentialityTest.php; outputs/offers-private-phase4-20261114/qa-result.json. Cinco casos territoriais desbloqueados com fixture sintetico guardado em transaccao QA; sem dados operacionais.",Parcial - Ofertas F1/F2/F3 aceites tecnicamente; F4 implementada em QA com NDA real pendente; publicacao bloqueada; nao instalada,"NDA aprovado: exemplar, versao e referencia de aprovacao por disponibilizar. Tipos juridicos de comprovativos por identificar; sem certificacao de titularidade. Visual/movel e email real F2 pendentes. Derivadas: GD/WebP indisponivel no PHP QA. Publicacao efectiva bloqueada; F5 nao iniciada. Procuras permanece por implementar.",Autorizacao expressa para Fase 3 e para instalacao; designacoes operacionais e configuracao de entrega reais apenas quando aplicaveis. Sem novos papeis ou permissoes por inferencia.,"RF-MN-003: Rascunho persiste; submissão bloqueia campos essenciais e gera número de protocolo. | MASTER-471012ab-0052: Criar, guardar, editar e submeter ofertas e procuras. | MASTER-e1169a77-0202: Criar, guardar, editar e submeter ofertas e procuras. | MASTER-f70ce307-0058: Criar, guardar, editar e submeter ofertas e procuras. | MASTER-51b8f0ad-0138: Criar, guardar, editar e submeter ofertas e procuras. | MASTER-edece80c-0202: Criar, guardar, editar e submeter ofertas e procuras.",P2,RF-MN-003: Relatório 25/09 conservado; estado histórico não promove conclusão | MASTER-471012ab-0052: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-e1169a77-0202: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-f70ce307-0058: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-51b8f0ad-0138: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-edece80c-0202: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,RF-MN-003 | MASTER-471012ab-0052 | MASTER-e1169a77-0202 | MASTER-f70ce307-0058 | MASTER-51b8f0ad-0138 | MASTER-edece80c-0202,6,"[{""id"":""RF-MN-003"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""upgrade v4 SHA f0b9815…; p. 2965; requirements-audit.csv""],""aceitacao"":""Rascunho persiste; submissão bloqueia campos essenciais e gera número de protocolo."",""estado"":""parcial"",""variantes"":""Relatório 25/09 conservado; estado histórico não promove conclusão"",""modulo"":""governação/outros""},{""id"":""MASTER-471012ab-0052"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\upgrade do portal de investimentos.docx; SHA 471012abb0f56377956f4ab822fe3186e2561bee0471508a5d807091f2e0154f; w:p 581""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-e1169a77-0202"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v6 programacao.docx; SHA e1169a7743b13ff6c950d3ffd4672a2afea32e81da84885e11154b7d8de9241e; w:p 3417""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-f70ce307-0058"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v1.docx; SHA f70ce3075523c6278e00dce15b9c7acf2b690679e16959ae5f7d007e754cfc6e; w:p 588""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-51b8f0ad-0138"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 2222""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-edece80c-0202"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v5 programacao.docx; SHA edece80c0519537244f8a8b86456af0aa92de1c526f791bb0ee9b85af7a7e8b2; w:p 3417""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""}]","QA - preparacao, revisao e historico privados; filtros e acesso pelo Dashboard existente; visual manual pendente",QA - operacoes autenticadas /minhas-oportunidades/ofertas e /{id}/revisao; MFA/ambito/conflitos verificados no servidor; publicacao recusada,"QA - cadastro Oportunidades, tarefas/auditoria/notificacoes nativos; snapshots e hashes por ciclo; sem tabelas ou migracoes novas","QA - recuperar oferta, correccao, decisao e versoes anteriores; conflitos, rollback e indisponibilidade sem downgrade | F4: guardar/recuperar particularidades; fotografias e provas no recurso existente; historico de aceitacao/autorizacao/retirada/revogacao por exemplar e versao, sem substituir dados ou decisoes.","QA - aprovacao interna privada distinta de publicacao; versao publica nativa anterior preservada | F3: projecao publica anterior preservada durante edicao/rejeicao da nova revisao; sem contacto, anexos ou coordenadas do negocio; moeda sem conversao. | F4: dados empresariais reservados so depois de NDA + autorizacao expressa do vendedor; comprovativos nunca entregues ao interessado.",Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,Requisito transversal; consultar estado-ativo-after.json,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.",Localizar obrigação verificável e consumidores/contrato atuais antes de estimar implementação ou declarar conclusão. Critério individual original preservado; cobertura atual ainda não individualizada.,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer","Ler secção/versão correspondente; distinguir requisito funcional, decisão e exemplo. Fontes oficiais já aceites.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: governação e demais cláusulas. Esforço de código adicional só após lacuna reproduzida.,outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,governação e demais cláusulas,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",AGENTS.md — componentes do domínio encontrados; correspondência integral deste requisito não presumida,; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.,Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,,
+RF-MN-003,"Criar, guardar, editar e submeter ofertas e procuras.",upgrade v4 SHA f0b9815…; p. 2965; requirements-audit.csv | portal do investidor\upgrade do portal de investimentos.docx; SHA 471012abb0f56377956f4ab822fe3186e2561bee0471508a5d807091f2e0154f; w:p 581 | portal do investidor\Portal V4\upgrade do portal de investimentos v6 programacao.docx; SHA e1169a7743b13ff6c950d3ffd4672a2afea32e81da84885e11154b7d8de9241e; w:p 3417 | portal do investidor\Portal V4\upgrade do portal de investimentos v1.docx; SHA f70ce3075523c6278e00dce15b9c7acf2b690679e16959ae5f7d007e754cfc6e; w:p 588 | portal do investidor\Portal V4\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 2222 | portal do investidor\Portal V4\upgrade do portal de investimentos v5 programacao.docx; SHA edece80c0519537244f8a8b86456af0aa92de1c526f791bb0ee9b85af7a7e8b2; w:p 3417,governação/outros,/api/v1/investment/admin/interests/{interest}/status; /management/history; /management/interests; /management/interests/{interest}; /management/interests/{interest}/consent/contact; /management/interests/{interest}/consent/proponent; /management/opportunities; /management/opportunities/create; /management/opportunities/drafts; /management/opportunities/{id}/draft; /management/opportunities/{id}/edit; /management/opportunities/{id}/lifecycle; /ofertas; /ofertas/{id}; /api/v1/offers; /api/v1/offers/{id}; /api/v1/offers/map; /api/v1/offers/export; /minhas-oportunidades/ofertas/{id}/recursos; /minhas-oportunidades/ofertas/{id}/confidencialidade; /minhas-oportunidades/ofertas/{id}/confidencial; /minhas-oportunidades/ofertas/{id}/nda; /minhas-oportunidades/ofertas/{id}/operacoes; /minhas-oportunidades/ofertas/{id}/denunciar; /minhas-oportunidades/ofertas/{id}/denuncias; /minhas-oportunidades/ofertas/{id}/destaque-administrativo,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual. | 14/11/2026 - Ofertas F1: oportunidades como cadastro principal; criar, guardar, recuperar, editar e submeter em privado; autorizacao existente, CRM e localizacao reutilizados; sem menu, publicacao ou instalacao. | 14/11/2026 F2 autorizada: circuito editorial privado no workflow e Dashboard existentes; revisao/atribuicao/rejeicao/correccao/reenvio/aprovacao interna; decisoes por snapshot/hash/ciclo, tarefas, auditoria e notificacoes. Sem publicacao ou instalacao. | 14/11/2026 F3 autorizada: consulta read-only da projeccao publica separada, filtros por categoria/localizacao/preco/moeda, grelha/lista/mapa administrativo e CSV. Barreira efectiva fechada; apenas amostras sinteticas em QA isolado podem ser consumidas por injeccao de teste. | 14/11/2026 F4 autorizada, campos propostos confirmados: particularidades opcionais de empresas/imoveis no mesmo rascunho; seis fotografias activas com edicao/retirada/restauro; originais cifrados e scan existente; comprovativos exclusivamente para validadores internos com ambito/MFA. NDA e autorizacao expressa do vendedor vinculados a versao/hash, revogacao e auditoria; exemplar juridico real pendente, configuracao nula. Sem publicacao ou instalacao. | 14/11/2026 F5 autorizada em QA: retirada imediata pelo dono, novo ciclo/v4 e aprovacao antes da reactivacao; validade congelada por versao, prazo configuravel nulo por defeito, expiracao independente de NDA/cron; denuncias cifradas com origem versionada, tarefas e Dashboard existentes; planos administrativos de destaque sem efeitos comerciais. Publicacao e instalacao bloqueadas.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.","outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual. | QA: PrivateOfferPreparationTest.php; regressao 35 testes/548 assercoes e passagem final focada 16/172 (grupos sobrepostos, nao somar). outputs/offers-private-phase1-20261114/qa-result.json. | F2 QA: 35 testes/499 assercoes; final focado 11/160; complemento contribuidores 1/14 e HTML compilado 1/10 aprovados, grupos sobrepostos. tests/Feature/PrivateOfferEditorialTest.php; outputs/offers-private-phase2-20261114/qa-result.json. | F3 QA: tests/Feature/OfferPublicCatalogueTest.php; outputs/offers-public-phase3-20261114/qa-result.json; sem instalacao ou publicacao real. | F4: tests/Feature/PrivateOfferConfidentialityTest.php; outputs/offers-private-phase4-20261114/qa-result.json. Cinco casos territoriais desbloqueados com fixture sintetico guardado em transaccao QA; sem dados operacionais. | F5: tests/Feature/PrivateOfferOperationsTest.php; outputs/offers-private-phase5-20261114/qa-result.json. Matriz de aceitacao das cinco fases no relatorio ofertas-fase5-fecho-qa-20261114.md; provas sinteticas e regressao afectada, sem testes gerais de outros modulos.",Parcial - Ofertas F1-F4 tecnicamente aceites com ressalvas; F5 implementada em QA para apreciacao; cinco fases nao instaladas; publicacao efectiva bloqueada,"Dependencias para uso publico: exemplar/versao/aprovacao do NDA; tipos juridicos de comprovativos; prazo/politica final de validade; avisos juridicos, visual/movel, GD/WebP e email real F2. Politica comercial/destaque activo nao definidos e nao activados. Publicacao/instalacao exigem autorizacao expressa. Procuras permanece por implementar.",Autorizacao expressa para Fase 3 e para instalacao; designacoes operacionais e configuracao de entrega reais apenas quando aplicaveis. Sem novos papeis ou permissoes por inferencia.,"RF-MN-003: Rascunho persiste; submissão bloqueia campos essenciais e gera número de protocolo. | MASTER-471012ab-0052: Criar, guardar, editar e submeter ofertas e procuras. | MASTER-e1169a77-0202: Criar, guardar, editar e submeter ofertas e procuras. | MASTER-f70ce307-0058: Criar, guardar, editar e submeter ofertas e procuras. | MASTER-51b8f0ad-0138: Criar, guardar, editar e submeter ofertas e procuras. | MASTER-edece80c-0202: Criar, guardar, editar e submeter ofertas e procuras.",P2,RF-MN-003: Relatório 25/09 conservado; estado histórico não promove conclusão | MASTER-471012ab-0052: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-e1169a77-0202: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-f70ce307-0058: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-51b8f0ad-0138: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-edece80c-0202: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,RF-MN-003 | MASTER-471012ab-0052 | MASTER-e1169a77-0202 | MASTER-f70ce307-0058 | MASTER-51b8f0ad-0138 | MASTER-edece80c-0202,6,"[{""id"":""RF-MN-003"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""upgrade v4 SHA f0b9815…; p. 2965; requirements-audit.csv""],""aceitacao"":""Rascunho persiste; submissão bloqueia campos essenciais e gera número de protocolo."",""estado"":""parcial"",""variantes"":""Relatório 25/09 conservado; estado histórico não promove conclusão"",""modulo"":""governação/outros""},{""id"":""MASTER-471012ab-0052"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\upgrade do portal de investimentos.docx; SHA 471012abb0f56377956f4ab822fe3186e2561bee0471508a5d807091f2e0154f; w:p 581""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-e1169a77-0202"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v6 programacao.docx; SHA e1169a7743b13ff6c950d3ffd4672a2afea32e81da84885e11154b7d8de9241e; w:p 3417""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-f70ce307-0058"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v1.docx; SHA f70ce3075523c6278e00dce15b9c7acf2b690679e16959ae5f7d007e754cfc6e; w:p 588""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-51b8f0ad-0138"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 2222""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-edece80c-0202"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v5 programacao.docx; SHA edece80c0519537244f8a8b86456af0aa92de1c526f791bb0ee9b85af7a7e8b2; w:p 3417""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""}]","QA - preparacao, revisao e historico privados; filtros e acesso pelo Dashboard existente; visual manual pendente",QA - operacoes autenticadas /minhas-oportunidades/ofertas e /{id}/revisao; MFA/ambito/conflitos verificados no servidor; publicacao recusada,"QA - cadastro Oportunidades, tarefas/auditoria/notificacoes nativos; snapshots e hashes por ciclo; sem tabelas ou migracoes novas","QA - recuperar oferta, correccao, decisao e versoes anteriores; conflitos, rollback e indisponibilidade sem downgrade | F4: guardar/recuperar particularidades; fotografias e provas no recurso existente; historico de aceitacao/autorizacao/retirada/revogacao por exemplar e versao, sem substituir dados ou decisoes. | F5: retirada sem perda de conteudo; revalidacao no mesmo identificador; historico de ciclos, expiracao, denuncias e planos administrativos com conflitos/etags.","QA - aprovacao interna privada distinta de publicacao; versao publica nativa anterior preservada | F3: projecao publica anterior preservada durante edicao/rejeicao da nova revisao; sem contacto, anexos ou coordenadas do negocio; moeda sem conversao. | F4: dados empresariais reservados so depois de NDA + autorizacao expressa do vendedor; comprovativos nunca entregues ao interessado. | F5: sem reexposicao de publicacao/grants antigos ao reactivar; exclusao uniforme em listas/filtros/mapas/API/CSV; validade opcional visivel quando definida; destaques administrativos privados.",Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,Requisito transversal; consultar estado-ativo-after.json,"Ofertas F1-F4 tecnicamente aceites com ressalvas nos commits 1f2990e4/91c18573/7691cab3/e431de8b. F5 desenvolvida em QA por autorizacao expressa de 14/11/2026, com retirada, revalidacao, validade configuravel, denuncias e preparacao administrativa. Publicacao efectiva e instalacao nao autorizadas.","Apreciacao da F5; NDA aprovado/exemplar/versao; tipos juridicos de comprovativos; prazo final de validade; avisos juridicos, visual/movel, GD/WebP e email real F2. Procuras nao abrangidas por este fecho de Ofertas.","Opportunity, PortalProfileAccess, OpportunityProfileWorkflow, OpportunityWorkflowTasks, documents/recursos, auditoria, Dashboard, filtros/mapa/API/CSV existentes; sem cadastro paralelo.",Documentos/decisoes concretos da Tempo e ferramenta visual; nao representam falta de autorizacao para este desenvolvimento QA. Instalacao/publicacao dependem de autorizacao expressa futura.,"Autorizacao F5 recebida: desenvolvimento exclusivo QA, sem publicacao, instalacao, pagamentos, dados operacionais ou mne. Prazo final e NDA nao presumidos; config por defeito nula.",Implementacao e verificacao QA de Ofertas; dependencias documentais/visuais e eventual entrega futura separadas.,Sem nova estimativa temporal nesta entrega; testes sobrepostos nao devem ser somados. Historico de estimativas conservado nas colunas anteriores.,docs/investmoz/v2/auditoria-portal/ofertas-fase5-fecho-qa-20261114.md; outputs/offers-private-phase5-20261114/qa-result.json; provas aceites F1-F4 referenciadas na matriz de aceitacao das cinco fases.,governação e demais cláusulas,14/11/2026 - fecho operacional de Ofertas em QA; visual/instalacao/publicacao com estados separados,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",AGENTS.md — componentes do domínio encontrados; correspondência integral deste requisito não presumida,; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.,Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,,
RF-MN-004,"Aplicar campos condicionais por tipo de pessoa, modalidade e fase.",upgrade v4 SHA f0b9815…; p. 2969; requirements-audit.csv,governação/outros,/api/v1/investment/admin/interests/{interest}/status; /management/history; /management/interests; /management/interests/{interest}; /management/interests/{interest}/consent/contact; /management/interests/{interest}/consent/proponent; /management/opportunities; /management/opportunities/create; /management/opportunities/drafts; /management/opportunities/{id}/draft; /management/opportunities/{id}/edit; /management/opportunities/{id}/lifecycle,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.",outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,Sem evidência suficiente,Localizar obrigação verificável e consumidores/contrato atuais antes de estimar implementação ou declarar conclusão. Critério individual original preservado; cobertura atual ainda não individualizada.,"Ler secção/versão correspondente; distinguir requisito funcional, decisão e exemplo. Fontes oficiais já aceites. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.","RF-MN-004: Campos aparecem e tornam-se obrigatórios conforme regras da matriz, sem perda de dados válidos.",P2,RF-MN-004: Relatório 25/09 conservado; estado histórico não promove conclusão,RF-MN-004,1,"[{""id"":""RF-MN-004"",""pedido"":""Aplicar campos condicionais por tipo de pessoa, modalidade e fase."",""fontes"":[""upgrade v4 SHA f0b9815…; p. 2969; requirements-audit.csv""],""aceitacao"":""Campos aparecem e tornam-se obrigatórios conforme regras da matriz, sem perda de dados válidos."",""estado"":""parcial"",""variantes"":""Relatório 25/09 conservado; estado histórico não promove conclusão"",""modulo"":""governação/outros""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,Requisito transversal; consultar estado-ativo-after.json,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.",Localizar obrigação verificável e consumidores/contrato atuais antes de estimar implementação ou declarar conclusão. Critério individual original preservado; cobertura atual ainda não individualizada.,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer","Ler secção/versão correspondente; distinguir requisito funcional, decisão e exemplo. Fontes oficiais já aceites.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: governação e demais cláusulas. Esforço de código adicional só após lacuna reproduzida.,outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,governação e demais cláusulas,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",AGENTS.md — componentes do domínio encontrados; correspondência integral deste requisito não presumida,; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.,Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,,
RF-MN-005,"Validar formatos, intervalos, dependências, duplicados e consentimentos.",upgrade v4 SHA f0b9815…; p. 2973; requirements-audit.csv | portal do investidor\upgrade do portal de investimentos.docx; SHA 471012abb0f56377956f4ab822fe3186e2561bee0471508a5d807091f2e0154f; w:p 589 | portal do investidor\Portal V4\upgrade do portal de investimentos v6 programacao.docx; SHA e1169a7743b13ff6c950d3ffd4672a2afea32e81da84885e11154b7d8de9241e; w:p 3425 | portal do investidor\Portal V4\upgrade do portal de investimentos v1.docx; SHA f70ce3075523c6278e00dce15b9c7acf2b690679e16959ae5f7d007e754cfc6e; w:p 596 | portal do investidor\Portal V4\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 2230 | portal do investidor\Portal V4\upgrade do portal de investimentos v5 programacao.docx; SHA edece80c0519537244f8a8b86456af0aa92de1c526f791bb0ee9b85af7a7e8b2; w:p 3425,governação/outros,/api/v1/investment/admin/interests/{interest}/status; /management/history; /management/interests; /management/interests/{interest}; /management/interests/{interest}/consent/contact; /management/interests/{interest}/consent/proponent; /management/opportunities; /management/opportunities/create; /management/opportunities/drafts; /management/opportunities/{id}/draft; /management/opportunities/{id}/edit; /management/opportunities/{id}/lifecycle,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.",outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,Sem evidência suficiente,Localizar obrigação verificável e consumidores/contrato atuais antes de estimar implementação ou declarar conclusão. Critério individual original preservado; cobertura atual ainda não individualizada.,"Ler secção/versão correspondente; distinguir requisito funcional, decisão e exemplo. Fontes oficiais já aceites. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.","RF-MN-005: O sistema impede submissão inválida e apresenta mensagem associada ao campo e orientação de correcção. | MASTER-471012ab-0054: Validar formatos, intervalos, dependências, duplicados e consentimentos. | MASTER-e1169a77-0204: Validar formatos, intervalos, dependências, duplicados e consentimentos. | MASTER-f70ce307-0060: Validar formatos, intervalos, dependências, duplicados e consentimentos. | MASTER-51b8f0ad-0140: Validar formatos, intervalos, dependências, duplicados e consentimentos. | MASTER-edece80c-0204: Validar formatos, intervalos, dependências, duplicados e consentimentos.",P2,RF-MN-005: Relatório 25/09 conservado; estado histórico não promove conclusão | MASTER-471012ab-0054: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-e1169a77-0204: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-f70ce307-0060: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-51b8f0ad-0140: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-edece80c-0204: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,RF-MN-005 | MASTER-471012ab-0054 | MASTER-e1169a77-0204 | MASTER-f70ce307-0060 | MASTER-51b8f0ad-0140 | MASTER-edece80c-0204,6,"[{""id"":""RF-MN-005"",""pedido"":""Validar formatos, intervalos, dependências, duplicados e consentimentos."",""fontes"":[""upgrade v4 SHA f0b9815…; p. 2973; requirements-audit.csv""],""aceitacao"":""O sistema impede submissão inválida e apresenta mensagem associada ao campo e orientação de correcção."",""estado"":""parcial"",""variantes"":""Relatório 25/09 conservado; estado histórico não promove conclusão"",""modulo"":""governação/outros""},{""id"":""MASTER-471012ab-0054"",""pedido"":""Validar formatos, intervalos, dependências, duplicados e consentimentos."",""fontes"":[""portal do investidor\\upgrade do portal de investimentos.docx; SHA 471012abb0f56377956f4ab822fe3186e2561bee0471508a5d807091f2e0154f; w:p 589""],""aceitacao"":""Validar formatos, intervalos, dependências, duplicados e consentimentos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-e1169a77-0204"",""pedido"":""Validar formatos, intervalos, dependências, duplicados e consentimentos."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v6 programacao.docx; SHA e1169a7743b13ff6c950d3ffd4672a2afea32e81da84885e11154b7d8de9241e; w:p 3425""],""aceitacao"":""Validar formatos, intervalos, dependências, duplicados e consentimentos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-f70ce307-0060"",""pedido"":""Validar formatos, intervalos, dependências, duplicados e consentimentos."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v1.docx; SHA f70ce3075523c6278e00dce15b9c7acf2b690679e16959ae5f7d007e754cfc6e; w:p 596""],""aceitacao"":""Validar formatos, intervalos, dependências, duplicados e consentimentos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-51b8f0ad-0140"",""pedido"":""Validar formatos, intervalos, dependências, duplicados e consentimentos."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 2230""],""aceitacao"":""Validar formatos, intervalos, dependências, duplicados e consentimentos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-edece80c-0204"",""pedido"":""Validar formatos, intervalos, dependências, duplicados e consentimentos."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v5 programacao.docx; SHA edece80c0519537244f8a8b86456af0aa92de1c526f791bb0ee9b85af7a7e8b2; w:p 3425""],""aceitacao"":""Validar formatos, intervalos, dependências, duplicados e consentimentos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,Requisito transversal; consultar estado-ativo-after.json,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.
diff --git a/docs/investmoz/v2/auditoria-portal/ofertas-fase5-fecho-qa-20261114.md b/docs/investmoz/v2/auditoria-portal/ofertas-fase5-fecho-qa-20261114.md
new file mode 100644
index 00000000..842500d7
--- /dev/null
+++ b/docs/investmoz/v2/auditoria-portal/ofertas-fase5-fecho-qa-20261114.md
@@ -0,0 +1,110 @@
+# Ofertas — Fase 5: fecho operacional em QA e aceitação das cinco fases
+
+14/11/2026. Autorização expressa da Tempo para desenvolvimento exclusivo em QA. Base aceite com ressalvas: `e431de8b` (Fase 4). Mantêm-se `7691cab3`, `91c18573`, `1f2990e4` e o plano `6e302db6`. Referência funcional: RF-MN-003 da matriz existente.
+
+**Fecho operacional implementado em QA. Publicação efectiva continua bloqueada. Nenhuma instalação em homologação/produção, alteração de dados operacionais, migração ou modificação em `mne`.** A aceitação desta entrega não é inferida da autorização para a desenvolver.
+
+## Disponibilidade, retirada e reactivação
+
+O dono registado da Oferta, com atribuição e representação ainda efectivas, pode retirar imediatamente a disponibilidade. A operação protectora não exige decisão editorial, MFA adicional do promotor ou uma versão de formulário actual: bloqueia o registo, reconfirma o dono e retira a versão corrente. Uma repetição não duplica a retirada. Não altera o conteúdo, apaga decisões ou converte o cadastro noutro produto.
+
+Disponibilidade e estado editorial são distintos. Uma aprovação privada pode continuar registada, mas a retirada exclui a Oferta das consultas, detalhe, filtros, mapa e CSV e fecha a leitura empresarial protegida pelo NDA. Uma consulta anterior já entregue ao utilizador não pode ser recolhida retroactivamente.
+
+Para voltar a disponibilizar, o proprietário pede nova validação no mesmo identificador. Abre-se um ciclo operacional que integra o hash do snapshot editorial (`private-offer-editorial-v4`); os contratos v2/v3 anteriores mantêm-se quando não há esse ciclo. O motor editorial existente conserva submissões, responsáveis, tarefas e decisões, reabre a preparação e exige submissão/revisão/aprovação independente. Só depois o proprietário pode repor a disponibilidade privada. Uma aprovação anterior ou um pedido de reactivação não bastam.
+
+Uma retirada durante revisão também pode abrir esse novo ciclo, fechando as tarefas anteriores pelo mecanismo existente. Um primeiro rascunho retirado continua a exigir a sua primeira validação. Preparações F1 antigas sem snapshot seguem primeiro a vinculação editorial já existente, sem fabricar um histórico de referências anterior.
+
+Reactivar disponibilidade **não publica**. O consumidor não reapresenta o snapshot público antigo: para um ciclo novo, exige a correspondência de `offer_cycle` e `editorial_hash` com o ciclo e a aprovação activados. Esses campos são o contrato de leitura futura, não um publicador. Nos testes, apenas projecções sintéticas são disponibilizadas por injecção interna na base isolada.
+
+## Validade independente do NDA
+
+`config/private_offers.php` acrescenta `validity_days` e `validity_policy_reference`, ambos inicialmente nulos. Não foi escolhido um prazo definitivo. Quando a Tempo definir um prazo, a configuração exige inteiro positivo e referência identificada; o limite técnico de 36.500 dias protege a aritmética e não é uma política comercial/jurídica.
+
+A política é congelada por hash na aprovação, e a contagem começa no instante UTC da publicação dessa versão. A aprovação privada não inicia a contagem. Alterar a configuração ou aprovar uma edição privada não prolonga a validade do snapshot público anterior; o exemplar legado sem `editorial_hash` conserva a primeira política congelada. Uma política pendente não é substituída por zero ou por um prazo presumido.
+
+No instante `agora >= publicação + duração`, a Oferta deixa de aparecer nas leituras. Essa exclusão não depende de cron, execução de um job, aceitação ou duração do NDA. A API acrescenta `expires_at` apenas quando a data é conhecida, e o cartão público mostra-a. O formato anterior do CSV foi preservado; os seus registos usam a mesma exclusão por disponibilidade/expiração. As respostas públicas de Ofertas usam `no-store` para não conservar uma versão retirada em caches HTTP.
+
+O comando `investmoz:offers-expire` confere por defeito **sem escrita**. Com `--confirm`, materializa uma expiração já efectiva e regista o evento uma vez, com actor de sistema nulo, sem criar utilizadores. O agendamento é optativo por `expiry_scheduler_enabled`, que permanece **false**. Não foi activado no ambiente operacional. Os ensaios do comando ocorreram exclusivamente na base de testes guardada.
+
+## Denúncias e Dashboard Colaborativo
+
+A Oferta dispõe de formulário de denúncia fundamentada. O utilizador deve estar autenticado, com email verificado, e poder consultar a versão pública ou já ter acesso privado legítimo ao registo. Um estranho não usa este percurso para descobrir um rascunho. A fundamentação é obrigatória; limites técnicos de texto e o throttle existente limitam os pedidos.
+
+Cada envio guarda identificador idempotente, autor, instante, versão do registo, hashes de referência e cópia cifrada do conteúdo referido. O fundamento também fica cifrado. Uma retoma com o mesmo actor/chave/conteúdo devolve o recibo sem criar outra tarefa, mesmo que entretanto a Oferta tenha sido retirada. Reutilizar a chave para outro conteúdo é conflito; outro actor não recebe o recibo.
+
+O mesmo `OpportunityWorkflowTasks` cria uma tarefa CRM restrita, sem inventar oportunidade CRM, responsável ou transporte. A entrada **Ofertas — acompanhamento**, dentro do Dashboard Colaborativo existente, abre a listagem privada existente em modo operacional, com filtro de denúncias pendentes. Não há outro dashboard nem alteração do menu principal.
+
+O validador interno com âmbito, MFA recente e sem conflito inicia a análise; fica associado à tarefa e à versão da denúncia. Conclui com fundamento, sem retirada ou com retirada da disponibilidade. Etag, versão da denúncia, estado e responsável são reconferidos no servidor sob bloqueio. O dono, auditor, outro âmbito ou outra conta não substituem essa autorização. A conclusão preserva os factos originais e o histórico; não é apresentada como decisão judicial ou certificação de fraude.
+
+O proprietário não recebe o fundamento original ou os dados do denunciante pela API de acompanhamento. A leitura desses dados exige o papel interno referido. A origem cifrada permite conferir a versão denunciada, mesmo que a preparação corrente seja alterada. Notificações, tarefas, pesquisa pública, mapa e CSV não recebem o texto da denúncia.
+
+## Preparação administrativa de destaques
+
+O Publicador existente, com âmbito, MFA, autorização efectiva e sem conflito, pode guardar uma preparação administrativa com designação, datas previstas, fundamento e estado **rascunho/arquivado**. O plano tem versão, etag, histórico e auditoria próprios no contexto da mesma Oferta.
+
+Não há estado comercial activo, alteração de `is_featured`, preço de destaque, cobrança, pagamento ou integração financeira. O proprietário e o público não recebem esse plano. A actualização de disponibilidade/denúncias preserva o plano local em edição; carregar o plano guardado exige uma acção explícita que identifica a substituição dos campos locais.
+
+## Segurança, confidencialidade e compatibilidade
+
+- Operações privadas reutilizam autenticação, perfis, representação, MFA, CSRF, limites de pedidos e respostas `no-store` existentes. As escritas usam transacções/bloqueio do registo; versões e etags impedem decisões sobre uma revisão desconhecida. A única dispensa de etag é a retirada protectora pelo dono, para não manter uma exposição por causa de um formulário antigo.
+- A retirada/expiração fecha imediatamente a leitura protegida da empresa. Uma nova validação tem hash/ciclo novo; não reaproveita silenciosamente a aceitação/autorização NDA anterior. O interessado conserva a possibilidade de retirar uma aceitação antiga durante revisão. Não se apagaram os seus registos históricos.
+- A Fase 4 mantém comprovativos apenas para validadores internos autorizados, originais cifrados, limite de seis fotografias, quarentena e análise de segurança. Não se acrescentou partilha externa de originais ou coordenadas exactas.
+- A projecção pública continua separada da edição privada; a barreira normal continua fechada. APIs nativas dos outros produtos não passam a devolver Ofertas. Filtros, detalhe, mapas e CSV de Ofertas usam a mesma condição de disponibilidade e expiração. A propriedade opcional `expires_at` não altera as chaves dos exemplares antigos sem validade definida.
+- Auditoria conserva actor, instante, versão, ciclo e identificadores de decisão. Fundamentos e origem das denúncias ficam cifrados; não há valores financeiros, contactos ou documentos nos títulos das tarefas ou nos eventos públicos.
+
+## Verificação
+
+Base: `offers_phase1_20261114_test`, loopback, porta **55441**, guardas existentes e dados exclusivamente sintéticos. Não foram utilizados processos reais, credenciais de negócio, publicação real ou entrega externa de email. O scanner sintético da Fase 4 mantém a identificação de teste e não foi registado no produto.
+
+Os resultados e hashes finais encontram-se em `outputs/offers-private-phase5-20261114/qa-result.json`. A passagem inicial focada teve **12 testes/151 asserções aprovados**. A regressão integrada terminou com **59 testes/780 asserções aprovados**, F1–F5, com os assets estabilizados. Os grupos sobrepostos não devem ser somados.
+
+| Grupo da passagem integrada | Testes | Asserções | Resultado |
+| --- | ---: | ---: | --- |
+| F1 — preparação privada | 14 | 146 | Aprovado |
+| F2 — circuito editorial | 11 | 161 | Aprovado |
+| F3 — consulta e filtros | 10 | 133 | Aprovado |
+| F4 — particularidades/confidencialidade | 11 | 160 | Aprovado |
+| F5 — fecho operacional | 13 | 180 | Aprovado |
+| **Total desta passagem, sem somar a inicial** | **59** | **780** | **Aprovado** |
+
+Pint, PHPStan sem erros, TypeScript, lint/formatação dos oito ficheiros frontend e compilação QA passaram. A formatação global falhou nos mesmos 61 ficheiros preexistentes, fora do delta e com conteúdo Git inalterado. Não foram enfraquecidos testes, adicionadas excepções de análise ou instaladas dependências. A primeira passagem delimitada revelou uma recusa da antiga lista de acções à reabertura durante revisão: foi corrigida exclusivamente para o ciclo operacional pedido pelo dono, mantendo as regras dos restantes percursos.
+
+A prova cobre retirada com formulário antigo, repetição, actor/âmbito incorrectos, novo ciclo e nova validação, ausência de reexposição do snapshot antigo, primeira preparação, retirada durante revisão, preservação do histórico, fronteira exacta da expiração, política nula/configuração congelada, NDA anterior fechado, idempotência da denúncia, fonte cifrada, tarefa/filas nativas, tratamento e conflitos de versão, MFA expirado, auditor, destaque administrativo sem efeitos comerciais e HTML compilado das páginas afectadas.
+
+## Matriz de aceitação das cinco fases
+
+| Fase | Resultado demonstrado em QA | Referência aceite/prova | Instalação/publicação | Ressalvas e pendências |
+| --- | --- | --- | --- | --- |
+| 1 — Preparação privada | Criar, guardar, recuperar, editar e submeter; autorização/isolamento; cadastro de Oportunidades reutilizado | `1f2990e4`; `PrivateOfferPreparationTest` | Não instalada | Visual pendente |
+| 2 — Circuito editorial | Dashboard existente, revisão, rejeição/correcção/reenvio, aprovação por versão, auditoria e notificações internas | `91c18573`, plano `6e302db6`; `PrivateOfferEditorialTest` | Não instalada; aprovação distinta de publicação | Visual e entrega real de email pendentes |
+| 3 — Consulta e filtros | Projecção aprovada/publicada sintética, categorias/localização/preços sem câmbio, mapa administrativo, API/CSV e preservação da versão anterior | `7691cab3`; `OfferPublicCatalogueTest` e compatibilidade F5 | Publicação efectiva bloqueada | Visual/móvel; abertura real não autorizada; limite de representação de 1.000 candidatos/30 territórios preservado |
+| 4 — Particularidades e confidencialidade | Campos confirmados, seis fotografias, comprovativos internos, duas condições NDA/vendedor, revogação/histórico e acessos por versão | `e431de8b`; `PrivateOfferConfidentialityTest` | Não instalada; NDA real inactivo | Exemplar/versão/aprovação do NDA; tipos jurídicos de comprovativo; GD/WebP; visual |
+| 5 — Fecho operacional | Retirada imediata, revalidação/reativação, validade configurável/expiração, denúncias/tarefas/Dashboard, destaques administrativos e compatibilidade | Esta entrega; `PrivateOfferOperationsTest` e recibo QA | Sem instalação ou activação comercial/pública | Apreciação desta entrega; prazo/política final por definir; pendências das fases anteriores preservadas |
+
+Esta matriz integra o relatório solicitado e actualiza RF-MN-003 e o manual existentes; não é um novo inventário geral. O conjunto de Ofertas fica preparado em QA, mas não é declarado operacional em público, instalado, juridicamente fechado ou visualmente aceite. Procuras e o restante portal não são encerrados por esta entrega.
+
+## Ficheiros alterados
+
+| Grupo | Finalidade |
+| --- | --- |
+| `app/Services/OfferAvailability.php`, `PrivateOfferOperations.php` | Disponibilidade/validade, operações do dono, denúncias, tarefas, auditoria e planos administrativos |
+| `OpportunityProfileWorkflow.php`, `PrivateOfferPreparation.php`, `PrivateOfferConfidentiality.php` | Ciclo operacional no snapshot, reabertura/revalidação existentes e exclusão da leitura NDA |
+| `OfferPublicCatalogue.php`, `OfferPublicCatalogueController.php` | Exclusão consistente, contrato anterior preservado, validade opcional e cache HTTP |
+| `PrivateOfferOperationsController.php`, `PrivateOfferPreparationController.php`, `PortalEditorialWorkflowController.php`, `routes/web.php` | Endpoints privados, filas no âmbito e entrada no Dashboard existente |
+| `app/Console/Commands/ExpirePrivateOffers.php`, `routes/console.php`, `config/private_offers.php` | Conferência/materialização da expiração; prazo e agendamento sem activação por defeito |
+| `resources/js/components/offer-operations.tsx`, `public-offer-card.tsx`, `lib/public-offer.ts` | Acompanhamento privado e data pública opcional |
+| Páginas `offers/report`, `offers/preparation`, `offers/preparation-index`, `offers/public-detail`, `management/editorial-workflow` | Denúncia, retirada/reativação, tratamento e preparação administrativa integrados |
+| `tests/Feature/PrivateOfferOperationsTest.php` | Testes novos de segurança, percursos e compatibilidade |
+| Matriz RF-MN-003, manual, este relatório e recibo QA | Saldo e evidência das cinco fases |
+
+## Limitações e decisões pendentes
+
+1. **NDA:** a Tempo confirmou que ainda falta o exemplar/versão e a referência da aprovação. `private_offers.nda` mantém-se nulo. Não foi inventado texto jurídico ou uma assinatura com validade presumida.
+2. **Imóveis:** os tipos jurídicos de comprovativo admissíveis continuam por identificar. Upload/scan/revisão interna não certificam titularidade, licenciamento ou disponibilidade do bem.
+3. **Validade e divulgação:** a Tempo ainda não definiu prazo definitivo. A configuração permanece nula, sem presumir uma duração. Avisos jurídicos e eventuais condições comerciais de uma publicação futura não foram fabricados.
+4. **Visual:** a conferência desktop/móvel das fases anteriores permanece pendente. A última tentativa registada do conector devolveu `The browser access host is unavailable`. Não foram repetidos reinícios, desactivados controlos ou apresentados HTML/compilação como aceitação visual.
+5. **Imagens:** GD/WebP não está disponível neste PHP QA. Os originais privados e as validações da F4 foram demonstrados; geração/inspecção de derivadas e galeria pública não são declaradas concluídas. Não foram instaladas extensões.
+6. **Email:** a entrega real da F2 continua pendente. Não foram activados novos transportes, SMTP, SMS ou destinatários operacionais.
+7. **Qualidade preexistente:** a formatação global mantém os mesmos 61 ficheiros fora do delta. Lint focado, tipos e análise estática são registados separadamente dessa falha anterior.
+
+Próximo passo: apreciação desta entrega QA e resolução das dependências documentais/visuais indicadas. **Qualquer instalação, configuração activa ou publicação aguarda autorização expressa da Tempo.**
diff --git a/outputs/offers-private-phase5-20261114/build-final.log b/outputs/offers-private-phase5-20261114/build-final.log
new file mode 100644
index 00000000..fe258ce0
--- /dev/null
+++ b/outputs/offers-private-phase5-20261114/build-final.log
@@ -0,0 +1,332 @@
+
+> build
+> vp build
+
+npm.cmd : [plugin laravel:fonts] Optimized font fallbacks require the optional "fontaine" package. Install it, or set
+"optimizedFallbacks: false" on your fonts to disable the feature.
+At line:2 char:30
++ ... _BUILD='1'; npm.cmd run build *> outputs/offers-private-phase5-202611 ...
++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ + CategoryInfo : NotSpecified: ([plugin laravel...le the feature.:String) [], RemoteException
+ + FullyQualifiedErrorId : NativeCommandError
+
+transforming...
+✓ 2847 modules transformed.
+rendering chunks...
+computing gzip size...
+public/build-qa-opportunities/assets/layers-BWBAp2CZ.png 0.69 kB
+public/build-qa-opportunities/assets/layers-2x-Bpkbi35X.png 1.25 kB
+public/build-qa-opportunities/assets/marker-icon-hN30_KVU.png 1.46 kB
+public/build-qa-opportunities/assets/noto-sans-400-normal-BTkUljjl.woff2 13.12 kB
+public/build-qa-opportunities/assets/noto-sans-700-normal-CrvTEWhy.woff2 13.46 kB
+public/build-qa-opportunities/assets/noto-sans-500-normal-DCA2Z9Cj.woff2 13.49 kB
+public/build-qa-opportunities/assets/noto-sans-600-normal-Dkgu5AE5.woff2 13.49 kB
+public/build-qa-opportunities/assets/noto-serif-400-normal-BV_iYGc9.woff2 14.40 kB
+public/build-qa-opportunities/assets/noto-serif-700-normal-CIw17UNa.woff2 15.23 kB
+public/build-qa-opportunities/assets/noto-serif-600-normal-BiuDA9L7.woff2 15.31 kB
+public/build-qa-opportunities/assets/noto-sans-400-normal-DPpKfCgK.woff 16.68 kB
+public/build-qa-opportunities/assets/instrument-sans-400-normal-DRC__1Mx.woff2 16.86 kB
+public/build-qa-opportunities/assets/noto-sans-700-normal-ZyrKABVF.woff 16.95 kB
+public/build-qa-opportunities/assets/noto-sans-500-normal-CDPngY5C.woff 17.09 kB
+public/build-qa-opportunities/assets/noto-sans-600-normal-DVJMj_t0.woff 17.10 kB
+public/build-qa-opportunities/assets/instrument-sans-500-normal-Dk9ku72i.woff2 17.23 kB
+public/build-qa-opportunities/assets/instrument-sans-600-normal-B7fBEWYG.woff2 17.40 kB
+public/build-qa-opportunities/assets/noto-serif-400-normal-DT0cwSa_.woff 17.81 kB
+public/build-qa-opportunities/fonts-manifest.json 18.53 kB │ gzip: 1.23 kB
+public/build-qa-opportunities/assets/noto-serif-700-normal-mX-AlFMT.woff 18.54 kB
+public/build-qa-opportunities/assets/noto-serif-600-normal-RZopCp-J.woff 18.68 kB
+public/build-qa-opportunities/assets/instrument-sans-400-normal-D1W7dsQl.woff 21.24 kB
+public/build-qa-opportunities/assets/instrument-sans-500-normal-Z6ESRlEs.woff 21.65 kB
+public/build-qa-opportunities/assets/instrument-sans-600-normal-B9e8oLYv.woff 21.67 kB
+public/build-qa-opportunities/manifest.json 114.60 kB │ gzip: 11.96 kB
+public/build-qa-opportunities/assets/fonts-CcFLQaQ-.css 7.91 kB │ gzip: 0.65 kB
+public/build-qa-opportunities/assets/investment-catalogue-WaRR19FV.css 8.15 kB │ gzip: 2.00 kB
+public/build-qa-opportunities/assets/interactive-investment-map-Bm3WjoQm.css 10.62 kB │ gzip: 2.65 kB
+public/build-qa-opportunities/assets/app-UznX-FqS.css 157.42 kB │ gzip: 25.28 kB
+public/build-qa-opportunities/assets/input-text-BlEFHnL-.js 0.09 kB │ gzip: 0.10 kB
+public/build-qa-opportunities/assets/check-PcBNn_NG.js 0.12 kB │ gzip: 0.14 kB
+public/build-qa-opportunities/assets/company-comparison-qLTgpXjz.js 0.12 kB │ gzip: 0.13 kB
+public/build-qa-opportunities/assets/display-value-DRR1EAAK.js 0.13 kB │ gzip: 0.12 kB
+public/build-qa-opportunities/assets/value-text-BztaYdGl.js 0.14 kB │ gzip: 0.12 kB
+public/build-qa-opportunities/assets/loader-circle-DS8Tf4er.js 0.14 kB │ gzip: 0.15 kB
+public/build-qa-opportunities/assets/shared-B-QpuOU2.js 0.15 kB │ gzip: 0.13 kB
+public/build-qa-opportunities/assets/arrow-left-DMVHIw8Z.js 0.16 kB │ gzip: 0.16 kB
+public/build-qa-opportunities/assets/search-BRvPeo47.js 0.17 kB │ gzip: 0.16 kB
+public/build-qa-opportunities/assets/circle-check-DckzuBoW.js 0.17 kB │ gzip: 0.16 kB
+public/build-qa-opportunities/assets/product-usage-CHhCL93_.js 0.18 kB │ gzip: 0.17 kB
+public/build-qa-opportunities/assets/editorial-states-Bc7NQzsj.js 0.19 kB │ gzip: 0.16 kB
+public/build-qa-opportunities/assets/types-BoD_IJFv.js 0.19 kB │ gzip: 0.16 kB
+public/build-qa-opportunities/assets/participation-identity-BWabb4yN.js 0.24 kB │ gzip: 0.18 kB
+public/build-qa-opportunities/assets/circle-alert-uV7omYjr.js 0.24 kB │ gzip: 0.18 kB
+public/build-qa-opportunities/assets/eye-CmcEBfxN.js 0.25 kB │ gzip: 0.20 kB
+public/build-qa-opportunities/assets/log-in-Wgc8VLfq.js 0.25 kB │ gzip: 0.22 kB
+public/build-qa-opportunities/assets/partnership-history-CfclSLAo.js 0.27 kB │ gzip: 0.19 kB
+public/build-qa-opportunities/assets/compiler-runtime-sKsOXjDo.js 0.27 kB │ gzip: 0.23 kB
+public/build-qa-opportunities/assets/file-text-CEJtuNfp.js 0.33 kB │ gzip: 0.21 kB
+public/build-qa-opportunities/assets/confirm-Culw9Q-x.js 0.36 kB │ gzip: 0.22 kB
+public/build-qa-opportunities/assets/text-link-BBvcP60s.js 0.40 kB │ gzip: 0.27 kB
+public/build-qa-opportunities/assets/content-management-entry-C_oY4VyW.js 0.42 kB │ gzip: 0.31 kB
+public/build-qa-opportunities/assets/handshake-YqyTTm3a.js 0.44 kB │ gzip: 0.28 kB
+public/build-qa-opportunities/assets/offer-editorial-BOYEEQS3.js 0.45 kB │ gzip: 0.27 kB
+public/build-qa-opportunities/assets/investment-guides-DO58L8zr.js 0.45 kB │ gzip: 0.26 kB
+public/build-qa-opportunities/assets/catalogue-context-epuehaJf.js 0.47 kB │ gzip: 0.30 kB
+public/build-qa-opportunities/assets/operation-key-Dr4VtGxa.js 0.51 kB │ gzip: 0.35 kB
+public/build-qa-opportunities/assets/spinner-Dmx1j41V.js 0.52 kB │ gzip: 0.36 kB
+public/build-qa-opportunities/assets/types-Cxp_Nskt.js 0.55 kB │ gzip: 0.32 kB
+public/build-qa-opportunities/assets/sliders-horizontal-D-4FqAHA.js 0.58 kB │ gzip: 0.26 kB
+public/build-qa-opportunities/assets/page-preparation-guard-B-OckhBt.js 0.59 kB │ gzip: 0.35 kB
+public/build-qa-opportunities/assets/portal-presentation-fH7FShU8.js 0.62 kB │ gzip: 0.38 kB
+public/build-qa-opportunities/assets/confidentiality-C2eqbRBb.js 0.64 kB │ gzip: 0.41 kB
+public/build-qa-opportunities/assets/dist-qw3FPjRO.js 0.74 kB │ gzip: 0.49 kB
+public/build-qa-opportunities/assets/labels-DxqjesIG.js 0.74 kB │ gzip: 0.46 kB
+public/build-qa-opportunities/assets/wayfinder-Dp7PqxvV.js 0.89 kB │ gzip: 0.53 kB
+public/build-qa-opportunities/assets/createLucideIcon-oMeAlXbs.js 0.95 kB │ gzip: 0.56 kB
+public/build-qa-opportunities/assets/use-interest-draft-waHUBAew.js 0.99 kB │ gzip: 0.51 kB
+public/build-qa-opportunities/assets/context-documents-Bh7vrWBo.js 1.01 kB │ gzip: 0.57 kB
+public/build-qa-opportunities/assets/label-Dpfg_IeY.js 1.02 kB │ gzip: 0.62 kB
+public/build-qa-opportunities/assets/technical-incident-labels-DlL6bSeE.js 1.02 kB │ gzip: 0.51 kB
+public/build-qa-opportunities/assets/investment-consumer-identity-Cxp_hGfX.js 1.07 kB │ gzip: 0.59 kB
+public/build-qa-opportunities/assets/article-C5PlVvrU.js 1.08 kB │ gzip: 0.65 kB
+public/build-qa-opportunities/assets/search-state-RHLy6yI7.js 1.10 kB │ gzip: 0.68 kB
+public/build-qa-opportunities/assets/rolldown-runtime-Dd_uD5pT.js 1.10 kB │ gzip: 0.62 kB
+public/build-qa-opportunities/assets/catalogue-investment-map-BMbmCrqR.js 1.12 kB │ gzip: 0.62 kB
+public/build-qa-opportunities/assets/investment-contexts-BQGiwu9n.js 1.17 kB │ gzip: 0.60 kB
+public/build-qa-opportunities/assets/profile-json-Bza-VQzw.js 1.20 kB │ gzip: 0.72 kB
+public/build-qa-opportunities/assets/document-T3QM1nf4.js 1.23 kB │ gzip: 0.72 kB
+public/build-qa-opportunities/assets/shared-JTxA6RNb.js 1.24 kB │ gzip: 0.74 kB
+public/build-qa-opportunities/assets/public-detail-TGFEaj8I.js 1.25 kB │ gzip: 0.72 kB
+public/build-qa-opportunities/assets/demographic-context-BMdtlxjJ.js 1.27 kB │ gzip: 0.73 kB
+public/build-qa-opportunities/assets/public-offer-card-Dhz4h4ig.js 1.30 kB │ gzip: 0.70 kB
+public/build-qa-opportunities/assets/input-SCOe2C97.js 1.36 kB │ gzip: 0.75 kB
+public/build-qa-opportunities/assets/cluster-competitiveness-hNV0FaaW.js 1.37 kB │ gzip: 0.73 kB
+public/build-qa-opportunities/assets/verify-email-CBwdNKm0.js 1.40 kB │ gzip: 0.74 kB
+public/build-qa-opportunities/assets/business-types-SGCXhH6X.js 1.47 kB │ gzip: 0.75 kB
+public/build-qa-opportunities/assets/verification-BOcn3HIw.js 1.48 kB │ gzip: 0.52 kB
+public/build-qa-opportunities/assets/country-select-4uP5BvMf.js 1.49 kB │ gzip: 0.78 kB
+public/build-qa-opportunities/assets/territorial-source-evidence-eKB4ZB36.js 1.53 kB │ gzip: 0.83 kB
+public/build-qa-opportunities/assets/manage-DNCBnmgf.js 1.56 kB │ gzip: 0.74 kB
+public/build-qa-opportunities/assets/educational-article-card-0rCqZ1kX.js 1.59 kB │ gzip: 0.72 kB
+public/build-qa-opportunities/assets/partnership-source-links-VfrXk2FZ.js 1.65 kB │ gzip: 0.91 kB
+public/build-qa-opportunities/assets/catalogue-vIlL0AoR.js 1.78 kB │ gzip: 0.96 kB
+public/build-qa-opportunities/assets/technical-incident-notice-CjziWA6m.js 1.79 kB │ gzip: 0.89 kB
+public/build-qa-opportunities/assets/report-DRxtlCFu.js 1.80 kB │ gzip: 0.93 kB
+public/build-qa-opportunities/assets/opportunity-technical-incidents-C4aVbut7.js 1.88 kB │ gzip: 0.98 kB
+public/build-qa-opportunities/assets/password-input-BCuBEWkB.js 1.88 kB │ gzip: 1.05 kB
+public/build-qa-opportunities/assets/passkey-verify-D57KR0ve.js 2.04 kB │ gzip: 1.15 kB
+public/build-qa-opportunities/assets/public-record-offline-BmjvnSLE.js 2.07 kB │ gzip: 1.22 kB
+public/build-qa-opportunities/assets/investors-Di6y94fY.js 2.09 kB │ gzip: 1.15 kB
+public/build-qa-opportunities/assets/forgot-password-T9DcZJvN.js 2.10 kB │ gzip: 0.98 kB
+public/build-qa-opportunities/assets/sources-C4rNpAhk.js 2.13 kB │ gzip: 1.12 kB
+public/build-qa-opportunities/assets/saved-CuZHxZQR.js 2.21 kB │ gzip: 1.10 kB
+public/build-qa-opportunities/assets/zone-map-SY-hKDZP.js 2.26 kB │ gzip: 1.16 kB
+public/build-qa-opportunities/assets/appearance-Drnhw1ee.js 2.36 kB │ gzip: 1.14 kB
+public/build-qa-opportunities/assets/shared-CBJ3c_x-.js 2.37 kB │ gzip: 1.24 kB
+public/build-qa-opportunities/assets/editorial-subjects-C6_MfRSK.js 2.38 kB │ gzip: 1.14 kB
+public/build-qa-opportunities/assets/context-installation-ixvPSLFr.js 2.39 kB │ gzip: 1.23 kB
+public/build-qa-opportunities/assets/workspace-4_R1CLYI.js 2.40 kB │ gzip: 1.20 kB
+public/build-qa-opportunities/assets/reset-password-Dm0VnxgR.js 2.43 kB │ gzip: 0.98 kB
+public/build-qa-opportunities/assets/public-document-content-BHvs9J7S.js 2.47 kB │ gzip: 0.80 kB
+public/build-qa-opportunities/assets/manage-Cdsvzg2y.js 2.47 kB │ gzip: 1.10 kB
+public/build-qa-opportunities/assets/confirm-password-BYmSXd2u.js 2.47 kB │ gzip: 1.06 kB
+public/build-qa-opportunities/assets/structures-Dr2nXdub.js 2.49 kB │ gzip: 1.35 kB
+public/build-qa-opportunities/assets/preparation-index-DEe7dOdy.js 2.53 kB │ gzip: 1.07 kB
+public/build-qa-opportunities/assets/bootstrap-confirmation-9RmA5-Yz.js 2.55 kB │ gzip: 1.19 kB
+public/build-qa-opportunities/assets/territory-associations-D-P2rzJv.js 2.55 kB │ gzip: 1.11 kB
+public/build-qa-opportunities/assets/installation-Dly8Xg6e.js 2.59 kB │ gzip: 1.28 kB
+public/build-qa-opportunities/assets/technical-support-DlW3Jv7q.js 2.60 kB │ gzip: 1.18 kB
+public/build-qa-opportunities/assets/management-CvdeRZYq.js 2.68 kB │ gzip: 1.18 kB
+public/build-qa-opportunities/assets/experiments-_6FE66dI.js 2.73 kB │ gzip: 1.31 kB
+public/build-qa-opportunities/assets/password-CB_NxgQN.js 2.73 kB │ gzip: 0.73 kB
+public/build-qa-opportunities/assets/opportunity-install-B232n6lM.js 2.75 kB │ gzip: 1.34 kB
+public/build-qa-opportunities/assets/dist-BobFCjoG.js 2.92 kB │ gzip: 1.29 kB
+public/build-qa-opportunities/assets/portal-api-CpQBRfcu.js 2.98 kB │ gzip: 1.50 kB
+public/build-qa-opportunities/assets/two-factor-challenge-C-VleRaI.js 3.24 kB │ gzip: 1.52 kB
+public/build-qa-opportunities/assets/territory-geometry-preview-952xh2Er.js 3.25 kB │ gzip: 1.47 kB
+public/build-qa-opportunities/assets/workspace-BiFp0p_Q.js 3.29 kB │ gzip: 1.44 kB
+public/build-qa-opportunities/assets/partnership-agreement-instrument-CCnuZNBy.js 3.43 kB │ gzip: 1.49 kB
+public/build-qa-opportunities/assets/funding-classification-CXvhHOwp.js 3.44 kB │ gzip: 1.64 kB
+public/build-qa-opportunities/assets/guide-mndEuKFT.js 3.45 kB │ gzip: 1.43 kB
+public/build-qa-opportunities/assets/history-DSPDM8gM.js 3.46 kB │ gzip: 1.70 kB
+public/build-qa-opportunities/assets/audit-Xdo8qR4z.js 3.51 kB │ gzip: 1.69 kB
+public/build-qa-opportunities/assets/show-D2EX4aTO.js 3.60 kB │ gzip: 1.82 kB
+public/build-qa-opportunities/assets/demography-BaQAqCAU.js 3.70 kB │ gzip: 1.79 kB
+public/build-qa-opportunities/assets/corridor-agribusiness-BcIVf-hW.js 3.72 kB │ gzip: 1.53 kB
+public/build-qa-opportunities/assets/business-opportunity-5M_31Cua.js 3.73 kB │ gzip: 1.63 kB
+public/build-qa-opportunities/assets/management-access-D4VE9LUB.js 3.75 kB │ gzip: 1.43 kB
+public/build-qa-opportunities/assets/businesses-A6nx0k6S.js 3.76 kB │ gzip: 1.57 kB
+public/build-qa-opportunities/assets/editorial-reading-DUFQmJtD.js 3.89 kB │ gzip: 1.68 kB
+public/build-qa-opportunities/assets/catalog-B6dpdVsW.js 3.95 kB │ gzip: 1.51 kB
+public/build-qa-opportunities/assets/desk-assignments-CfWpiIpK.js 3.99 kB │ gzip: 1.95 kB
+public/build-qa-opportunities/assets/dashboard-CxoTLkSg.js 3.99 kB │ gzip: 1.61 kB
+public/build-qa-opportunities/assets/investment-context-cards-gJe_oyxw.js 4.05 kB │ gzip: 1.84 kB
+public/build-qa-opportunities/assets/shared-7qc3eq3D.js 4.09 kB │ gzip: 2.05 kB
+public/build-qa-opportunities/assets/opportunity-contact-form-Cb6iDQ1U.js 4.10 kB │ gzip: 1.67 kB
+public/build-qa-opportunities/assets/demographic-evidence-DGwOnf5g.js 4.38 kB │ gzip: 1.84 kB
+public/build-qa-opportunities/assets/conversation-Dkxk4yT2.js 4.42 kB │ gzip: 1.95 kB
+public/build-qa-opportunities/assets/portfolio-Dmt3KDgL.js 4.45 kB │ gzip: 1.74 kB
+public/build-qa-opportunities/assets/reference-C9cU7M38.js 4.46 kB │ gzip: 2.00 kB
+public/build-qa-opportunities/assets/territory-geometry-import-BnSl5jzG.js 4.49 kB │ gzip: 1.90 kB
+public/build-qa-opportunities/assets/opportunity-origin-D3o6OTRg.js 4.64 kB │ gzip: 2.07 kB
+public/build-qa-opportunities/assets/context-inheritance-dd0VgP8h.js 4.64 kB │ gzip: 2.00 kB
+public/build-qa-opportunities/assets/installation-CnslW5oi.js 4.67 kB │ gzip: 1.95 kB
+public/build-qa-opportunities/assets/opportunities-YsMGMCR_.js 4.73 kB │ gzip: 1.77 kB
+public/build-qa-opportunities/assets/references-Cu0zjt4N.js 4.79 kB │ gzip: 2.22 kB
+public/build-qa-opportunities/assets/offer-confidential-access-Bu9EgZ3d.js 4.83 kB │ gzip: 1.91 kB
+public/build-qa-opportunities/assets/opportunity-curation-7Gt2cWer.js 4.88 kB │ gzip: 2.04 kB
+public/build-qa-opportunities/assets/business-verifications-D6fOYDwf.js 4.90 kB │ gzip: 1.71 kB
+public/build-qa-opportunities/assets/crm-edit-comparison-Db53T7I7.js 4.91 kB │ gzip: 1.99 kB
+public/build-qa-opportunities/assets/investment-complements-CXfMOoGq.js 5.00 kB │ gzip: 2.05 kB
+public/build-qa-opportunities/assets/agreement-history-BfSa8E7Q.js 5.13 kB │ gzip: 2.10 kB
+public/build-qa-opportunities/assets/catalogue-D8gbcfdu.js 5.13 kB │ gzip: 2.54 kB
+public/build-qa-opportunities/assets/workspace-BeyUwWtD.js 5.20 kB │ gzip: 2.18 kB
+public/build-qa-opportunities/assets/article-editor-ByLxbPbJ.js 5.25 kB │ gzip: 2.20 kB
+public/build-qa-opportunities/assets/context-start-B2aU0zYo.js 5.46 kB │ gzip: 2.10 kB
+public/build-qa-opportunities/assets/economic-references-C7dNVUC6.js 5.61 kB │ gzip: 2.52 kB
+public/build-qa-opportunities/assets/convert-B3LtI2n6.js 5.81 kB │ gzip: 2.40 kB
+public/build-qa-opportunities/assets/editorial-subject-form-B2W8E2NJ.js 5.87 kB │ gzip: 2.66 kB
+public/build-qa-opportunities/assets/public-sector-cards-Esb1aTm6.js 5.94 kB │ gzip: 2.55 kB
+public/build-qa-opportunities/assets/demography-install-B57rikBv.js 6.05 kB │ gzip: 2.50 kB
+public/build-qa-opportunities/assets/context-C_aFYdX-.js 6.06 kB │ gzip: 2.65 kB
+public/build-qa-opportunities/assets/request-YZw1-_QG.js 6.10 kB │ gzip: 2.41 kB
+public/build-qa-opportunities/assets/structure-geography-ELCgr0dd.js 6.12 kB │ gzip: 2.22 kB
+public/build-qa-opportunities/assets/editorial-workflow-DYYZzXOw.js 6.22 kB │ gzip: 2.29 kB
+public/build-qa-opportunities/assets/investment-map-panel-BgicrnXE.js 6.27 kB │ gzip: 2.88 kB
+public/build-qa-opportunities/assets/procurement-HZSwnYGC.js 6.35 kB │ gzip: 2.73 kB
+public/build-qa-opportunities/assets/register-vy51_8_H.js 6.39 kB │ gzip: 2.70 kB
+public/build-qa-opportunities/assets/funding-Dp_3_Uu2.js 6.49 kB │ gzip: 2.00 kB
+public/build-qa-opportunities/assets/leads-Bu2UK8ON.js 6.56 kB │ gzip: 2.82 kB
+public/build-qa-opportunities/assets/business-marketplace-C4h1dExR.js 6.59 kB │ gzip: 2.51 kB
+public/build-qa-opportunities/assets/interest-xqtiIFPC.js 6.63 kB │ gzip: 2.23 kB
+public/build-qa-opportunities/assets/demography-BxaBdOBx.js 6.66 kB │ gzip: 2.51 kB
+public/build-qa-opportunities/assets/fiscal-qa-review-CEzw2OQP.js 6.75 kB │ gzip: 2.70 kB
+public/build-qa-opportunities/assets/business-interest-B01b-tRY.js 6.82 kB │ gzip: 2.60 kB
+public/build-qa-opportunities/assets/interests-BcpPowrJ.js 6.83 kB │ gzip: 2.78 kB
+public/build-qa-opportunities/assets/catalogue-CoNk3Fdv.js 6.84 kB │ gzip: 2.81 kB
+public/build-qa-opportunities/assets/company-comparison-8kehiDPm.js 6.92 kB │ gzip: 2.60 kB
+public/build-qa-opportunities/assets/business-classification-C8hVj93v.js 7.01 kB │ gzip: 2.57 kB
+public/build-qa-opportunities/assets/dist-Ddn_hEUd.js 7.02 kB │ gzip: 2.71 kB
+public/build-qa-opportunities/assets/blank-2jFZEe2q.js 7.07 kB │ gzip: 2.77 kB
+public/build-qa-opportunities/assets/analysis-fj0joeC4.js 7.11 kB │ gzip: 2.96 kB
+public/build-qa-opportunities/assets/profile-CidlqkJE.js 7.19 kB │ gzip: 2.47 kB
+public/build-qa-opportunities/assets/install-BUqVTelf.js 7.23 kB │ gzip: 3.03 kB
+public/build-qa-opportunities/assets/investor-COYuuRY_.js 7.30 kB │ gzip: 2.89 kB
+public/build-qa-opportunities/assets/investor-path-CSExMjAt.js 7.36 kB │ gzip: 3.02 kB
+public/build-qa-opportunities/assets/proactive-supply-review-Bspf5LP7.js 7.55 kB │ gzip: 2.73 kB
+public/build-qa-opportunities/assets/legacy-Cgz97_0u.js 7.67 kB │ gzip: 2.56 kB
+public/build-qa-opportunities/assets/jsx-runtime-BpzPEenQ.js 7.89 kB │ gzip: 2.99 kB
+public/build-qa-opportunities/assets/shared-BFCbvJo7.js 7.90 kB │ gzip: 3.08 kB
+public/build-qa-opportunities/assets/business-objects-Uo9gKzR4.js 7.95 kB │ gzip: 2.57 kB
+public/build-qa-opportunities/assets/assisted-tUytMJpX.js 8.06 kB │ gzip: 3.16 kB
+public/build-qa-opportunities/assets/value-network-Cp8UYtMs.js 8.06 kB │ gzip: 3.08 kB
+public/build-qa-opportunities/assets/editor-BpHntkYH.js 8.21 kB │ gzip: 2.97 kB
+public/build-qa-opportunities/assets/territorial-editorial-B0ni24WA.js 8.22 kB │ gzip: 3.03 kB
+public/build-qa-opportunities/assets/opportunity-promotion-CyWpImc1.js 8.40 kB │ gzip: 2.96 kB
+public/build-qa-opportunities/assets/publication-history-DfLO6jOc.js 8.42 kB │ gzip: 3.02 kB
+public/build-qa-opportunities/assets/print-Tn3aXHlY.js 8.66 kB │ gzip: 3.55 kB
+public/build-qa-opportunities/assets/dialog-C4ibpf_J.js 8.81 kB │ gzip: 2.77 kB
+public/build-qa-opportunities/assets/shared-DXIXdF9G.js 8.92 kB │ gzip: 3.83 kB
+public/build-qa-opportunities/assets/iva-modelo-a-CNylSdBc.js 8.94 kB │ gzip: 3.27 kB
+public/build-qa-opportunities/assets/zone-relations-BLf96C6y.js 8.95 kB │ gzip: 2.99 kB
+public/build-qa-opportunities/assets/investment-territory-MNjWS_u0.js 8.96 kB │ gzip: 3.43 kB
+public/build-qa-opportunities/assets/similar-businesses-HTwQGVGQ.js 9.11 kB │ gzip: 3.58 kB
+public/build-qa-opportunities/assets/interactive-investment-map-BClK9yWO.js 9.13 kB │ gzip: 3.60 kB
+public/build-qa-opportunities/assets/investment-land-preparation-BHeC9B2U.js 9.15 kB │ gzip: 3.33 kB
+public/build-qa-opportunities/assets/why-invest-Cwz2gK0p.js 9.16 kB │ gzip: 3.49 kB
+public/build-qa-opportunities/assets/interest-history-oHNykKmC.js 9.48 kB │ gzip: 3.45 kB
+public/build-qa-opportunities/assets/content-hub-CzsGgtNo.js 9.95 kB │ gzip: 2.93 kB
+public/build-qa-opportunities/assets/privacy-policy-LI1lpUCK.js 10.36 kB │ gzip: 3.07 kB
+public/build-qa-opportunities/assets/editorial-edition-Zq39X7mf.js 10.36 kB │ gzip: 3.47 kB
+public/build-qa-opportunities/assets/panel-D5bg3ajo.js 10.46 kB │ gzip: 4.29 kB
+public/build-qa-opportunities/assets/result-yez5wOFo.js 10.57 kB │ gzip: 4.73 kB
+public/build-qa-opportunities/assets/dashboard-C0NQF0VJ.js 10.92 kB │ gzip: 3.47 kB
+public/build-qa-opportunities/assets/zone-attachments-CLqpntOa.js 10.94 kB │ gzip: 2.75 kB
+public/build-qa-opportunities/assets/business-D-GsDwzk.js 11.10 kB │ gzip: 3.09 kB
+public/build-qa-opportunities/assets/detail-3fwgDkH3.js 11.73 kB │ gzip: 4.30 kB
+public/build-qa-opportunities/assets/login-CHsqdM2X.js 11.82 kB │ gzip: 4.90 kB
+public/build-qa-opportunities/assets/support-installation-tsBpnQkP.js 12.36 kB │ gzip: 4.86 kB
+public/build-qa-opportunities/assets/show-HFfMuLZ-.js 12.37 kB │ gzip: 3.98 kB
+public/build-qa-opportunities/assets/show-D9Qj4eSh.js 12.41 kB │ gzip: 4.39 kB
+public/build-qa-opportunities/assets/guide-eWGplRw4.js 12.58 kB │ gzip: 4.03 kB
+public/build-qa-opportunities/assets/contact-CrhBB858.js 12.65 kB │ gzip: 4.33 kB
+public/build-qa-opportunities/assets/project-dossier-document-CiCplPXo.js 12.68 kB │ gzip: 5.20 kB
+public/build-qa-opportunities/assets/dossier-prefill-B90PO41l.js 12.72 kB │ gzip: 4.65 kB
+public/build-qa-opportunities/assets/technical-incident-GDB_x0wp.js 12.73 kB │ gzip: 4.96 kB
+public/build-qa-opportunities/assets/competitiveness-60nqrOfg.js 12.74 kB │ gzip: 4.23 kB
+public/build-qa-opportunities/assets/editor-t_I3E9vl.js 12.91 kB │ gzip: 3.75 kB
+public/build-qa-opportunities/assets/fiscal-forms-CCePiKSA.js 13.04 kB │ gzip: 4.75 kB
+public/build-qa-opportunities/assets/opportunity-classification-Dm_W66ep.js 13.44 kB │ gzip: 4.55 kB
+public/build-qa-opportunities/assets/react-CjVep8xp.js 13.90 kB │ gzip: 4.61 kB
+public/build-qa-opportunities/assets/opportunity-search-filters-B1kHkt50.js 14.73 kB │ gzip: 4.72 kB
+public/build-qa-opportunities/assets/case-wwTeoOmv.js 14.77 kB │ gzip: 5.38 kB
+public/build-qa-opportunities/assets/product-usage-Csn5TQSM.js 14.81 kB │ gzip: 5.30 kB
+public/build-qa-opportunities/assets/company-preparation-8z1Qvdvd.js 15.17 kB │ gzip: 4.92 kB
+public/build-qa-opportunities/assets/terms-of-use-CUQdwKes.js 15.27 kB │ gzip: 4.20 kB
+public/build-qa-opportunities/assets/edit-qyf8y1Qe.js 15.38 kB │ gzip: 5.15 kB
+public/build-qa-opportunities/assets/relationship-repair-brZoqSge.js 16.29 kB │ gzip: 5.62 kB
+public/build-qa-opportunities/assets/edit-CWwLVVG7.js 16.60 kB │ gzip: 4.88 kB
+public/build-qa-opportunities/assets/edit-DeIwdeuK.js 16.93 kB │ gzip: 4.91 kB
+public/build-qa-opportunities/assets/search-hMTBVa-Z.js 17.08 kB │ gzip: 5.65 kB
+public/build-qa-opportunities/assets/funding-institution-DQq7N6kA.js 17.24 kB │ gzip: 5.83 kB
+public/build-qa-opportunities/assets/use-two-factor-auth-D9w0KtSq.js 17.31 kB │ gzip: 6.30 kB
+public/build-qa-opportunities/assets/administration-CgUGyaX7.js 17.41 kB │ gzip: 4.30 kB
+public/build-qa-opportunities/assets/notifications-COYs8HfG.js 17.61 kB │ gzip: 5.83 kB
+public/build-qa-opportunities/assets/ai-CNDnFHJf.js 18.08 kB │ gzip: 6.92 kB
+public/build-qa-opportunities/assets/project-dossier-report-BPxivfbL.js 18.98 kB │ gzip: 6.95 kB
+public/build-qa-opportunities/assets/submission-DYV8rsdX.js 19.98 kB │ gzip: 6.32 kB
+public/build-qa-opportunities/assets/opportunity-resources-B8WM1sIO.js 20.19 kB │ gzip: 7.22 kB
+public/build-qa-opportunities/assets/about-D8OtIyBz.js 20.87 kB │ gzip: 6.99 kB
+public/build-qa-opportunities/assets/zones-CAkKqhGW.js 21.16 kB │ gzip: 5.22 kB
+public/build-qa-opportunities/assets/service-editor-Bu5x27nt.js 21.30 kB │ gzip: 6.58 kB
+public/build-qa-opportunities/assets/context-D8SLahjT.js 21.54 kB │ gzip: 6.99 kB
+public/build-qa-opportunities/assets/declaration-Bzl3lixx.js 22.15 kB │ gzip: 7.14 kB
+public/build-qa-opportunities/assets/Combination-BBDI-RbM.js 23.03 kB │ gzip: 8.24 kB
+public/build-qa-opportunities/assets/desk-internal-operations-BwHIs_KM.js 23.14 kB │ gzip: 6.53 kB
+public/build-qa-opportunities/assets/education-CFNgleBU.js 23.40 kB │ gzip: 8.19 kB
+public/build-qa-opportunities/assets/investment-opportunity-choices-D56R3GRo.js 24.28 kB │ gzip: 8.06 kB
+public/build-qa-opportunities/assets/preparation-rSUWHMu_.js 24.56 kB │ gzip: 6.73 kB
+public/build-qa-opportunities/assets/utils-CCDJ05Ll.js 27.31 kB │ gzip: 8.71 kB
+public/build-qa-opportunities/assets/business-wizard-BQMAm94O.js 28.60 kB │ gzip: 9.42 kB
+public/build-qa-opportunities/assets/proactive-investment-DfIQuXvs.js 28.65 kB │ gzip: 9.16 kB
+public/build-qa-opportunities/assets/lead-vUXgd2g8.js 29.03 kB │ gzip: 9.96 kB
+public/build-qa-opportunities/assets/preparation-Ck3nokWs.js 29.32 kB │ gzip: 8.51 kB
+public/build-qa-opportunities/assets/form-ClhCCJAo.js 29.80 kB │ gzip: 8.04 kB
+public/build-qa-opportunities/assets/profile-Chg149hk.js 30.71 kB │ gzip: 9.33 kB
+public/build-qa-opportunities/assets/welcome-bviWRO1E.js 30.73 kB │ gzip: 6.49 kB
+public/build-qa-opportunities/assets/manage-GV_WvYCX.js 31.17 kB │ gzip: 7.63 kB
+public/build-qa-opportunities/assets/security-DsABJZBj.js 32.11 kB │ gzip: 10.06 kB
+public/build-qa-opportunities/assets/editor-CWbYf7YK.js 35.03 kB │ gzip: 11.76 kB
+public/build-qa-opportunities/assets/case-Kr0q2Gh8.js 37.38 kB │ gzip: 10.33 kB
+public/build-qa-opportunities/assets/room-Bg8k9V54.js 38.25 kB │ gzip: 12.14 kB
+public/build-qa-opportunities/assets/simulation-form-CVV000gF.js 39.43 kB │ gzip: 10.27 kB
+public/build-qa-opportunities/assets/request-D6rDWCh-.js 41.86 kB │ gzip: 11.29 kB
+public/build-qa-opportunities/assets/detail-D8tQRv3B.js 46.42 kB │ gzip: 15.18 kB
+public/build-qa-opportunities/assets/PortalInvestidor-C7BfhVCR.js 48.72 kB │ gzip: 14.86 kB
+public/build-qa-opportunities/assets/business-financial-plan-s1uoiktr.js 49.19 kB │ gzip: 11.98 kB
+public/build-qa-opportunities/assets/catalogue-DqISkm8V.js 49.97 kB │ gzip: 16.08 kB
+public/build-qa-opportunities/assets/zone-DEQbIXLp.js 62.43 kB │ gzip: 15.63 kB
+public/build-qa-opportunities/assets/show-vTvBmoW9.js 68.49 kB │ gzip: 21.16 kB
+public/build-qa-opportunities/assets/opportunity-draft-C7XuiI1O.js 70.65 kB │ gzip: 20.89 kB
+public/build-qa-opportunities/assets/document-zip-bqFSTgG4.js 95.97 kB │ gzip: 28.50 kB
+public/build-qa-opportunities/assets/leaflet-src-CUWrUTR2.js 148.82 kB │ gzip: 43.39 kB
+public/build-qa-opportunities/assets/app-Bez7gBWz.js 221.29 kB │ gzip: 71.24 kB
+public/build-qa-opportunities/assets/dist-BRfsww6z.js 307.99 kB │ gzip: 96.67 kB
+public/build-qa-opportunities/assets/document-word-CqTGSyXu.js 403.66 kB │ gzip: 115.44 kB
+public/build-qa-opportunities/assets/services-DR6lbhJH.js 965.92 kB │ gzip: 226.48 kB
+public/build-qa-opportunities/assets/project-dossier-a3IvmE5B.js 1,009.94 kB │ gzip: 274.44 kB
+
+✓ built in 45.51s
+[plugin builtin:vite-reporter]
+(!) Some chunks are larger than 500 kB after minification. Consider:
+- Using dynamic import() to code-split the application
+- Use build.rolldownOptions.output.codeSplitting to improve chunking:
+https://rolldown.rs/reference/OutputOptions.codeSplitting
+- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
+[33m[PLUGIN_TIMINGS] [0mYour build spent 99% of 45.5s inside plugin hooks (44.9s).
+Measured inside the callback, so queue time is excluded and time the callback itself awaited is not:
+ - @rolldown/plugin-babel transform (86%, 39.2s, 411 calls)
+ - @tailwindcss/vite:generate:build transform (10%, 4.6s, 3 calls)
+Those rows are 96% of the build; the rest of the 99% is below.
+Not measurable — 1 hook whose calls overlap, so elapsed time covers work other calls were doing. Profile with `node
+--cpu-prof`:
+ - vite:css transform (3 calls)
+See https://rolldown.rs/reference/InputOptions.checks#plugintimings for more details.
diff --git a/outputs/offers-private-phase5-20261114/focused.log b/outputs/offers-private-phase5-20261114/focused.log
new file mode 100644
index 00000000..4b2cea7c
--- /dev/null
+++ b/outputs/offers-private-phase5-20261114/focused.log
@@ -0,0 +1 @@
+{"tool":"phpunit","result":"passed","tests":12,"passed":12,"assertions":151,"duration_ms":153336}
diff --git a/outputs/offers-private-phase5-20261114/focused.xml b/outputs/offers-private-phase5-20261114/focused.xml
new file mode 100644
index 00000000..b1a4007f
--- /dev/null
+++ b/outputs/offers-private-phase5-20261114/focused.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/outputs/offers-private-phase5-20261114/global-format.log b/outputs/offers-private-phase5-20261114/global-format.log
new file mode 100644
index 00000000..3c3aa955
--- /dev/null
+++ b/outputs/offers-private-phase5-20261114/global-format.log
@@ -0,0 +1,73 @@
+
+> check
+> vp check
+
+resources/js/components/funding-classification.tsx (2ms)
+resources/js/components/institutional-source/source-text.tsx (0ms)
+resources/js/components/password-input.tsx (1ms)
+resources/js/components/sector-entry.tsx (32ms)
+resources/js/components/sector-resource-reference.tsx (4ms)
+resources/js/components/simulation-form.tsx (109ms)
+resources/js/components/simulation-irpc-annual.tsx (9ms)
+resources/js/components/structure-geography.tsx (2ms)
+resources/js/components/tempo-header-tools.tsx (6ms)
+resources/js/lib/investor-path-stage.ts (0ms)
+resources/js/lib/irpc-annual-settlement.ts (11ms)
+resources/js/pages/funding/assisted.tsx (3ms)
+resources/js/pages/funding/manage.tsx (2ms)
+resources/js/pages/funding/saved.tsx (2ms)
+resources/js/pages/investment/catalogue.tsx (16ms)
+resources/js/pages/investment/detail.tsx (4ms)
+resources/js/pages/investor-path/index.tsx (3ms)
+resources/js/pages/investor-path/preparation.tsx (11ms)
+resources/js/pages/investor-path/show.tsx (35ms)
+resources/js/pages/management/demography-install.tsx (4ms)
+resources/js/pages/portal/fiscal-forms.tsx (8ms)
+resources/js/pages/portal/search.tsx (32ms)
+resources/js/translation/page-translator.tsx (1ms)
+resources/js/translation/service.ts (2ms)
+resources/js/translation/types.ts (0ms)
+resources/js/translation/utils.ts (3ms)
+tests/frontend/cluster-editorial.test.mjs (0ms)
+tests/frontend/dashboard-navigation-sections.test.mjs (0ms)
+tests/frontend/editorial-completion.test.mjs (0ms)
+tests/frontend/editorial-dashboard-manifest.test.mjs (0ms)
+tests/frontend/editorial-dashboard-presentation.test.mjs (0ms)
+tests/frontend/editorial-edition.test.mjs (0ms)
+tests/frontend/education-review.test.mjs (1ms)
+tests/frontend/fiscal-form-entry.test.mjs (0ms)
+tests/frontend/funding-directory.test.mjs (1ms)
+tests/frontend/global-search.test.mjs (1ms)
+tests/frontend/helpers/irpc-advance-period-case.mjs (0ms)
+tests/frontend/helpers/irpc-annual-noncivil-case.mjs (0ms)
+tests/frontend/helpers/irpc-investment-combination-case.mjs (0ms)
+tests/frontend/investment-map-indicators.test.mjs (6ms)
+tests/frontend/investment-preparation.test.mjs (9ms)
+tests/frontend/irpc-advance-cashflow-ui.test.mjs (0ms)
+tests/frontend/irpc-advance-cashflow.test.mjs (0ms)
+tests/frontend/irpc-advance-period-ui.test.mjs (0ms)
+tests/frontend/irpc-advance-period.test.mjs (0ms)
+tests/frontend/irpc-annual-noncivil-ui.test.mjs (0ms)
+tests/frontend/irpc-annual-noncivil.test.mjs (0ms)
+tests/frontend/irpc-investment-carry-combination.test.mjs (0ms)
+tests/frontend/irpc-pec-correction.test.mjs (11ms)
+tests/frontend/irpc-pec-last-amount.test.mjs (0ms)
+tests/frontend/irpc-pec-obligation-ui.test.mjs (0ms)
+tests/frontend/irpc-pec-obligation.test.mjs (1ms)
+tests/frontend/irpc-ppc-obligation-ui.test.mjs (0ms)
+tests/frontend/licensing-stage.test.mjs (4ms)
+tests/frontend/navigation-sidebar-render.test.mjs (0ms)
+tests/frontend/opportunity-curation.test.mjs (0ms)
+tests/frontend/opportunity-information.test.mjs (0ms)
+tests/frontend/portal-institutional-contact.test.mjs (0ms)
+tests/frontend/portal-navigation.test.mjs (1ms)
+tests/frontend/portal-six-group-menu.test.mjs (1ms)
+tests/frontend/sector-resource-reference.test.mjs (2ms)
+
+Found formatting issues in 61 files (5491ms, 6 threads). Run `vp check --fix` to fix them.
+npm.cmd : [1m[31merror:[39m[0m Formatting issues found
+At line:2 char:1
++ npm.cmd run check *> outputs/offers-private-phase5-20261114/global-fo ...
++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ + CategoryInfo : NotSpecified: ([1m[31merror:...ng issues found:String) [], RemoteException
+ + FullyQualifiedErrorId : NativeCommandError
diff --git a/outputs/offers-private-phase5-20261114/qa-result.json b/outputs/offers-private-phase5-20261114/qa-result.json
new file mode 100644
index 00000000..32b60a46
--- /dev/null
+++ b/outputs/offers-private-phase5-20261114/qa-result.json
@@ -0,0 +1,170 @@
+{
+ "phase": 5,
+ "base": "e431de8b423a4b64b9875ca7a8627e3e4b3c9c35",
+ "state": "implemented and technically verified in QA; awaits acceptance; no installation or effective publication",
+ "scope": "Ofertas only, phases one to five preserved",
+ "database": {
+ "host": "127.0.0.1",
+ "port": 55441,
+ "name": "offers_phase1_20261114_test",
+ "data": "synthetic, existing guards and rollback"
+ },
+ "verified_runs": {
+ "focused.log": {
+ "tool": "phpunit",
+ "result": "passed",
+ "tests": 12,
+ "passed": 12,
+ "assertions": 151,
+ "duration_ms": 153336
+ },
+ "regression.log": {
+ "tool": "phpunit",
+ "result": "passed",
+ "tests": 59,
+ "passed": 59,
+ "assertions": 780,
+ "duration_ms": 447792
+ }
+ },
+ "integrated_suites": {
+ "Tests\\Feature\\PrivateOfferOperationsTest": {
+ "tests": 13,
+ "assertions": 180,
+ "errors": 0,
+ "failures": 0
+ },
+ "Tests\\Feature\\PrivateOfferConfidentialityTest": {
+ "tests": 11,
+ "assertions": 160,
+ "errors": 0,
+ "failures": 0
+ },
+ "Tests\\Feature\\OfferPublicCatalogueTest": {
+ "tests": 10,
+ "assertions": 133,
+ "errors": 0,
+ "failures": 0
+ },
+ "Tests\\Feature\\PrivateOfferPreparationTest": {
+ "tests": 14,
+ "assertions": 146,
+ "errors": 0,
+ "failures": 0
+ },
+ "Tests\\Feature\\PrivateOfferEditorialTest": {
+ "tests": 11,
+ "assertions": 161,
+ "errors": 0,
+ "failures": 0
+ }
+ },
+ "checks": {
+ "pint": "passed",
+ "phpstan": "passed, zero errors",
+ "typescript": "passed",
+ "frontend_lint_format": "eight affected files passed",
+ "qa_build": "passed, build-final.log",
+ "html": "real compiled preparation, report, operations list, editorial dashboard and earlier page cases passed",
+ "git_diff_check": "passed",
+ "global_format": {
+ "failures": 61,
+ "unchanged_git_content": true,
+ "outside_delta": true,
+ "same_baseline_list": true
+ }
+ },
+ "acceptance_matrix": "docs/investmoz/v2/auditoria-portal/ofertas-fase5-fecho-qa-20261114.md",
+ "matrix_updated": "RF-MN-003 only; all 49 columns and other rows preserved",
+ "features": {
+ "withdrawal": "owner, immediate and idempotent, no editorial approval or stale-etag block",
+ "reactivation": "new operational cycle, exact new editorial validation and owner confirmation; old public snapshot and NDA grants not resurrected",
+ "validity": "frozen by approval hash, starts at publication UTC, read-time expiry independent of NDA and cron",
+ "reports": "encrypted grounds and reported source, idempotent receipt, native tasks/dashboard, scoped MFA reviewer, version and etag",
+ "highlights": "private draft/archive plans only, versioned and audited; is_featured untouched",
+ "public_consumers": "list/detail/filters/map/API/CSV exclude unavailable or expired snapshots, no-store HTTP; optional expires_at only when known"
+ },
+ "default_configuration": {
+ "nda": null,
+ "validity_days": null,
+ "validity_policy_reference": null,
+ "expiry_scheduler_enabled": false
+ },
+ "restrictions": {
+ "effective_publication_enabled": false,
+ "installations_performed": false,
+ "operational_data_modified": false,
+ "mne_modified": false,
+ "migrations_added": false,
+ "payments_billing_or_commercial_promotion_enabled": false,
+ "unrelated_features_started": false,
+ "dependencies_installed": false
+ },
+ "accepted_commits_preserved": [
+ "1f2990e4",
+ "6e302db6",
+ "91c18573",
+ "7691cab3",
+ "e431de8b"
+ ],
+ "pending": [
+ "approved real NDA exemplar/version/approval reference",
+ "legal identification of property evidence types",
+ "final validity period/policy and legal notices",
+ "desktop/mobile visual acceptance; last browser connector evidence host unavailable",
+ "GD/WebP image derivatives, not installed",
+ "real email delivery from phase two",
+ "61 pre-existing format failures",
+ "express authorization for any installation or effective publication"
+ ],
+ "source_hash_encoding": "SHA256 of UTF-8 with Git LF normalization",
+ "source_sha256": {
+ "app/Console/Commands/ExpirePrivateOffers.php": "19c1a80f73eb063c7c9f26872710954f30f13dea82eaeca3e99190d881960ec5",
+ "app/Http/Controllers/OfferPublicCatalogueController.php": "04034126a52660e10a6e64207a3aa4fb09a9b03aee045383b240d434477f03ca",
+ "app/Http/Controllers/PortalEditorialWorkflowController.php": "de74af5fb777033eaf67889e2239b3aa24ec56a758db0a42826367c5c79b8f49",
+ "app/Http/Controllers/PrivateOfferOperationsController.php": "872ef0dbfae74e07c9ca4181cd2594a365dced8df93f3e4904b374a660a1eade",
+ "app/Http/Controllers/PrivateOfferPreparationController.php": "32415ac61a948a0bd53f440c728b5e99522017d765910101eb1a7df1d8b6d0d7",
+ "app/Services/OfferAvailability.php": "72f975c8c01c7a3ccd3cc00d372210b3546dc6bbae1c34d91514b341a7968adf",
+ "app/Services/OfferPublicCatalogue.php": "7eee7ab9864cc29c2de90b071ff2c3bf3cac093d3adde9e1937ded324b2223c6",
+ "app/Services/OpportunityProfileWorkflow.php": "2b16a309035514967f9e0290f9167433b4979940aa26f18db2afdb24996d5130",
+ "app/Services/PrivateOfferConfidentiality.php": "17e02925c3cfe26a4853db272b6d3c2acd1095efac826371a517a603eaa429ca",
+ "app/Services/PrivateOfferOperations.php": "1b6032e96712f9d19befe76029a47464c3bb52e4f018d858dcc6f8e605cf0cdc",
+ "app/Services/PrivateOfferPreparation.php": "442d7aa1093001630bdfc581fde3b9f16e4b5b76a4e8d028ba4b3da41b1cfe4d",
+ "config/private_offers.php": "975e94b3362a60efe60b7c3a227b8e7727e1e0ef63a42f3a1a575ed7c4725f0e",
+ "docs/investmoz/unification/traceability-and-manual.md": "ba9f42c7c45a58900548fbfb452359b90e90cdcd08f864a90f8da24e2bbdb9f5",
+ "docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv": "ba2008946615c5f10b6285f725fe32076baabb72612d053b6ab3223fe3ddc3ad",
+ "docs/investmoz/v2/auditoria-portal/ofertas-fase5-fecho-qa-20261114.md": "4a56d21a4a2d212efc81c780351282846e6f4d970db287f5fc4ebddf75a66ff5",
+ "resources/js/components/offer-operations.tsx": "ce4a32b422c7a8e33dade2002b9565d53b4294c44f051ab06022fa6fcfc5ee4e",
+ "resources/js/components/public-offer-card.tsx": "ac8f01a1c9d864b5f83c48774afcd95d1d42005bb0212b15bbc3f6f7258c0db9",
+ "resources/js/lib/public-offer.ts": "8ec6f030328af26d0ce70665ce82a41102fadf892120b14deb4e90f1ae2a8a48",
+ "resources/js/pages/management/editorial-workflow.tsx": "9758c6a1cfc4018905c8493777917d316e18a649eb404c868e9c87356d6c273c",
+ "resources/js/pages/offers/preparation-index.tsx": "50a0feb8ab2c6abf4e725e2c280d05f4a0842d2be49e0c33d2a0a2d5313a361f",
+ "resources/js/pages/offers/preparation.tsx": "bf7c02232ffa13617f5f65b9fe7704e31410bd622e9270cdbeef92fe5cbcf1f3",
+ "resources/js/pages/offers/public-detail.tsx": "387c8a9a0013920bf74c85853362671d76131849508a9b86fc2391622cb0c647",
+ "resources/js/pages/offers/report.tsx": "91f9e32e88c82b1af984226ae81ede188fddaff395743905b8ab8ce483a8f859",
+ "routes/console.php": "fe352a044273a39d58c1b58f591324879553c8a3deadb0c45aa96ada804f8f2a",
+ "routes/web.php": "993577842b5cc3cc8cf5f6f136cfe544795e7e838d26c28cde7cb204934d9073",
+ "tests/Feature/PrivateOfferOperationsTest.php": "f6e60f25543e9a9b15a8f06e563ad1f7e41c31d69b6801daab899619ff7b98e5"
+ },
+ "evidence_sha256": {
+ "focused.log": "0ee1e92b0d653ff397f493ae55e3dce44b99213dcf3b9d27ee22e6e47fca14d7",
+ "focused.xml": "e397adfb3d556ad39df603b1dd9a0bbd4983095112663406196cebd394bc304c",
+ "regression.log": "81d4ae963170ae290c0b0a536e1fb4c227db909b4b3dcfc51c2c22b8745389d7",
+ "regression.xml": "756c8a2dd7d4713e74994f5a27d3acac8e196592eb16d355e8b2fab51ce03218",
+ "global-format.log": "b9290879ba5de47cb4ad6194125287d3dc6e82bf626ef6f8a03716d453161912",
+ "build-final.log": "a00480048296be75d57a95658234841bde8db2510aaad908ba00f068e53b3399"
+ },
+ "overlapping_runs_not_summed": true,
+ "report": "docs/investmoz/v2/auditoria-portal/ofertas-fase5-fecho-qa-20261114.md",
+ "acceptance_matrix_section": "Matriz de aceitação das cinco fases",
+ "commands": [
+ "php vendor/bin/phpunit tests/Feature/PrivateOfferOperationsTest.php tests/Feature/PrivateOfferConfidentialityTest.php tests/Feature/OfferPublicCatalogueTest.php tests/Feature/PrivateOfferPreparationTest.php tests/Feature/PrivateOfferEditorialTest.php --log-junit outputs/offers-private-phase5-20261114/regression.xml",
+ "php vendor/bin/pint --dirty",
+ "php vendor/bin/phpstan analyse --no-progress --memory-limit=1G",
+ "npm.cmd run types:check",
+ "npx.cmd vp check resources/js/components/offer-operations.tsx resources/js/components/public-offer-card.tsx resources/js/lib/public-offer.ts resources/js/pages/offers/report.tsx resources/js/pages/offers/preparation.tsx resources/js/pages/offers/preparation-index.tsx resources/js/pages/offers/public-detail.tsx resources/js/pages/management/editorial-workflow.tsx",
+ "$env:INVESTMOZ_QA_BUILD='1'; npm.cmd run build",
+ "npm.cmd run check"
+ ],
+ "evidence_normalization": "UTF-8, LF; trailing whitespace removed from build/global-format logs without removing messages"
+}
diff --git a/outputs/offers-private-phase5-20261114/regression.log b/outputs/offers-private-phase5-20261114/regression.log
new file mode 100644
index 00000000..c0047a08
--- /dev/null
+++ b/outputs/offers-private-phase5-20261114/regression.log
@@ -0,0 +1 @@
+{"tool":"phpunit","result":"passed","tests":59,"passed":59,"assertions":780,"duration_ms":447792}
diff --git a/outputs/offers-private-phase5-20261114/regression.xml b/outputs/offers-private-phase5-20261114/regression.xml
new file mode 100644
index 00000000..97fe276c
--- /dev/null
+++ b/outputs/offers-private-phase5-20261114/regression.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/components/offer-operations.tsx b/resources/js/components/offer-operations.tsx
new file mode 100644
index 00000000..243732f6
--- /dev/null
+++ b/resources/js/components/offer-operations.tsx
@@ -0,0 +1,550 @@
+import { Link } from '@inertiajs/react';
+import { useEffect, useState } from 'react';
+import { profileJson } from '@/lib/profile-json';
+type Report = {
+ id: string;
+ version: number;
+ status: string;
+ reason: string;
+ resolution_reason: string | null;
+ source_record_version: number;
+ source_snapshot?: {
+ title?: string;
+ summary?: string;
+ description?: string;
+ } | null;
+};
+type Highlight = {
+ version: number;
+ status: string;
+ label: string;
+ note: string;
+ starts_at: string | null;
+ ends_at: string | null;
+};
+type Operation = {
+ id: number;
+ etag: string;
+ availability: {
+ state: string;
+ expires_at: string | null;
+ policy_pending: boolean;
+ };
+ editorial_state: string;
+ owner: boolean;
+ reviewer: boolean;
+ publisher: boolean;
+ reports: Report[];
+ highlight: Highlight | null;
+ history: { id: string; action: string; at: string; cycle: number }[];
+};
+const labels: Record = {
+ available: 'Disponível',
+ withdrawn: 'Retirada',
+ expired: 'Expirada',
+ revalidation: 'Aguarda nova validação',
+ unavailable: 'Indisponível',
+};
+export default function OfferOperations({
+ id,
+ onChanged,
+}: {
+ id: number;
+ onChanged: () => void;
+}) {
+ const [value, setValue] = useState(null);
+ const [message, setMessage] = useState('');
+ const [busy, setBusy] = useState(false);
+ const [reason, setReason] = useState('');
+ const [confirm, setConfirm] = useState(false);
+ const [plan, setPlan] = useState({
+ label: '',
+ starts_at: '',
+ ends_at: '',
+ note: '',
+ status: 'draft',
+ });
+ const base = `/minhas-oportunidades/ofertas/${id}`;
+ useEffect(() => {
+ let active = true;
+ profileJson(`${base}/operacoes`, {}, 'GET')
+ .then((result) => {
+ if (active) {
+ setValue(result);
+ if (result.highlight)
+ setPlan({
+ label: result.highlight.label,
+ starts_at: result.highlight.starts_at ?? '',
+ ends_at: result.highlight.ends_at ?? '',
+ note: result.highlight.note,
+ status: result.highlight.status,
+ });
+ }
+ })
+ .catch((error) => {
+ if (active) setMessage(error.message);
+ });
+ return () => {
+ active = false;
+ };
+ }, [base]);
+ async function command(path: string, data: object) {
+ if (busy) return;
+ setBusy(true);
+ setMessage('');
+ try {
+ const result = await profileJson(`${base}/${path}`, data);
+ setValue(result);
+ setConfirm(false);
+ onChanged();
+ setMessage(
+ 'Operação guardada em privado. A publicação permanece bloqueada.',
+ );
+ } catch (error) {
+ setMessage(
+ error instanceof Error
+ ? error.message
+ : 'Não foi possível concluir.',
+ );
+ } finally {
+ setBusy(false);
+ }
+ }
+ async function refresh(loadPlan = false) {
+ setBusy(true);
+ try {
+ const next: Operation = await profileJson(
+ `${base}/operacoes`,
+ {},
+ 'GET',
+ );
+ setValue((previous) =>
+ loadPlan
+ ? next
+ : { ...next, highlight: previous?.highlight ?? null },
+ );
+ if (loadPlan)
+ setPlan({
+ label: next.highlight?.label ?? '',
+ starts_at: next.highlight?.starts_at ?? '',
+ ends_at: next.highlight?.ends_at ?? '',
+ note: next.highlight?.note ?? '',
+ status: next.highlight?.status ?? 'draft',
+ });
+ setConfirm(false);
+ onChanged();
+ setMessage(
+ loadPlan
+ ? 'Plano guardado carregado para conferência.'
+ : 'Disponibilidade e denúncias actualizadas. As observações e o plano local foram preservados.',
+ );
+ } catch (error) {
+ setMessage(
+ error instanceof Error
+ ? error.message
+ : 'Não foi possível actualizar.',
+ );
+ } finally {
+ setBusy(false);
+ }
+ }
+ function owner(action: string) {
+ if (value)
+ void command('operacoes', {
+ action,
+ etag: value.etag,
+ confirm: true,
+ reason,
+ });
+ }
+ return (
+
+ Disponibilidade e acompanhamento
+ void refresh()}
+ >
+ Actualizar disponibilidade e denúncias
+
+ {value && (
+ <>
+ {labels[value.availability.state] ?? 'Indisponível'}
+
+ {value.availability.policy_pending
+ ? 'Prazo de validade por definir.'
+ : value.availability.expires_at
+ ? `Validade até ${new Date(value.availability.expires_at).toLocaleString('pt-MZ')}.`
+ : 'A contagem de validade inicia-se com a publicação da versão.'}{' '}
+ O NDA tem controlo independente.
+
+ {value.owner && (
+
+
+ Observação (opcional)
+
+
+ owner('withdraw')}
+ >
+ Retirar disponibilidade agora
+
+ {['withdrawn', 'expired'].includes(
+ value.availability.state,
+ ) && (
+
+ owner('request_reactivation')
+ }
+ >
+ Pedir nova validação para reactivar
+
+ )}
+ {value.availability.state ===
+ 'revalidation' && (
+ owner('reactivate')}
+ >
+ Repor disponibilidade após validação
+
+ )}
+
+
+ )}
+ {value.reviewer && (
+
+
+ Denúncias no meu âmbito
+
+
+ Fundamento do tratamento
+
+
+
+ setConfirm(event.target.checked)
+ }
+ />
+ Confirmo a decisão para a versão consultada.
+
+ {value.reports.map((report) => (
+
+
+ {report.reason}
+
+ {report.source_snapshot && (
+
+
+ Conteúdo referido na denúncia
+
+
+ {report.source_snapshot.title}
+
+
+ {report.source_snapshot
+ .summary ??
+ report.source_snapshot
+ .description}
+
+
+ )}
+
+ Versão denunciada:{' '}
+ {report.source_record_version} ·{' '}
+ {
+ (
+ {
+ open: 'Por analisar',
+ in_review: 'Em análise',
+ resolved: 'Tratada',
+ } as Record
+ )[report.status]
+ }
+
+ {report.resolution_reason && (
+ {report.resolution_reason}
+ )}
+
+ {(report.status === 'open'
+ ? ['start_review']
+ : report.status === 'in_review'
+ ? [
+ 'resolve_no_withdrawal',
+ 'resolve_withdrawal',
+ ]
+ : []
+ ).map((action) => (
+
+ void command(
+ `denuncias/${report.id}`,
+ {
+ action,
+ etag: value.etag,
+ version:
+ report.version,
+ confirm,
+ reason,
+ },
+ )
+ }
+ >
+ {
+ (
+ {
+ start_review:
+ 'Iniciar análise',
+ resolve_no_withdrawal:
+ 'Concluir sem retirada',
+ resolve_withdrawal:
+ 'Concluir e retirar disponibilidade',
+ } as Record<
+ string,
+ string
+ >
+ )[action]
+ }
+
+ ))}
+
+
+ ))}
+
+ )}
+ {value.publisher && (
+
+
+ Preparação administrativa de destaque
+
+ void refresh(true)}
+ >
+ Carregar plano guardado (substitui os campos
+ locais)
+
+
+ Registo interno, sem cobrança, pagamento ou
+ promoção comercial activa.
+
+
+
+ Designação
+
+ setPlan({
+ ...plan,
+ label: event.target.value,
+ })
+ }
+ />
+
+
+ Estado
+
+ setPlan({
+ ...plan,
+ status: event.target.value,
+ })
+ }
+ >
+
+ Rascunho administrativo
+
+
+ Arquivado
+
+
+
+
+ Início previsto (UTC)
+
+ setPlan({
+ ...plan,
+ starts_at: event.target.value
+ ? `${event.target.value}Z`
+ : '',
+ })
+ }
+ />
+
+
+ Fim previsto (UTC)
+
+ setPlan({
+ ...plan,
+ ends_at: event.target.value
+ ? `${event.target.value}Z`
+ : '',
+ })
+ }
+ />
+
+
+ Fundamento
+
+
+
+ setConfirm(event.target.checked)
+ }
+ />
+ Conferi a preparação administrativa.
+
+
+ void command(
+ 'destaque-administrativo',
+ {
+ ...plan,
+ starts_at:
+ plan.starts_at || null,
+ ends_at: plan.ends_at || null,
+ etag: value.etag,
+ version:
+ value.highlight?.version ??
+ 0,
+ confirm,
+ },
+ )
+ }
+ >
+ Guardar preparação
+
+
+
+ )}
+
+ Registar denúncia fundamentada
+
+
+ Histórico de disponibilidade
+
+ {value.history
+ .filter((event) =>
+ [
+ 'offer_withdrawn',
+ 'offer_reactivation_requested',
+ 'offer_reactivated_privately',
+ 'offer_expired',
+ ].includes(event.action),
+ )
+ .map((event) => (
+
+ {
+ (
+ {
+ offer_withdrawn:
+ 'Disponibilidade retirada',
+ offer_reactivation_requested:
+ 'Nova validação pedida',
+ offer_reactivated_privately:
+ 'Disponibilidade reposta',
+ offer_expired:
+ 'Validade terminada',
+ } as Record
+ )[event.action]
+ }{' '}
+ ·{' '}
+ {new Date(event.at).toLocaleString(
+ 'pt-MZ',
+ )}
+
+ ))}
+
+
+ >
+ )}
+ {message}
+
+ );
+}
diff --git a/resources/js/components/public-offer-card.tsx b/resources/js/components/public-offer-card.tsx
index 79ed81c1..244d382d 100644
--- a/resources/js/components/public-offer-card.tsx
+++ b/resources/js/components/public-offer-card.tsx
@@ -38,6 +38,12 @@ export default function PublicOfferCard({
Publicada em{' '}
{new Date(offer.published_at).toLocaleDateString('pt-MZ')}
+ {offer.expires_at && (
+
+ Validade até{' '}
+ {new Date(offer.expires_at).toLocaleString('pt-MZ')}
+
+ )}
);
}
diff --git a/resources/js/lib/public-offer.ts b/resources/js/lib/public-offer.ts
index edd76e01..4818a5e0 100644
--- a/resources/js/lib/public-offer.ts
+++ b/resources/js/lib/public-offer.ts
@@ -11,6 +11,7 @@ export type PublicOffer = {
};
status: 'published';
published_at: string;
+ expires_at?: string | null;
revision: number;
url: string;
};
diff --git a/resources/js/pages/management/editorial-workflow.tsx b/resources/js/pages/management/editorial-workflow.tsx
index 7ae27bfb..a793f802 100644
--- a/resources/js/pages/management/editorial-workflow.tsx
+++ b/resources/js/pages/management/editorial-workflow.tsx
@@ -41,6 +41,7 @@ export default function EditorialWorkflow({
resourceAccess,
opportunitiesUrl,
offersUrl = null,
+ offerOperationsUrl = null,
opportunityPending,
clustersUrl,
portfolioUrl,
@@ -56,6 +57,7 @@ export default function EditorialWorkflow({
resourceAccess: boolean;
opportunitiesUrl: string | null;
offersUrl?: string | null;
+ offerOperationsUrl?: string | null;
opportunityPending: number;
clustersUrl?: string | null;
portfolioUrl?: string | null;
@@ -110,6 +112,7 @@ export default function EditorialWorkflow({
label: 'Ofertas — revisão privada',
href: offersUrl,
},
+ { label: 'Ofertas — acompanhamento', href: offerOperationsUrl },
{
label: 'Biblioteca e editores',
href: resourceAccess ? '/management/portal-services' : null,
diff --git a/resources/js/pages/offers/preparation-index.tsx b/resources/js/pages/offers/preparation-index.tsx
index 9d30d8a3..36d5994b 100644
--- a/resources/js/pages/offers/preparation-index.tsx
+++ b/resources/js/pages/offers/preparation-index.tsx
@@ -12,12 +12,17 @@ export default function OfferPreparations({
title: string | null;
reference: string;
state: string;
+ availability?: string;
}[];
prev_page_url: string | null;
next_page_url: string | null;
};
canCreate: boolean;
- filters?: { state?: string };
+ filters?: {
+ state?: string;
+ operational?: boolean;
+ pending_reports?: boolean;
+ };
}) {
return (
@@ -25,7 +30,9 @@ export default function OfferPreparations({
- As minhas ofertas
+ {filters.operational || filters.pending_reports
+ ? 'Ofertas — acompanhamento'
+ : 'As minhas ofertas'}
Prepare e guarde as suas ofertas num espaço privado.
@@ -62,6 +69,19 @@ export default function OfferPreparations({
))}
+ {filters.operational && (
+
+
+ Todas no meu âmbito
+
+
+ Denúncias pendentes
+
+
+ )}
{!records.data.length && (
Ainda não existem ofertas neste âmbito.
)}
@@ -77,6 +97,24 @@ export default function OfferPreparations({
{record.reference} ·{' '}
{offerStates[record.state] ?? record.state}
+ {record.availability && (
+ <>
+ {' '}
+ ·{' '}
+ {
+ (
+ {
+ available: 'Disponível',
+ withdrawn: 'Retirada',
+ expired: 'Expirada',
+ revalidation:
+ 'Aguarda nova validação',
+ unavailable: 'Indisponível',
+ } as Record
+ )[record.availability]
+ }
+ >
+ )}
))}
diff --git a/resources/js/pages/offers/preparation.tsx b/resources/js/pages/offers/preparation.tsx
index 1e7b0c4a..3b6bdfd8 100644
--- a/resources/js/pages/offers/preparation.tsx
+++ b/resources/js/pages/offers/preparation.tsx
@@ -15,6 +15,7 @@ import OfferParticularsFields, {
} from '@/components/offer-particulars';
import OfferPrivateResources from '@/components/offer-private-resources';
import OfferConfidentialAccess from '@/components/offer-confidential-access';
+import OfferOperations from '@/components/offer-operations';
type Offer = OfferParticulars & {
price_mode?: string | null;
@@ -196,6 +197,23 @@ export default function OfferPreparation({
{message}
+ {saved && (
+
{
+ void profileJson(
+ `/minhas-oportunidades/ofertas/${saved.id}`,
+ {},
+ 'GET',
+ )
+ .then((result) => {
+ setSaved(result.record);
+ setConfirmed(false);
+ })
+ .catch((error) => setMessage(error.message));
+ }}
+ />
+ )}
{Object.keys(errors).length > 0 && (
+
+ Denunciar esta oferta
+
{territoryId && (
diff --git a/resources/js/pages/offers/report.tsx b/resources/js/pages/offers/report.tsx
new file mode 100644
index 00000000..569bb468
--- /dev/null
+++ b/resources/js/pages/offers/report.tsx
@@ -0,0 +1,80 @@
+import { Head } from '@inertiajs/react';
+import { useState } from 'react';
+import { profileJson } from '@/lib/profile-json';
+import { createOperationKey } from '@/lib/operation-key';
+export default function OfferReport({ offerId }: { offerId: number }) {
+ const [key] = useState(createOperationKey);
+ const [reason, setReason] = useState('');
+ const [confirm, setConfirm] = useState(false);
+ const [busy, setBusy] = useState(false);
+ const [sent, setSent] = useState(false);
+ const [message, setMessage] = useState('');
+ async function submit() {
+ setBusy(true);
+ try {
+ const result = await profileJson(
+ `/minhas-oportunidades/ofertas/${offerId}/denuncias`,
+ { client_key: key, reason, confirm },
+ );
+ setSent(true);
+ setMessage(`Denúncia registada. Referência: ${result.id}.`);
+ } catch (error) {
+ setMessage(
+ error instanceof Error
+ ? error.message
+ : 'Não foi possível registar a denúncia.',
+ );
+ } finally {
+ setBusy(false);
+ }
+ }
+ return (
+
+
+ Denunciar oferta
+
+ Explique os factos que justificam a denúncia. A equipa
+ responsável irá analisá-la em privado.
+
+
+ {message}
+
+ );
+}
diff --git a/routes/console.php b/routes/console.php
index 2468f426..50e33d49 100644
--- a/routes/console.php
+++ b/routes/console.php
@@ -10,3 +10,6 @@ Artisan::command('inspire', function () {
Schedule::command('investmoz:escalate-overdue-tasks')->everyFifteenMinutes()->withoutOverlapping();
Schedule::command('investmoz:dispatch-opportunity-alerts')->everyFiveMinutes()->withoutOverlapping();
+if (config('private_offers.expiry_scheduler_enabled')) {
+ Schedule::command('investmoz:offers-expire --confirm')->everyMinute()->withoutOverlapping();
+}
diff --git a/routes/web.php b/routes/web.php
index f9833f05..1a267973 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -44,6 +44,7 @@ use App\Http\Controllers\PortalManagementController;
use App\Http\Controllers\PortalProfileController;
use App\Http\Controllers\PortalProfileInstallationController;
use App\Http\Controllers\PortalServicesController;
+use App\Http\Controllers\PrivateOfferOperationsController;
use App\Http\Controllers\PrivateOfferPreparationController;
use App\Http\Controllers\ProactiveSupplyController;
use App\Http\Controllers\ProductExperimentController;
@@ -252,6 +253,12 @@ Route::middleware(['auth', 'verified', EnsurePortalProfiles::class])->group(func
Route::post('/{id}/confidencialidade', [$offers, 'confidentialityAction'])->whereNumber('id');
Route::get('/{id}/confidencial', [$offers, 'confidentialRead'])->whereNumber('id');
Route::get('/{id}/nda', [$offers, 'nda'])->whereNumber('id');
+ Route::get('/{id}/operacoes', [PrivateOfferOperationsController::class, 'show'])->whereNumber('id');
+ Route::post('/{id}/operacoes', [PrivateOfferOperationsController::class, 'owner'])->whereNumber('id');
+ Route::get('/{id}/denunciar', [PrivateOfferOperationsController::class, 'reportForm'])->whereNumber('id');
+ Route::post('/{id}/denuncias', [PrivateOfferOperationsController::class, 'report'])->whereNumber('id');
+ Route::post('/{id}/denuncias/{reportId}', [PrivateOfferOperationsController::class, 'handleReport'])->whereNumber('id')->whereUuid('reportId');
+ Route::post('/{id}/destaque-administrativo', [PrivateOfferOperationsController::class, 'highlight'])->whereNumber('id');
});
Route::get('/minhas-oportunidades/oferta-interna', [ProactiveSupplyController::class, 'index'])->middleware('throttle:60,1');
Route::get('/minhas-oportunidades/oferta-interna/revisao', [ProactiveSupplyController::class, 'review'])->middleware(['throttle:60,1', PrivateInvestmentResponse::class]);
diff --git a/tests/Feature/PrivateOfferOperationsTest.php b/tests/Feature/PrivateOfferOperationsTest.php
new file mode 100644
index 00000000..ea6f2d96
--- /dev/null
+++ b/tests/Feature/PrivateOfferOperationsTest.php
@@ -0,0 +1,351 @@
+withoutVite();
+ Queue::fake();
+ Mail::fake();
+ Storage::fake('local');
+ config(['portal.profile_access_enabled' => true, 'editorial_workflow.enabled' => true, 'private_offers.validity_days' => null, 'private_offers.validity_policy_reference' => null]);
+ }
+
+ private function actor(?string $role = null, string $category = 'business_sale'): User
+ {
+ $actor = User::factory()->create(['two_factor_secret' => Crypt::encryptString('SYNTHETIC QA'), 'two_factor_confirmed_at' => now()]);
+ if ($role) {
+ app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), User::factory()->create(), ['user_id' => $actor->id,
+ 'role' => $role, 'organization_id' => null, 'responsibilities' => [], 'opportunity_types' => [$category], 'source' => 'synthetic-test-fixture']);
+ }
+
+ return $actor;
+ }
+
+ private function login(User $actor): static
+ {
+ $actor = $actor->fresh();
+
+ return $this->actingAs($actor)->withSession(['login.id' => null, 'portal.mfa_actor' => $actor->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $actor->portal_access_revision]);
+ }
+
+ private function draft(User $owner): Opportunity
+ {
+ $contact = (string) Str::uuid();
+ DB::table('contacts')->insert(['id' => $contact, 'full_name' => 'PRIVATE CONTACT CANARY', 'source_system' => 'portal-user-contact', 'external_id' => 'user:'.$owner->id]);
+ $id = $this->login($owner)->postJson(self::BASE.'/rascunhos', ['creation_key' => (string) Str::uuid(), 'opportunity_category' => 'business_sale', 'title' => 'PRIVATE TITLE CANARY',
+ 'description' => 'PRIVATE DESCRIPTION CANARY', 'private_offer' => ['contact_id' => $contact, 'price_mode' => 'amount', 'amount' => '1200.01', 'currency' => 'MZN'], 'reason' => self::REASON])->assertCreated()->json('record.id');
+
+ return Opportunity::findOrFail($id);
+ }
+
+ private function etag(Opportunity $record): string
+ {
+ return app(OpportunityManagement::class)->etag($record->fresh());
+ }
+
+ private function approve(User $owner, Opportunity $record): void
+ {
+ $this->login($owner)->postJson(self::BASE.'/'.$record->id.'/submeter', ['etag' => $this->etag($record), 'confirm' => true])->assertOk();
+ $reviewer = $this->actor('validator');
+ foreach (['start_review', 'approve'] as $action) {
+ $this->login($reviewer)->postJson(self::BASE.'/'.$record->id.'/revisao', ['etag' => $this->etag($record), 'action' => $action, 'reason' => self::REASON])->assertOk();
+ }
+ }
+
+ private function publication(Opportunity $record, ?string $at = null): void
+ {
+ $record = $record->fresh();
+ $context = $record->workflow_context;
+ $payload = ['title' => 'Synthetic public summary', 'summary' => 'Safe synthetic summary.', 'category' => 'business_sale', 'province_id' => null, 'district_id' => null,
+ 'price_mode' => 'amount', 'amount' => '1200.01', 'currency' => 'MZN'];
+ $hash = OfferPublicCatalogue::projectionHash($payload);
+ $context['offer_publication'] = ['contract' => OfferPublicCatalogue::CONTRACT, 'qa_fixture' => true, 'revision' => 1, 'payload' => $payload,
+ 'approval' => ['state' => 'approved', 'hash' => $hash, 'actor_id' => 1, 'at' => now()->subYears(2)->toIso8601String()],
+ 'publication' => ['state' => 'published', 'hash' => $hash, 'actor_id' => 2, 'at' => $at ?? now()->subHour()->toIso8601String()],
+ 'offer_cycle' => $context['offer_operation']['cycle'] ?? 0, 'editorial_hash' => $context['approval']['hash'] ?? 'synthetic-legacy'];
+ $record->workflow_context = $context;
+ $record->save();
+ $this->app->instance(OfferPublicCatalogue::class, new OfferPublicCatalogue(true));
+ }
+
+ private function owner(User $actor, Opportunity $record, string $action, ?string $etag = null): TestResponse
+ {
+ return $this->login($actor)->postJson(self::BASE.'/'.$record->id.'/operacoes', ['action' => $action, 'etag' => $etag ?? $this->etag($record), 'confirm' => true]);
+ }
+
+ public function test_withdrawal_is_immediate_with_a_stale_form_and_excludes_every_public_consumer(): void
+ {
+ $owner = $this->actor('promoter');
+ $record = $this->draft($owner);
+ $this->publication($record);
+ $this->getJson('/api/v1/offers')->assertJsonPath('results.total', 1)->assertHeader('Cache-Control', 'no-store, private');
+ $this->owner($owner, $record, 'withdraw', str_repeat('0', 64))->assertOk()->assertJsonPath('availability.state', 'withdrawn');
+ $this->getJson('/api/v1/offers')->assertJsonPath('results.total', 0);
+ $this->getJson('/api/v1/offers/'.$record->id)->assertNotFound();
+ $this->get('/ofertas/'.$record->id)->assertNotFound();
+ $this->getJson('/api/v1/offers/map')->assertJsonPath('matched_count', 0)->assertJsonPath('features', []);
+ $this->assertStringNotContainsString('Synthetic public summary', $this->get('/api/v1/offers/export')->streamedContent());
+ $this->owner($owner, $record, 'withdraw')->assertOk();
+ $this->assertSame(1, DB::table('audit_events')->where('action', 'offer_withdrawn')->count());
+ $this->assertNotNull($record->fresh()->workflow_context['offer_publication']);
+ $this->assertNull($record->fresh()->published_at);
+ }
+
+ public function test_reactivation_requires_a_new_exact_validation_and_does_not_resurrect_old_publication(): void
+ {
+ $owner = $this->actor('promoter');
+ $record = $this->draft($owner);
+ $this->approve($owner, $record);
+ $this->publication($record);
+ $old = $record->fresh()->workflow_context['approval']['hash'];
+ $this->owner($owner, $record, 'withdraw')->assertOk();
+ $this->owner($owner, $record, 'reactivate')->assertConflict();
+ $this->owner($owner, $record, 'request_reactivation')->assertOk()->assertJsonPath('availability.state', 'revalidation');
+ $this->owner($owner, $record, 'reactivate')->assertConflict();
+ $this->approve($owner, $record);
+ $fresh = $record->fresh();
+ $this->assertNotSame($old, $fresh->workflow_context['approval']['hash']);
+ $this->assertSame(1, $fresh->workflow_context['submission']['payload']['operational_cycle']);
+ $this->owner($owner, $record, 'reactivate')->assertOk()->assertJsonPath('availability.state', 'available');
+ $this->getJson('/api/v1/offers/'.$record->id)->assertNotFound();
+ $this->publication($record);
+ $this->getJson('/api/v1/offers/'.$record->id)->assertOk()->assertJsonPath('data.title', 'Synthetic public summary');
+ $this->assertSame('draft', $record->fresh()->status);
+ $this->assertNull($record->fresh()->published_at);
+ }
+
+ public function test_withdrawal_during_review_can_be_reopened_by_the_owner_without_erasing_history(): void
+ {
+ $owner = $this->actor('promoter');
+ $record = $this->draft($owner);
+ $this->login($owner)->postJson(self::BASE.'/'.$record->id.'/submeter', ['etag' => $this->etag($record), 'confirm' => true])->assertOk();
+ $submission = $record->fresh()->workflow_context['submission'];
+ $this->owner($owner, $record, 'withdraw')->assertOk();
+ $this->owner($owner, $record, 'request_reactivation')->assertOk();
+ $history = $record->fresh()->workflow_context['decision_history'];
+ $this->assertSame($submission['hash'], end($history)['submission']['hash']);
+ $this->login($owner)->getJson(self::BASE.'/'.$record->id)->assertOk()->assertJsonPath('record.canEdit', true);
+ }
+
+ public function test_expiry_is_immediate_at_the_boundary_frozen_and_independent_of_nda_and_scheduler(): void
+ {
+ config(['private_offers.validity_days' => 7, 'private_offers.validity_policy_reference' => 'SYNTHETIC QA POLICY NOT A FINAL DEADLINE']);
+ $this->travelTo(now()->startOfSecond());
+ $owner = $this->actor('promoter');
+ $record = $this->draft($owner);
+ $this->approve($owner, $record);
+ $start = now()->toIso8601String();
+ $this->publication($record, $start);
+ $this->getJson('/api/v1/offers/'.$record->id)->assertOk()->assertJsonPath('data.expires_at', app(OfferAvailability::class)->record($record->fresh())['expires_at']);
+ config(['private_offers.validity_days' => 365, 'private_offers.nda' => null]);
+ $this->travel(7)->days();
+ $view = app(OfferAvailability::class)->record($record->fresh());
+ $this->assertSame('expired', $view['state']);
+ $this->getJson('/api/v1/offers?currency=MZN&min_amount=1')->assertOk()->assertJsonPath('results.total', 0);
+ $this->getJson('/api/v1/offers/'.$record->id)->assertNotFound();
+ $before = $record->fresh()->management_version;
+ $this->artisan('investmoz:offers-expire')->assertSuccessful();
+ $this->assertSame($before, $record->fresh()->management_version);
+ $this->artisan('investmoz:offers-expire', ['--confirm' => true])->assertSuccessful();
+ $this->artisan('investmoz:offers-expire', ['--confirm' => true])->assertSuccessful();
+ $this->assertSame(1, DB::table('audit_events')->where('action', 'offer_expired')->count());
+ $this->owner($owner, $record, 'reactivate')->assertConflict();
+ $this->travelBack();
+ }
+
+ public function test_no_deadline_is_invented_when_the_policy_is_pending(): void
+ {
+ $owner = $this->actor('promoter');
+ $record = $this->draft($owner);
+ $this->approve($owner, $record);
+ $this->publication($record, now()->subYear()->toIso8601String());
+ $this->assertSame(['state' => 'available', 'expires_at' => null, 'policy_pending' => true], app(OfferAvailability::class)->record($record->fresh()));
+ $this->assertFalse(config('private_offers.expiry_scheduler_enabled'));
+ $this->assertNull(config('private_offers.validity_days'));
+ }
+
+ public function test_strangers_reviewers_wrong_scopes_and_auditors_cannot_withdraw_or_reactivate(): void
+ {
+ $owner = $this->actor('promoter');
+ $record = $this->draft($owner);
+ foreach ([$this->actor(), $this->actor('promoter'), $this->actor('validator'), $this->actor('auditor')] as $other) {
+ $this->owner($other, $record, 'withdraw')->assertForbidden();
+ }
+ $this->owner($owner, $record, 'withdraw')->assertOk();
+ $this->owner($owner, $record, 'request_reactivation', str_repeat('0', 64))->assertConflict();
+ $this->assertSame('withdrawn', app(OfferAvailability::class)->record($record->fresh())['state']);
+ }
+
+ public function test_reports_are_private_idempotent_and_treated_in_the_existing_dashboard_and_tasks(): void
+ {
+ $owner = $this->actor('promoter');
+ $record = $this->draft($owner);
+ $this->publication($record);
+ $reporter = $this->actor();
+ $key = (string) Str::uuid();
+ $input = ['client_key' => $key, 'reason' => 'PRIVATE REPORT CANARY with concrete synthetic facts.', 'confirm' => true];
+ $url = self::BASE.'/'.$record->id;
+ $this->login($reporter)->postJson($url.'/denuncias', $input)->assertCreated();
+ $this->postJson($url.'/denuncias', $input)->assertCreated()->assertJsonPath('replayed', true);
+ $this->postJson($url.'/denuncias', [...$input, 'reason' => self::REASON])->assertConflict();
+ $this->assertSame(1, DB::table('tasks')->where('task_type', 'offer_report_'.$key)->count());
+ $this->assertStringNotContainsString('PRIVATE REPORT CANARY', json_encode($record->fresh()->workflow_context));
+ $reviewer = $this->actor('validator');
+ $this->login($reviewer)->getJson('/management/editorial')->assertOk()->assertJsonPath('offerOperationsUrl', '/minhas-oportunidades/ofertas?operational=1');
+ $this->get(self::BASE.'?operational=1&pending_reports=1')->assertOk();
+ $this->getJson($url.'/operacoes')->assertOk()->assertJsonPath('reports.0.reason', $input['reason']);
+ $this->getJson($url.'/operacoes')->assertJsonPath('reports.0.source_snapshot.title', 'Synthetic public summary');
+ $handle = ['etag' => $this->etag($record), 'version' => 1, 'action' => 'start_review', 'reason' => self::REASON, 'confirm' => true];
+ $this->login($owner)->postJson($url.'/denuncias/'.$key, $handle)->assertForbidden();
+ $this->login($reviewer)->postJson($url.'/denuncias/'.$key, $handle)->assertOk();
+ $this->postJson($url.'/denuncias/'.$key, [...$handle, 'action' => 'resolve_withdrawal'])->assertConflict();
+ $this->postJson($url.'/denuncias/'.$key, [...$handle, 'etag' => $this->etag($record), 'version' => 2, 'action' => 'resolve_withdrawal'])->assertOk()->assertJsonPath('availability.state', 'withdrawn');
+ $this->assertSame('done', DB::table('tasks')->where('task_type', 'offer_report_'.$key)->value('status'));
+ $this->login($owner)->getJson($url.'/operacoes')->assertOk()->assertJsonPath('reports', []);
+ $this->getJson('/api/v1/offers/'.$record->id)->assertNotFound();
+ $this->login($reporter)->postJson($url.'/denuncias', $input)->assertCreated()->assertJsonPath('replayed', true);
+ }
+
+ public function test_drafts_cannot_be_reported_by_unrelated_users_and_report_read_requires_current_scope_and_mfa(): void
+ {
+ $owner = $this->actor('promoter');
+ $record = $this->draft($owner);
+ $other = $this->actor();
+ $this->login($other)->postJson(self::BASE.'/'.$record->id.'/denuncias', ['client_key' => (string) Str::uuid(), 'reason' => self::REASON, 'confirm' => true])->assertNotFound();
+ $this->getJson(self::BASE.'/'.$record->id.'/operacoes')->assertForbidden();
+ $this->login($this->actor('validator', 'real_estate'))->get(self::BASE.'?operational=1')->assertOk();
+ $this->getJson(self::BASE.'/'.$record->id.'/operacoes')->assertForbidden();
+ $this->login($this->actor('validator'))->withSession(['portal.mfa_at' => time() - 1000])->get(self::BASE.'?operational=1')->assertForbidden();
+ $this->login($this->actor('auditor'))->get(self::BASE.'?operational=1')->assertForbidden();
+ }
+
+ public function test_highlights_are_versioned_administrative_only_and_cannot_set_commercial_flags(): void
+ {
+ $owner = $this->actor('promoter');
+ $record = $this->draft($owner);
+ $publisher = $this->actor('publisher');
+ $url = self::BASE.'/'.$record->id;
+ $plan = ['etag' => $this->etag($record), 'version' => 0, 'label' => 'Synthetic plan', 'status' => 'draft', 'note' => self::REASON, 'confirm' => true, 'is_featured' => true, 'payment_enabled' => true];
+ $this->login($owner)->postJson($url.'/destaque-administrativo', $plan)->assertForbidden();
+ $this->login($this->actor('validator'))->postJson($url.'/destaque-administrativo', $plan)->assertForbidden();
+ $this->login($publisher)->postJson($url.'/destaque-administrativo', $plan)->assertOk()->assertJsonPath('commercial_activation', false);
+ $this->postJson($url.'/destaque-administrativo', $plan)->assertConflict();
+ $this->postJson($url.'/destaque-administrativo', [...$plan, 'etag' => $this->etag($record), 'version' => 1, 'status' => 'active'])->assertUnprocessable();
+ $this->postJson($url.'/destaque-administrativo', [...$plan, 'etag' => $this->etag($record), 'version' => 1, 'status' => 'archived'])->assertOk()->assertJsonPath('highlight.version', 2);
+ $this->assertFalse($record->fresh()->is_featured);
+ $this->assertNull($record->fresh()->published_at);
+ $this->login($owner)->getJson($url.'/operacoes')->assertJsonPath('highlight', null);
+ }
+
+ public function test_a_withdrawn_first_draft_still_requires_its_first_editorial_validation(): void
+ {
+ $owner = $this->actor('promoter');
+ $record = $this->draft($owner);
+ $this->owner($owner, $record, 'withdraw')->assertOk();
+ $this->owner($owner, $record, 'request_reactivation')->assertOk()->assertJsonPath('availability.state', 'revalidation');
+ $this->owner($owner, $record, 'reactivate')->assertConflict();
+ $this->approve($owner, $record);
+ $this->owner($owner, $record, 'reactivate')->assertOk();
+ $this->assertSame('draft', $record->fresh()->status);
+ }
+
+ public function test_withdrawal_and_revalidation_do_not_restore_previous_nda_grants(): void
+ {
+ $owner = $this->actor('promoter');
+ $record = $this->draft($owner);
+ $this->approve($owner, $record);
+ $bytes = "%PDF-1.7\nSYNTHETIC QA NOT A LEGAL NDA\n%%EOF";
+ $docId = (string) Str::uuid();
+ $sha = hash('sha256', $bytes);
+ Storage::disk('local')->put('qa-nda/'.$docId, Crypt::encryptString($bytes));
+ DB::table('documents')->insert(['id' => $docId, 'file_name' => 'qa-only.pdf', 'media_type' => 'application/pdf', 'storage_key' => 'qa-nda/'.$docId, 'storage_encryption' => 'laravel',
+ 'sha256' => $sha, 'status' => 'active', 'malware_scan_status' => 'clean', 'malware_scanned_sha256' => $sha, 'malware_engine' => 'QA-Test-Double', 'version_no' => 1, 'data_classification' => 'restricted']);
+ config(['private_offers.nda' => ['document_id' => $docId, 'version' => 1, 'sha256' => $sha, 'approval_reference' => 'SYNTHETIC QA NOT LEGAL APPROVAL']]);
+ $nda = app(PrivateOfferConfidentiality::class)->nda();
+ $interested = $this->actor();
+ $url = self::BASE.'/'.$record->id;
+ $input = ['action' => 'accept', 'confirm' => true, 'version_hash' => $record->fresh()->workflow_context['approval']['hash'], 'nda_sha256' => $sha, 'nda_fingerprint' => $nda['fingerprint']];
+ $this->login($interested)->postJson($url.'/confidencialidade', $input)->assertOk();
+ $acceptance = $this->getJson($url.'/confidencialidade')->json('acceptance');
+ $this->login($owner)->postJson($url.'/confidencialidade', [...$input, 'action' => 'authorize', 'interested_user_id' => $interested->id, 'acceptance_id' => $acceptance['id'], 'reason' => self::REASON])->assertOk();
+ $this->login($interested)->getJson($url.'/confidencial')->assertOk();
+ $this->owner($owner, $record, 'withdraw')->assertOk();
+ $this->login($interested)->getJson($url.'/confidencial')->assertConflict();
+ $this->owner($owner, $record, 'request_reactivation')->assertOk();
+ $this->approve($owner, $record);
+ $this->owner($owner, $record, 'reactivate')->assertOk();
+ $this->login($interested)->getJson($url.'/confidencial')->assertForbidden();
+ $this->assertSame($acceptance['id'], $record->fresh()->workflow_context['offer_confidentiality']['acceptances'][(string) $interested->id]['id']);
+ }
+
+ public function test_operational_and_report_screens_use_real_compiled_entries_and_private_cache_headers(): void
+ {
+ $owner = $this->actor('promoter');
+ $record = $this->draft($owner);
+ $directory = getenv('EDITORIAL_QA_BUILD_DIRECTORY') ?: 'build';
+ $manifest = json_decode(file_get_contents(public_path($directory.'/manifest.json')), true, 512, JSON_THROW_ON_ERROR);
+ $this->app->instance(Vite::class, (new Vite)->useBuildDirectory($directory)->useHotFile(storage_path('phpunit-offers-no-hot')));
+ foreach (['offers/preparation' => self::BASE.'/'.$record->id, 'offers/report' => self::BASE.'/'.$record->id.'/denunciar'] as $page => $url) {
+ $key = 'resources/js/pages/'.$page.'.tsx';
+ $this->assertArrayHasKey($key, $manifest);
+ $response = $this->login($owner)->get($url, ['Accept' => 'text/html'])->assertOk()->assertHeader('Cache-Control', 'no-store, private')->assertSee($manifest[$key]['file'], false);
+ if ($page === 'offers/report') {
+ $response->assertDontSee('PRIVATE CONTACT CANARY')->assertDontSee('PRIVATE DESCRIPTION CANARY');
+ }
+ }
+ $reviewer = $this->actor('validator');
+ foreach (['offers/preparation-index' => self::BASE.'?operational=1', 'management/editorial-workflow' => '/management/editorial'] as $page => $url) {
+ $key = 'resources/js/pages/'.$page.'.tsx';
+ $this->assertArrayHasKey($key, $manifest);
+ $this->login($reviewer)->get($url, ['Accept' => 'text/html'])->assertOk()->assertHeader('Cache-Control', 'no-store, private')->assertSee($manifest[$key]['file'], false);
+ }
+ }
+
+ public function test_an_edited_private_approval_does_not_extend_the_existing_public_deadline(): void
+ {
+ config(['private_offers.validity_days' => 7, 'private_offers.validity_policy_reference' => 'SYNTHETIC QA FIRST']);
+ $owner = $this->actor('promoter');
+ $record = $this->draft($owner);
+ $this->approve($owner, $record);
+ $this->publication($record);
+ $deadline = app(OfferAvailability::class)->record($record->fresh())['expires_at'];
+ $this->login($owner)->postJson(self::BASE.'/'.$record->id.'/revisao', ['action' => 'request_revision', 'etag' => $this->etag($record), 'reason' => self::REASON])->assertOk();
+ $this->postJson(self::BASE.'/'.$record->id.'/guardar', ['title' => 'Changed private title', 'etag' => $this->etag($record), 'reason' => self::REASON])->assertOk();
+ config(['private_offers.validity_days' => 30, 'private_offers.validity_policy_reference' => 'SYNTHETIC QA SECOND']);
+ $this->approve($owner, $record);
+ $this->assertSame($deadline, app(OfferAvailability::class)->record($record->fresh())['expires_at']);
+ $context = $record->fresh()->workflow_context;
+ unset($context['offer_publication']['editorial_hash']);
+ $record->forceFill(['workflow_context' => $context])->save();
+ $this->assertSame($deadline, app(OfferAvailability::class)->record($record->fresh())['expires_at']);
+ $this->getJson('/api/v1/offers/'.$record->id)->assertOk()->assertJsonPath('data.title', 'Synthetic public summary');
+ }
+}