From 91c185736868b5a59b9290438464be7f8a853022 Mon Sep 17 00:00:00 2001 From: nala Date: Sat, 14 Nov 2026 16:20:30 +0200 Subject: [PATCH] feat: integrate private offers with existing editorial workflow --- .../PortalEditorialWorkflowController.php | 4 +- .../PrivateOfferPreparationController.php | 23 +- app/Jobs/NotifyPrivateOfferDecision.php | 46 +++ app/Services/OpportunityManagement.php | 6 + app/Services/OpportunityProfileWorkflow.php | 117 +++++- app/Services/PortalEditorialWorkflow.php | 44 ++- app/Services/PortalProfileAccess.php | 6 +- app/Services/PrivateOfferPreparation.php | 103 ++++- .../unification/traceability-and-manual.md | 13 +- .../v2/auditoria-portal/matriz-requisitos.csv | 2 +- .../ofertas-fase2-entrega-qa-20261114.md | 75 ++++ .../qa-result.json | 212 +++++++++++ .../js/components/offer-editorial-review.tsx | 259 +++++++++++++ resources/js/lib/offer-editorial.ts | 52 +++ .../pages/management/editorial-workflow.tsx | 6 + .../js/pages/offers/preparation-index.tsx | 29 +- resources/js/pages/offers/preparation.tsx | 29 +- routes/web.php | 1 + tests/Feature/PrivateOfferEditorialTest.php | 356 ++++++++++++++++++ 19 files changed, 1353 insertions(+), 30 deletions(-) create mode 100644 app/Jobs/NotifyPrivateOfferDecision.php create mode 100644 docs/investmoz/v2/auditoria-portal/ofertas-fase2-entrega-qa-20261114.md create mode 100644 outputs/offers-private-phase2-20261114/qa-result.json create mode 100644 resources/js/components/offer-editorial-review.tsx create mode 100644 resources/js/lib/offer-editorial.ts create mode 100644 tests/Feature/PrivateOfferEditorialTest.php diff --git a/app/Http/Controllers/PortalEditorialWorkflowController.php b/app/Http/Controllers/PortalEditorialWorkflowController.php index d956504e..6dc5dcd1 100644 --- a/app/Http/Controllers/PortalEditorialWorkflowController.php +++ b/app/Http/Controllers/PortalEditorialWorkflowController.php @@ -105,7 +105,9 @@ final class PortalEditorialWorkflowController extends Controller } $items = $query->orderByDesc('e.updated_at')->paginate(25, ['r.id', DB::raw("e.payload->'resource'->>'title' AS title"), 'r.document_type', 'r.published', 'e.id as edition_id', 'e.revision_no', 'e.state', 'e.legal_required', 'u.name as responsible'])->withQueryString(); $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, 'opportunityPending' => $workflow->opportunityPending($actor), 'clustersUrl' => $destinations['clusters']['index'] ?? null, + $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, + '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, 'nativePending' => app(PortalEditorialNativeNotices::class)->pending($actor), 'zonesUrl' => app(PortalEditorialNativeNotices::class)->access($actor, 'investment-zones') ? '/management/investment/zones' : null, diff --git a/app/Http/Controllers/PrivateOfferPreparationController.php b/app/Http/Controllers/PrivateOfferPreparationController.php index 6f0d6d2a..2c8457f4 100644 --- a/app/Http/Controllers/PrivateOfferPreparationController.php +++ b/app/Http/Controllers/PrivateOfferPreparationController.php @@ -4,6 +4,8 @@ namespace App\Http\Controllers; use App\Models\Opportunity; use App\Services\OpportunityCanonicalContract; +use App\Services\OpportunityManagement; +use App\Services\OpportunityProfileWorkflow; use App\Services\PortalProfileAccess; use App\Services\PrivateOfferPreparation; use Illuminate\Http\JsonResponse; @@ -15,13 +17,15 @@ 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') - ->orderByDesc('id')->paginate(20); + ->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' => $r->title, 'reference' => $r->reference, - 'state' => data_get($r->workflow_context, 'private_offer_preparation.state'), - ]), 'canCreate' => count($access->navigation($request->user())['contexts']) > 0]); + 'id' => $r->id, 'title' => app(OpportunityManagement::class)->values($r)['title'] ?? null, 'reference' => $r->reference, + 'state' => $access->state($r), + ]), 'filters' => $filters, 'canCreate' => count($access->navigation($request->user())['contexts']) > 0]); } public function edit(Request $request, PrivateOfferPreparation $service, ?int $id = null): Response|JsonResponse @@ -56,4 +60,15 @@ final class PrivateOfferPreparationController extends Controller return response()->json(['record' => $service->presentation($request->user(), $record), 'message' => 'Submissão privada registada. A oferta não foi publicada.']); } + + public function transition(Request $request, PrivateOfferPreparation $service, OpportunityProfileWorkflow $workflow, int $id): JsonResponse + { + $service->record($request->user(), $id); + $data = $request->validate(['etag' => 'required|string|size:64', 'action' => 'required|in:start_review,assign_review,request_correction,reject,approve,request_revision', + 'assignee_user_id' => 'required_if:action,assign_review|integer|min:1', + 'reason' => 'required|string|min:20|max:1000', 'correction_items' => 'sometimes|array', 'correction_items.*' => 'string']); + $record = $workflow->transition($request->user(), $id, $data); + + return response()->json(['record' => $service->presentation($request->user(), $record), 'message' => 'Decisão privada registada. A oferta não foi publicada.']); + } } diff --git a/app/Jobs/NotifyPrivateOfferDecision.php b/app/Jobs/NotifyPrivateOfferDecision.php new file mode 100644 index 00000000..a499e3b4 --- /dev/null +++ b/app/Jobs/NotifyPrivateOfferDecision.php @@ -0,0 +1,46 @@ + */ + public function backoff(): array + { + return [60, 300, 900]; + } + + public function handle(): void + { + if (! config('portal.notifications_enabled')) { + return; + } + $notice = DB::table('portal_editorial_notifications')->where('id', $this->noticeId)->first(); + $user = $notice ? User::query()->whereKey($notice->user_id)->first() : null; + $record = $notice ? Opportunity::query()->whereKey($notice->opportunity_id)->first() : null; + if (! $notice || ! $user || ! $record?->isPrivateOfferPreparation() + || ! in_array($user->email, config('portal.notification_recipients', []), true) + || ! app(PortalEditorialWorkflow::class)->visibleNotice($user, $notice)) { + return; + } + $body = 'Existe uma actualização na revisão privada da oferta '.$record->reference.'. Consulte a decisão e a versão na área protegida: '.url('/minhas-oportunidades/ofertas/'.$record->id); + Mail::raw($body, fn ($message) => $message->to($user->email)->subject('Actualização da oferta — '.$record->reference)); + } +} diff --git a/app/Services/OpportunityManagement.php b/app/Services/OpportunityManagement.php index ffccac47..6c53beb3 100644 --- a/app/Services/OpportunityManagement.php +++ b/app/Services/OpportunityManagement.php @@ -195,6 +195,12 @@ class OpportunityManagement $record->management_closed_once = true; $previous = $record->management_draft ? json_decode($record->management_draft, true) : $record->only(self::FIELDS); $record->management_draft = json_encode([...$previous, ...$changes], JSON_THROW_ON_ERROR); + if ($privateOffer) { + // All columns of an Offer remain private; keep scope/category and location current. + foreach (array_intersect_key($changes, array_flip(self::FIELDS)) as $field => $value) { + $record->{$field} = $value; + } + } } else { foreach ($changes as $key => $value) { if (in_array($key, ['real_estate', 'canonical', 'proactive', 'private_offer', 'maturity_stage', 'maturity_catalogue_version'], true) || (($changes['maturity_catalogue_version'] ?? null) === OpportunityMaturity::VERSION && in_array($key, ['maturity_level', 'maturity_label'], true))) { diff --git a/app/Services/OpportunityProfileWorkflow.php b/app/Services/OpportunityProfileWorkflow.php index aefc9e29..cc1b9d9b 100644 --- a/app/Services/OpportunityProfileWorkflow.php +++ b/app/Services/OpportunityProfileWorkflow.php @@ -4,6 +4,7 @@ namespace App\Services; use App\Models\Opportunity; use App\Models\User; +use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; @@ -24,9 +25,12 @@ class OpportunityProfileWorkflow /** @return list */ public function actions(User $actor, Opportunity $record): array { - if ($record->isPrivateOfferPreparation() || $record->owner_user_id === null || ! $this->access->canView($actor, $record)) { + if ($record->owner_user_id === null || ! $this->access->canView($actor, $record)) { return []; } + if ($record->isPrivateOfferPreparation()) { + return $this->offerActions($actor, $record); + } $actions = []; $state = $this->access->state($record); if ($this->access->promoterRecordScope($actor, $record) @@ -99,6 +103,42 @@ class OpportunityProfileWorkflow return $actions; } + /** Existing roles and transition engine, bounded to the private offer contract. + * @return list */ + private function offerActions(User $actor, Opportunity $record): array + { + if (! app(PortalEditorialWorkflow::class)->available()) { + return []; + } + $state = $this->access->state($record); + $actions = []; + if ($this->access->promoterRecordScope($actor, $record) && $this->access->operational($actor, 'edit', $record->id)) { + if (in_array($state, ['draft', 'needs_information'], true)) { + $actions[] = 'submit'; + } + if (in_array($state, ['rejected', 'approved'], true)) { + $actions[] = 'request_revision'; + } + } + if ($this->access->scoped($actor, 'validator', $record->organization_id, $record->opportunity_category) + && $this->access->operational($actor, 'validate', $record->id) && ! $this->access->conflict($actor, $record)) { + if (in_array($state, ['submitted', 'in_review'], true) && $this->responsibility($actor, $record, 'validation_coordinator')) { + $actions[] = 'assign_review'; + } + $assigned = data_get($record->workflow_context, 'assignment.assignee_user_id'); + if ($assigned === null || (int) $assigned === $actor->id) { + if ($state === 'submitted') { + $actions[] = 'start_review'; + } + if ($state === 'in_review') { + array_push($actions, 'request_correction', 'reject', 'approve'); + } + } + } + + return $actions; + } + public function reauthenticationRequired(User $actor, Opportunity $record): bool { return ! $this->access->mfa($actor) @@ -178,6 +218,7 @@ class OpportunityProfileWorkflow abort_unless(hash_equals($this->management->etag($record), $data['etag']), 409, 'Versão alterada.'); $this->authorizeAction($actor, $record, $action); $context = $record->workflow_context ?? []; + $offer = $record->isPrivateOfferPreparation(); if ($action === 'submit' && app(PortalEditorialWorkflow::class)->available()) { $context['editorial_version_binding'] = 'phase1-exact-v1'; $record->workflow_context = $context; @@ -191,7 +232,7 @@ class OpportunityProfileWorkflow 'review_gate' => ['submitted', 'in_review', 'information_pending', 'approved'], 'publish' => ['approved'], 'suspend' => [$state], 'assign_publication' => ['approved'], - 'request_revision' => ['approved', 'published', 'suspended'], + 'request_revision' => $offer ? ['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'], @@ -205,11 +246,35 @@ class OpportunityProfileWorkflow } $next = $state; $snapshot = []; - if ($action === 'submit' && $record->opportunity_category === 'land_rights') { + if ($offer) { + $snapshot = in_array($action, ['request_revision', 'request_correction'], true) + ? ($context['submission'] ?? []) : app(PrivateOfferPreparation::class)->snapshot($record); + // Explicit first review of an immutable phase-1 submission; no writes on read. + if (in_array($action, ['start_review', 'assign_review'], true) && empty($context['submission'])) { + abort_unless(($context['private_offer_preparation']['version'] ?? null) === 1 + && $record->management_version === (int) ($context['private_offer_preparation']['submitted_revision'] ?? -2) + 1, 409, 'Reconcilie a submissão privada anterior.'); + $snapshot['record_version'] = $context['private_offer_preparation']['submitted_revision']; + $context['submission'] = [...$snapshot, 'actor_id' => $context['private_offer_preparation']['submitted_by'], 'at' => $context['private_offer_preparation']['submitted_at']]; + $context['phase1_binding'] = ['bound_at' => $decision['at'], 'bound_by' => $actor->id, 'hash' => $snapshot['hash'], 'original_reference_fingerprint' => 'not_recorded']; + $context['review_cycle'] = 1; + $decision['review_cycle'] = 1; + $context['phase1_submission'] = $context['private_offer_preparation']; + $record->first_submitted_at ??= $context['private_offer_preparation']['submitted_at']; + $record->management_closed_once = true; + } + if ($action !== 'submit') { + abort_unless(hash_equals($context['submission']['hash'] ?? '', $snapshot['hash']), 409, 'A oferta ou as referências mudaram; prepare nova revisão antes da decisão.'); + } + $decision['hash'] = $snapshot['hash']; + $decision['opportunity_version'] = $record->management_version; + $decision['submitted_revision'] = $action === 'submit' ? $snapshot['record_version'] : ($context['submission']['record_version'] ?? $record->management_version); + $record->workflow_context = $context; + } + if (! $offer && $action === 'submit' && $record->opportunity_category === 'land_rights') { // Serialize the existing JSON-backed §27 uniqueness check across records. DB::select('SELECT pg_advisory_xact_lock(111026,270000)'); } - if (in_array($action, ['submit', 'review_gate', 'pending_opinion', 'respond_opinion', 'approve', 'publish'], true)) { + if (! $offer && in_array($action, ['submit', 'review_gate', 'pending_opinion', 'respond_opinion', 'approve', 'publish'], true)) { $blockers = $this->contract->deliveryBlockers($this->management->values($record)['canonical'] ?? []); try { $snapshot = $this->contract->validateSubmission($record); @@ -228,6 +293,12 @@ class OpportunityProfileWorkflow } } if ($action === 'submit') { + if ($offer && isset($context['submission'])) { + $context['submission_history'][] = ['submission' => $context['submission'], 'approval' => $context['approval'] ?? null, 'rejection' => $context['rejection'] ?? null]; + } + if ($offer) { + unset($context['rejection'], $context['correction_items']); + } // New review attempt, even when the canonical content is unchanged. // Historical decisions/tasks and the content revision keep their IDs. $context['review_cycle'] = max((int) ($context['review_cycle'] ?? 0), $this->submissionCount($context)) + 1; @@ -239,7 +310,7 @@ class OpportunityProfileWorkflow } } $baseline = $context['review_baseline'] ?? $context['submission'] ?? []; - $materiality = app(OpportunityReviewMateriality::class)->reconcile($baseline, $snapshot, $context['reviews'] ?? [], $this->contract->gates($record), ! empty($context['force_review']), $strictEditorial); + $materiality = app(OpportunityReviewMateriality::class)->reconcile($baseline, $snapshot, $context['reviews'] ?? [], $offer ? [] : $this->contract->gates($record), ! empty($context['force_review']), $strictEditorial); $context['materiality'] = array_diff_key($materiality, ['reviews' => true]); $context['reviews'] = $materiality['reviews']; app(OpportunityReviewConditions::class)->reconcile($context, $snapshot, $materiality['reopened']); @@ -270,6 +341,11 @@ class OpportunityProfileWorkflow } $context['review_task_id'] ??= $this->tasks->ensure($record, 'opportunity_review', 'Validação da oportunidade '.$record->reference); $this->tasks->assign($record, $context['review_task_id'], $subject); + if ($offer) { + $decision['previous_assignee_user_id'] = $context['assignment']['assignee_user_id'] ?? null; + $decision['assignee_user_id'] = $subject->id; + $context['assignment_history'][] = $decision; + } $context['assignment'] = [...$decision, 'assignee_user_id' => $subject->id]; $next = 'in_review'; } elseif ($action === 'review_gate') { @@ -292,7 +368,7 @@ class OpportunityProfileWorkflow app(OpportunityReviewConditions::class)->authorize($context, $snapshot, 'approve'); abort_if(count(array_filter($context['opinions'] ?? [], fn ($o) => ($o['hash'] ?? '') === $snapshot['hash'] && ($o['status'] ?? '') === 'pending')) > 0, 422, 'Parecer da versão atual ainda pendente.'); abort_if(count(array_filter($context['opinions'] ?? [], fn ($o) => ($o['hash'] ?? '') === $snapshot['hash'] && ($o['response']['decision'] ?? '') === 'rejected')) > 0, 422, 'Parecer desfavorável exige correção e nova versão.'); - foreach ($this->contract->gates($record) as $gate) { + foreach ($offer ? [] : $this->contract->gates($record) as $gate) { $review = $context['reviews'][$gate] ?? []; abort_unless(($review['decision'] ?? null) === 'approved' && (($review['hash'] ?? '') === $snapshot['hash'] || ($review['accepted_submission_hash'] ?? '') === $snapshot['hash']), 422, 'Gate '.$gate.' pendente, rejeitado ou de outra versão.'); abort_unless(app(OpportunityReviewMateriality::class)->matches($snapshot, $gate, $review, $strictEditorial), 409, 'Âmbito ou versão da revisão alterado.'); @@ -304,7 +380,9 @@ class OpportunityProfileWorkflow $context['approval'] = [...$decision, 'hash' => $snapshot['hash'], 'gates' => $context['reviews'] ?? []]; $this->tasks->complete($context['review_task_id'] ?? null, $data['reason']); $record->workflow_context = $context; - $context['publication_task_id'] = $this->tasks->ensure($record, 'opportunity_publication', 'Preparação editorial '.$record->reference); + if (! $offer) { + $context['publication_task_id'] = $this->tasks->ensure($record, 'opportunity_publication', 'Preparação editorial '.$record->reference); + } $next = 'approved'; } elseif ($action === 'assign_publication') { $subject = User::whereKey($data['assignee_user_id'] ?? 0)->firstOrFail(); @@ -414,6 +492,9 @@ class OpportunityProfileWorkflow $next = count(array_filter($context['opinions'], fn ($o) => ($o['hash'] ?? '') === $snapshot['hash'] && ($o['status'] ?? '') === 'pending')) > 0 ? 'information_pending' : 'in_review'; } elseif ($action === 'request_correction') { abort_unless($data['correction_items'] ?? [], 422, 'Indique campos e documentos a corrigir.'); + if ($offer) { + abort_if(array_diff($data['correction_items'], ['title', 'description', 'opportunity_category', 'private_offer', 'contact', 'location']) !== [], 422, 'Seleccione campos desta oferta.'); + } $this->tasks->complete($context['review_task_id'] ?? null, $data['reason']); $context['correction_items'] = $data['correction_items']; $context['correction_cycles'] = ($context['correction_cycles'] ?? 0) + 1; @@ -423,7 +504,7 @@ class OpportunityProfileWorkflow } elseif ($action === 'request_revision' || $action === 'appeal_reopen') { $this->tasks->complete($context['review_task_id'] ?? null, $data['reason']); $this->tasks->complete($context['publication_task_id'] ?? null, $data['reason']); - $context['decision_history'][] = ['approval' => $context['approval'] ?? null, 'reviews' => $context['reviews'] ?? null, 'submission' => $context['submission'] ?? null]; + $context['decision_history'][] = ['approval' => $context['approval'] ?? null, 'reviews' => $context['reviews'] ?? null, 'submission' => $context['submission'] ?? null, ...($offer ? ['rejection' => $context['rejection'] ?? null] : [])]; $context['review_baseline'] = $context['submission'] ?? []; $context['force_review'] = $action === 'appeal_reopen' || $state === 'suspended'; unset($context['approval']); @@ -476,7 +557,14 @@ class OpportunityProfileWorkflow $context['working_state'] = $next; $context['last_decision'] = $decision; $context['events'][] = [...$decision, 'from' => $state, 'to' => $next]; - if ($record->status !== 'published' || in_array($action, ['suspend', 'archive'], true)) { + if ($offer) { + $context['private_offer_preparation'] = [...$context['private_offer_preparation'], 'version' => 2, 'state' => $next]; + if ($action === 'submit') { + $context['private_offer_preparation'] = [...$context['private_offer_preparation'], 'submitted_by' => $actor->id, + 'submitted_at' => $decision['at'], 'submitted_revision' => $snapshot['record_version']]; + } + } + if (! $offer && ($record->status !== 'published' || in_array($action, ['suspend', 'archive'], true))) { $record->status = $next; } $record->management_version++; @@ -484,7 +572,12 @@ class OpportunityProfileWorkflow $record->save(); app(PortalEditorialWorkflow::class)->notifyOpportunity($actor, $record, $action); $this->access->audit($actor, 'workflow_'.$action, ['record_id' => $record->id, 'before' => $before, - 'after' => ['status' => $next, 'publication_status' => $record->status, 'version' => $record->management_version], 'reason' => $data['reason']]); + 'after' => ['status' => $next, 'publication_status' => $record->status, 'version' => $record->management_version], 'reason' => $data['reason'], + ...($offer ? ['decision' => $decision, 'submission_hash' => $context['submission']['hash'] ?? null, + 'snapshot' => Crypt::encryptString(json_encode($snapshot, JSON_THROW_ON_ERROR))] : [])]); + if ($offer && $action === 'submit') { + $this->access->audit($actor, 'private_offer_submitted', ['record_id' => $record->id, 'version' => $record->management_version, 'public' => false]); + } return $record; }); @@ -520,7 +613,9 @@ class OpportunityProfileWorkflow private function authorizeAction(User $actor, Opportunity $record, string $action): void { - abort_if($record->isPrivateOfferPreparation(), 403, 'A oferta está em preparação privada; o circuito editorial não está disponível nesta fase.'); + if ($record->isPrivateOfferPreparation()) { + abort_unless(in_array($action, $this->actions($actor, $record), true), 403, 'Operação editorial não autorizada para esta oferta privada.'); + } $this->access->authorize($actor, 'view', $record); $promoter = $this->access->promoterRecordScope($actor, $record) && ($record->organization_id !== null || (int) $record->owner_user_id === $actor->id); diff --git a/app/Services/PortalEditorialWorkflow.php b/app/Services/PortalEditorialWorkflow.php index 9b3e6898..5fc22e16 100644 --- a/app/Services/PortalEditorialWorkflow.php +++ b/app/Services/PortalEditorialWorkflow.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Jobs\NotifyPrivateOfferDecision; use App\Models\DevelopmentCorridor; use App\Models\EconomicCluster; use App\Models\InvestmentDossier; @@ -237,12 +238,50 @@ final class PortalEditorialWorkflow } $context = $record->workflow_context ?? []; $recipients = array_unique(array_filter([$record->owner_user_id, $context['assignment']['assignee_user_id'] ?? null, $context['publication_assignment']['assignee_user_id'] ?? null])); + $profiles = app(PortalProfileAccess::class); + if ($record->isPrivateOfferPreparation() && $action === 'assign_review' && isset($context['last_decision']['previous_assignee_user_id'])) { + $recipients[] = $context['last_decision']['previous_assignee_user_id']; + } + if ($record->isPrivateOfferPreparation() && $action === 'submit') { + $coordinators = DB::table('portal_role_assignments')->where('role', 'validator')->whereNull('revoked_at') + ->where('starts_at', '<=', now())->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now())) + ->whereJsonContains('responsibilities', 'validation_coordinator')->pluck('user_id'); + foreach (User::whereIn('id', $coordinators)->get() as $candidate) { + if ($profiles->scoped($candidate, 'validator', $record->organization_id, $record->opportunity_category) + && $profiles->operational($candidate, 'validate', $record->id) && ! $profiles->conflict($candidate, $record)) { + $recipients[] = $candidate->id; + } + } + } + $recipients = array_unique($recipients); foreach ($recipients as $recipient) { if ((int) $recipient === $actor->id) { continue; } - DB::table('portal_editorial_notifications')->insertOrIgnore(['user_id' => $recipient, 'edition_id' => null, 'opportunity_id' => $record->id, 'opportunity_version' => $record->management_version, + if ($record->isPrivateOfferPreparation()) { + $candidate = User::query()->whereKey($recipient)->first(); + if (! $candidate || ! $profiles->canView($candidate, $record) || ! $this->canEnter($candidate)) { + continue; + } + } + $inserted = DB::table('portal_editorial_notifications')->insertOrIgnore(['user_id' => $recipient, 'edition_id' => null, 'opportunity_id' => $record->id, 'opportunity_version' => $record->management_version, 'content_hash' => $context['submission']['hash'] ?? $context['approval']['hash'] ?? null, 'action' => $action, 'created_at' => now()]); + if ($inserted && $record->isPrivateOfferPreparation()) { + $notice = DB::table('portal_editorial_notifications')->where('user_id', $recipient)->where('opportunity_id', $record->id) + ->where('opportunity_version', $record->management_version)->where('action', $action)->first(); + if ($notice) { + DB::afterCommit(function () use ($notice, $actor, $record): void { + try { + NotifyPrivateOfferDecision::dispatch($notice->id); + } catch (\Throwable $error) { + // A committed decision must not become an uncertain HTTP failure. + app(PortalProfileAccess::class)->audit($actor, 'offer_notification_delivery_failed', [ + 'record_id' => $record->id, 'notice_id' => $notice->id, 'error_type' => get_class($error), + ]); + } + }); + } + } } } @@ -334,6 +373,9 @@ final class PortalEditorialWorkflow } $edition = $notice->edition_id ? DB::table('portal_editorial_editions')->where('id', $notice->edition_id)->first() : null; $subjectHref = ($edition->subject_kind ?? null) ? '/management/editorial/subjects/'.$edition->subject_kind.'/'.$edition->subject_key.'?edition='.$edition->id : null; + if ($notice->opportunity_id !== null && Opportunity::query()->whereKey($notice->opportunity_id)->first()?->isPrivateOfferPreparation()) { + $subjectHref = '/minhas-oportunidades/ofertas/'.$notice->opportunity_id; + } $result[] = ['id' => $notice->id, 'action' => $notice->action, 'read_at' => $notice->read_at, 'href' => $subjectHref ?? ($notice->opportunity_id !== null ? '/minhas-oportunidades/'.$notice->opportunity_id : (($edition->demographic_import_id ?? null) ? '/management/editorial/demography/'.$edition->demographic_import_id : (($edition->dossier_id ?? null) ? '/management/editorial/portfolio/'.$edition->dossier_id : '/management/editorial/resources/'.$edition->resource_id)).'?edition='.$notice->edition_id), 'reference' => $notice->opportunity_id !== null ? 'Revisão '.$notice->opportunity_version : 'Edição '.$edition->revision_no]; if (count($result) === 50) { diff --git a/app/Services/PortalProfileAccess.php b/app/Services/PortalProfileAccess.php index c8406dc8..2e9d804d 100644 --- a/app/Services/PortalProfileAccess.php +++ b/app/Services/PortalProfileAccess.php @@ -241,7 +241,7 @@ class PortalProfileAccess public function canEdit(User $actor, Opportunity $record, ?string $newType = null): bool { - if ($record->isPrivateOfferPreparation() && data_get($record->workflow_context, 'private_offer_preparation.state') !== 'draft') { + if ($record->isPrivateOfferPreparation() && ! in_array($this->state($record), ['draft', 'needs_information'], true)) { return false; } if ($record->owner_user_id === null) { @@ -267,6 +267,10 @@ class PortalProfileAccess public function state(Opportunity $record): string { + if ($record->isPrivateOfferPreparation()) { + return data_get($record->workflow_context, 'private_offer_preparation.state', 'draft'); + } + return $record->workflow_context['working_state'] ?? $record->status; } diff --git a/app/Services/PrivateOfferPreparation.php b/app/Services/PrivateOfferPreparation.php index ad2419d0..f655bf1e 100644 --- a/app/Services/PrivateOfferPreparation.php +++ b/app/Services/PrivateOfferPreparation.php @@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; use Illuminate\Validation\ValidationException; -/** Phase 1 adapter: the existing opportunity owns identity, versions and audit. */ +/** Private commercial adapter: the existing opportunity owns identity, workflow and audit. */ final class PrivateOfferPreparation { /** @return array */ @@ -58,17 +58,31 @@ final class PrivateOfferPreparation return $record; } - /** Register private submission only: no editorial task, approval or publication. + /** Register submission through the existing editorial engine when available. * @param array $input */ public function submit(User $actor, int $id, array $input): Opportunity { $data = Validator::make($input, ['etag' => 'required|string|size:64', 'confirm' => 'required|accepted'])->validate(); + if (app(PortalEditorialWorkflow::class)->available()) { + $record = $this->record($actor, $id); + $access = app(PortalProfileAccess::class); + if ($access->state($record) === 'submitted') { + abort_unless($access->promoterRecordScope($actor, $record) && $access->canCreate($actor, $record->organization_id, $record->opportunity_category) + && $access->operational($actor, 'edit', $id), 403); + abort_unless(hash_equals(app(OpportunityManagement::class)->etag($record), $data['etag']), 409); + + return $record; + } + + return app(OpportunityProfileWorkflow::class)->transition($actor, $id, [...$data, 'action' => 'submit', 'reason' => 'Submissão privada da oferta conferida pelo promotor.']); + } return DB::transaction(function () use ($actor, $id, $data) { $record = Opportunity::whereKey($id)->lockForUpdate()->firstOrFail(); abort_unless($record->isPrivateOfferPreparation(), 404); $access = app(PortalProfileAccess::class); $access->authorize($actor, 'view', $record); + abort_if(($record->workflow_context['private_offer_preparation']['version'] ?? 1) >= 2, 503, 'O circuito editorial está indisponível. A versão e o histórico foram preservados; não é permitida a retoma pelo percurso anterior.'); abort_unless($access->promoterRecordScope($actor, $record) && $access->canCreate($actor, $record->organization_id, $record->opportunity_category) && $access->operational($actor, 'edit', $record->id), 403); @@ -106,14 +120,93 @@ final class PrivateOfferPreparation }); } + /** Adapter of the five common fields; never relaxes the full canonical contract. + * @return array */ + public function snapshot(Opportunity $record): array + { + $values = app(OpportunityManagement::class)->values($record); + Validator::make($values, ['title' => 'required|string|max:150', 'description' => 'required|string|max:50000', + 'opportunity_category' => 'required', 'private_offer.price_mode' => 'required|in:amount,negotiable', 'private_offer.contact_id' => 'required|uuid'])->validate(); + $offer = $this->validate($values['private_offer'], true); + if (! app(OpportunityContacts::class)->related($record, $offer['contact_id'])) { + throw ValidationException::withMessages(['private_offer.contact_id' => 'O contacto já não está disponível neste âmbito.']); + } + $contact = DB::table('contacts')->where('id', $offer['contact_id'])->first(); + $path = app(InvestmentGeography::class)->path($values['primary_territory_id'] ?? null, true); + if (! empty($values['primary_territory_id']) && $path === []) { + throw ValidationException::withMessages(['province_id' => 'Reconcilie a localização antes da decisão.']); + } + if ($path !== []) { + app(InvestmentGeography::class)->selection(array_combine(array_slice(array_keys(InvestmentGeography::FIELDS), 0, count($path)), array_column($path, 'id')), true); + } + $payload = ['title' => $values['title'], 'description' => $values['description'], 'opportunity_category' => $values['opportunity_category'], + 'owner_user_id' => $record->owner_user_id, 'organization_id' => $record->organization_id, 'private_offer' => $offer, + 'contact_fingerprint' => hash('sha256', json_encode($contact, JSON_THROW_ON_ERROR)), 'territory' => $path]; + $normalize = function (mixed $value) use (&$normalize): mixed { + if (is_object($value)) { + $value = get_object_vars($value); + } + if (! is_array($value)) { + return $value; + } + if (! array_is_list($value)) { + ksort($value); + } + + return array_map($normalize, $value); + }; + $payload = $normalize($payload); + + return ['payload' => $payload, 'hash' => hash('sha256', json_encode(['contract' => 'private-offer-editorial-v2', 'payload' => $payload], JSON_THROW_ON_ERROR)), + 'record_version' => $record->management_version, 'contract_version' => 'private-offer-editorial-v2', 'resources' => ['documents' => [], 'images' => [], 'locations' => []]]; + } + /** @return array */ public function presentation(User $actor, Opportunity $record): array { app(PortalProfileAccess::class)->authorize($actor, 'view', $record); + $values = app(OpportunityManagement::class)->values($record); + $fresh = null; + $contactId = $values['private_offer']['contact_id'] ?? null; + $contactLabel = $contactId && app(OpportunityContacts::class)->related($record, $contactId) + ? DB::table('contacts')->where('id', $contactId)->value('full_name') : null; + if (isset($record->workflow_context['approval'])) { + try { + $fresh = hash_equals($record->workflow_context['approval']['hash'], $this->snapshot($record)['hash']); + } catch (ValidationException $error) { + $fresh = false; + } + } + $history = $record->workflow_context['events'] ?? []; + $names = User::whereIn('id', array_filter([...array_column($history, 'actor_id'), ...array_column($history, 'assignee_user_id')]))->pluck('name', 'id'); + $history = array_map(fn ($event) => [...$event, 'actor_name' => $names[$event['actor_id']] ?? 'Interveniente indisponível', + 'assignee_name' => isset($event['assignee_user_id']) ? ($names[$event['assignee_user_id']] ?? 'Interveniente indisponível') : null], $history); + $versions = [...($record->workflow_context['decision_history'] ?? []), ...($record->workflow_context['submission_history'] ?? [])]; + if (isset($record->workflow_context['submission'])) { + $versions[] = ['submission' => $record->workflow_context['submission'], 'approval' => $record->workflow_context['approval'] ?? null, + 'rejection' => $record->workflow_context['rejection'] ?? null]; + } + $uniqueVersions = []; + foreach ($versions as $version) { + if (! isset($version['submission']['payload']['private_offer'])) { + continue; + } + $key = $version['submission']['record_version'].':'.$version['submission']['hash'].':'.($version['submission']['review_cycle'] ?? 0); + $previous = $uniqueVersions[$key] ?? []; + $uniqueVersions[$key] = ['submission' => $version['submission'], 'approval' => $version['approval'] ?? $previous['approval'] ?? null, + 'rejection' => $version['rejection'] ?? $previous['rejection'] ?? null]; + } + $workflow = app(OpportunityProfileWorkflow::class); + $actions = $workflow->actions($actor, $record); - return ['id' => $record->id, 'reference' => $record->reference, 'values' => app(OpportunityManagement::class)->values($record), - 'etag' => app(OpportunityManagement::class)->etag($record), 'state' => data_get($record->workflow_context, 'private_offer_preparation.state'), + return ['id' => $record->id, 'reference' => $record->reference, 'values' => $values, + 'etag' => app(OpportunityManagement::class)->etag($record), 'state' => app(PortalProfileAccess::class)->state($record), 'canEdit' => app(PortalProfileAccess::class)->canEdit($actor, $record), 'organization_id' => $record->organization_id, - 'territory' => app(InvestmentGeography::class)->path($record->primary_territory_id, true)]; + 'actions' => $actions, + 'reviewers' => in_array('assign_review', $actions, true) ? $workflow->participants($actor, $record)['validators'] : [], + 'reauthenticationRequired' => app(OpportunityProfileWorkflow::class)->reauthenticationRequired($actor, $record), + 'history' => $history, 'submittedVersions' => array_values($uniqueVersions), 'correctionItems' => $record->workflow_context['correction_items'] ?? [], + 'approval' => $record->workflow_context['approval'] ?? null, 'approvalFresh' => $fresh, 'contactLabel' => $contactLabel, 'tasks' => app(OpportunityWorkflowTasks::class)->listing($record), + 'territory' => app(InvestmentGeography::class)->path($values['primary_territory_id'] ?? null, true)]; } } diff --git a/docs/investmoz/unification/traceability-and-manual.md b/docs/investmoz/unification/traceability-and-manual.md index eff519c3..e5d56d52 100644 --- a/docs/investmoz/unification/traceability-and-manual.md +++ b/docs/investmoz/unification/traceability-and-manual.md @@ -10152,4 +10152,15 @@ Verificação: 14 testes novos. Bateria de regressão de Oportunidades/perfis/ed Limitações: conferência visual manual não efectuada. npm run check global acusa formatação preexistente em 61 ficheiros alheios ao delta; esses ficheiros não foram alterados e a falha não foi dispensada. A bateria canónica extensa foi interrompida, não contabilizada como aprovada e substituída pela verificação do cenário afectado. Uma prova antiga de divulgação exige outra base pelo seu próprio controlo de segurança; esse controlo foi preservado, sem tocar nessa base, e a compatibilidade do âmbito nativo foi testada na nova preparação sintética. -Saldo de RF-MN-003: Fase 1 de Ofertas implementada e verificada tecnicamente em QA, sem instalação. Não encerra o requisito integral nem Procuras. Fase 2 e seguintes permanecem por autorizar; NDA, comprovativos, fotografias, validade, moderação e publicação não são simulados nesta entrega. A autorização anterior de validador jurídico para legislação não é estendida a Ofertas. Instalação em homologação ou produção exige verificação e autorização expressa separada. \ No newline at end of file +Saldo de RF-MN-003: Fase 1 de Ofertas implementada e verificada tecnicamente em QA, sem instalação. Não encerra o requisito integral nem Procuras. Fase 2 e seguintes permanecem por autorizar; NDA, comprovativos, fotografias, validade, moderação e publicação não são simulados nesta entrega. A autorização anterior de validador jurídico para legislação não é estendida a Ofertas. Instalação em homologação ou produção exige verificação e autorização expressa separada. +## 14/11/2026 — Ofertas Fase 2, circuito editorial privado em QA + +Autorização expressa da Tempo, após encerramento técnico da F1 com ressalva visual. Implementação limitada ao plano 6e302db6, mantendo Oportunidades como cadastro principal. Dashboard existente → Ofertas/revisão privada → validador inicia ou coordenação atribui → pede correcção/rejeita com fundamento → promotor corrige/reenvia no mesmo identificador → aprovação interna independente. Preserva versões, rejeições, aprovação histórica e tarefas. Reatribuição guarda quem alterou, anterior e novo responsável, sem conceder papéis. + +Snapshots/hash, revisão e ciclo vinculam cada decisão aos campos e referências conferidos. Mudança no contacto/localização bloqueia decisão ou assinala reconferência; não substitui a conclusão. Primeira revisão explícita de submissão F1 preserva autor/data/revisão e regista a vinculação actual sem inventar fingerprints históricos do CRM. Estados editoriais privados não tornam a oferta pública; status de publicação mantém-se privado, marca/exclusão pública e percursos alternativos bloqueados. Os percursos nativos conservam a versão pública anterior durante alterações pendentes. Desactivar o circuito não permite submeter v2 pelo fallback v1 nem apagar histórico. + +Reutiliza perfis/MFA/independência, workflow, tarefas, auditoria e caixa de notificações. Email pelo transporte existente, depois do commit e com revalidação, activação/lista operacional nativas; em QA apenas memória, sem mensagens reais ou SMS. Configurações de entrega e designações operacionais não são inventadas. Sem migrações/tabelas novas, mudanças em mne, dados operacionais, menu principal ou instalação. + +QA sintética offers_phase1_20261114_test:55441: regressão 35/499, final F2 11/160, complemento de contribuidores 1/14 e HTML final 1/10 aprovados; grupos sobrepostos, não somar. Pint, PHPStan, tipos, lint/formatação focados e build QA aprovados. Testa autoria/contribuidores/âmbito/MFA/revogação, snapshots, rejeição/correcção/reenvio, reatribuição, fila sem duplicação, rollback, notificações idempotentes, fonte alterada, recuperação e recusa de publicação. Conferência visual mantém a ressalva da ferramenta; formatação global continua nos 61 ficheiros preexistentes exteriores ao delta, sem dispensa. Entregabilidade real não demonstrada. Relatório: docs/investmoz/v2/auditoria-portal/ofertas-fase2-entrega-qa-20261114.md. Recibo: outputs/offers-private-phase2-20261114/qa-result.json. + +Saldo RF-MN-003: F1/F2 implementadas e verificadas tecnicamente em QA, não instaladas; requisito integral/Procuras permanecem parciais. Fase 3 e instalação em homologação/produção dependem de autorização expressa própria. Não foram iniciados catálogo público, NDA, comprovativos, fotografias ou validade. Próximo passo é rever esta entrega e conferir visualmente quando o ambiente permitir, sem reabrir provas técnicas aceites. \ No newline at end of file diff --git a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv index b9d13f7b..8dc782d9 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,"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.","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.",Parcial - Ofertas Fase 1 implementada e verificada tecnicamente em QA; nao instalada,"Ofertas: Fase 2 e seguintes nao autorizadas nem implementadas neste delta; moderacao, publicacao, NDA/comprovativos, fotografias e validade continuam separados. Procuras nao abrangidas. Conferencia visual por efectuar. Check global de formatacao acusa 61 ficheiros preexistentes fora do delta; verificacao focada aprovada.","Autorizacao expressa para Fase 2 e, separadamente, instalacao; decisoes restantes da Fase 0 aplicam-se apenas as fases dependentes.","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 - formulario e lista privados; HTML compilado real conferido; visual manual pendente,QA - rotas autenticadas /minhas-oportunidades/ofertas; autorizacao no servidor; sem rota publica nova,"QA - oportunidades, management_draft, workflow_context e auditoria existentes; sem novo cadastro",QA - recuperacao por identidade existente e controlo de concorrencia por etag,"QA - rascunho ou submissao privada; sem aprovacao, publicacao ou exposicao publica",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,"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.","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.",Parcial - Ofertas Fases 1 e 2 implementadas e verificadas tecnicamente em QA; nao instaladas,"Fase 3 e restantes fases nao autorizadas: catalogo/publicacao de Ofertas, NDA, comprovativos, fotografias, validade e restantes ramos. Procuras nao abrangidas. Conferencia visual pendente. Check global de formatacao: 61 ficheiros preexistentes fora do delta, falha nao dispensada. Email operacional/entregabilidade nao activados.",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",QA - aprovacao interna privada distinta de publicacao; versao publica nativa anterior preservada,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-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-fase2-entrega-qa-20261114.md b/docs/investmoz/v2/auditoria-portal/ofertas-fase2-entrega-qa-20261114.md new file mode 100644 index 00000000..e37ad23c --- /dev/null +++ b/docs/investmoz/v2/auditoria-portal/ofertas-fase2-entrega-qa-20261114.md @@ -0,0 +1,75 @@ +# Ofertas — Fase 2, circuito editorial privado em QA + +14/11/2026. Autorização expressa da Tempo nesta sessão, limitada à Fase 2 do plano `6e302db6`. Base: Fase 1 `1f2990e4`, mantida no histórico Git. Referência: RF-MN-003 da matriz existente. + +**Implementado e verificado tecnicamente em QA. Sem instalação em homologação ou produção. A aprovação é interna; a publicação de Ofertas permanece bloqueada. Fase 3 não iniciada.** + +## Percurso que ficou utilizável + +No Dashboard Colaborativo existente, a área de Oportunidades inclui a entrada «Ofertas — revisão privada». Abre a mesma lista privada da Fase 1, agora com filtros por estado. Não há outro dashboard, outra tabela de ofertas, outro login ou alteração ao menu principal. As contagens de Oportunidades continuam a contar cada registo uma vez; a entrada filtrada não acrescenta outro contador ao total. + +O promotor cria/guarda/recupera/submete. Um validador autorizado inicia a revisão, pede correcções nos campos comuns ou rejeita com fundamento. O promotor prepara a revisão após rejeição ou aprovação, conserva a decisão anterior, corrige e reenvia no mesmo identificador. O validador independente aprova a nova versão em privado. A coordenação de validação existente pode atribuir/reassociar um revisor elegível, com fundamento, histórico do responsável anterior e do novo responsável; não são criadas designações ou atribuições por inferência. + +Estados: rascunho → submetida → em revisão → correcções solicitadas/rejeitada/aprovada internamente. Correcção e reenvio abrem novo ciclo; não eliminam rejeições, aprovações, snapshots ou tarefas encerradas. Aprovação não publica nem certifica titularidade, licenciamento ou viabilidade do negócio. + +## Integração e garantias + +- O mesmo `OpportunityProfileWorkflow` executa as transições. O adaptador privado valida os campos comerciais comuns; não exige nem fabrica os perfis normativos completos e não enfraquece o contrato dos investimentos existentes. +- Oportunidades continua a ser o cadastro principal. `management_draft`, versões, etag, workflow_context, tarefas CRM, auditoria e notificações existentes são reutilizados. Não há novas migrações, tabelas ou mudanças em `mne`. +- O estado editorial da Oferta é projectado a partir de uma única origem privada e sincronizado com as filas. O estado de publicação conserva a barreira privada: `status=draft`, `published_at=null` e a marca de preparação permanece. Nenhuma operação F2 remove a exclusão do catálogo. +- Cada submissão regista versão do contrato, snapshot, hash determinístico, versão do registo submetido e ciclo de revisão. Cada decisão conserva actor, instante UTC, fundamento, hash e revisão a que respeita. O snapshot da decisão também fica cifrado na auditoria existente. +- Contacto e localização são referenciados pelos cadastros existentes. O hash inclui os dados efectivamente conferidos nas referências; uma mudança posterior impede a aprovação dessa submissão ou sinaliza reconferência se a aprovação já ocorreu. A leitura não substitui silenciosamente a decisão. +- Ofertas antigas da Fase 1 só são vinculadas na primeira operação explícita de revisão. Conservam autor, data e revisão originais. A vinculação regista quem/quando estabeleceu o fingerprint: a Fase 1 não tinha esse fingerprint de referências, pelo que não se inventa um estado histórico do CRM na data original. +- As permissões são verificadas no servidor, na transacção e nos destinos das notificações. Autor, contribuidores e representantes em conflito não aprovam; acumulação de papéis não remove os bloqueios. Revisão/decisões internas exigem MFA recente. O papel de auditor permanece sem escrita operacional. +- Tentativas de publicação são recusadas na rota privada, nas rotas alternativas nativas e no workflow chamado directamente. Os bloqueios anteriores do modelo e da divulgação e a exclusão da pesquisa pública permanecem. Não se implementou um publicador para Ofertas. +- A versão pública anterior dos percursos nativos mantém-se durante edições pendentes; o ramo nativo não foi convertido para a preparação comercial. A regressão dos registos publicados confirma a preservação dos valores públicos. Não existem Ofertas públicas aceites nesta fase para simular uma publicação nova. + +## Notificações e recuperação + +A caixa editorial existente recebe as ocorrências por oportunidade/versão/acção e destinatário. A restrição de unicidade existente evita ocorrências repetidas. Submissões notificam coordenação elegível já designada; sem designação, a fila fica por atribuir. Decisões notificam autor e responsáveis efectivos, e a reatribuição inclui o responsável anterior se continuar autorizado. As ligações abrem a Oferta privada correcta e revalidam o acesso. + +O novo job é apenas o consumidor do transporte de email/fila já existente: usa a activação e a lista de destinatários operacionais actuais, consulta a notificação novamente, reconfirma acesso e não envia conteúdo da Oferta. É agendado após a confirmação da transacção. Falhas imediatas de despacho/entrega ficam auditadas sem converter a decisão já confirmada numa resposta incerta. O job usa as tentativas/backoff do padrão existente. Não existe transporte SMS novo, envio real em QA ou promessa de entrega exactamente uma vez. + +Se o circuito editorial ficar indisponível, uma Oferta v2 não é submetida pelo fallback v1: responde com indisponibilidade e conserva versão e histórico. Ofertas genuinamente v1 continuam com a preparação anterior. Não se faz rollback destrutivo de dados para regressar ao código precedente. + +## Ficheiros do delta + +| Ficheiros | Finalidade | +| --- | --- | +| `app/Services/PrivateOfferPreparation.php` | Adaptador, snapshots, vinculação F1, apresentação e histórico privado | +| `app/Services/OpportunityProfileWorkflow.php` | Revisão, atribuição, decisões por versão e ciclos no motor existente | +| `app/Services/PortalProfileAccess.php` | Projecção do estado e edição nos estados permitidos | +| `app/Services/OpportunityManagement.php` | Guardar a correcção privada sem perder categoria/âmbito/localização actuais | +| `app/Services/PortalEditorialWorkflow.php` | Destinatários, unicidade, destino privado e agendamento de notificações | +| `app/Jobs/NotifyPrivateOfferDecision.php` | Email pelo transporte existente, com revalidação de acesso | +| `app/Http/Controllers/PrivateOfferPreparationController.php` | Filtros e endpoint privado de decisões | +| `app/Http/Controllers/PortalEditorialWorkflowController.php` | Entrada de Ofertas na área existente de Oportunidades | +| `routes/web.php` | Endpoint privado de revisão, sob autenticação/perfis/resposta privada | +| `resources/js/components/offer-editorial-review.tsx` | Fundamento, campos a corrigir, responsável, decisões e histórico | +| `resources/js/lib/offer-editorial.ts` | Tipos e rótulos partilhados, sem importar uma página como biblioteca | +| `resources/js/pages/offers/preparation.tsx` e `preparation-index.tsx` | Revisão, recuperação e filtros na preparação existente | +| `resources/js/pages/management/editorial-workflow.tsx` | Entrada integrada, sem duplicar contagens | +| `tests/Feature/PrivateOfferEditorialTest.php` | 11 testes novos do circuito e dos bloqueios | +| Matriz, manual, este relatório e recibo QA | Saldo e evidência da entrega, sem outro inventário funcional | + +## Verificação + +Base sintética isolada: `offers_phase1_20261114_test:55441`, com as guardas existentes. Não foram utilizados processos reais, contas operacionais ou notificações externas. + +- Integração e regressão: **35 testes / 499 asserções aprovados**, incluindo Fase 1, permissões, gravação/publicação nativa e o cenário canónico/editorial afectado. +- Passagem final F2: **11 testes / 160 asserções aprovados**. O teste de conflito de contribuidores foi ainda reforçado e verificado separadamente: **1 teste / 14 asserções aprovado**. +- HTML completo das duas páginas privadas com o build final: **1 teste / 10 asserções aprovado**. +- Pint, PHPStan, tipos, lint/formatação focados e compilação QA aprovados. A análise PHP foi repetida para o motor após o ajuste final do ramo privado. Sem enfraquecer testes, baseline ou guardas. +- Os grupos sobrepõem-se; não somar os totais. Execuções interrompidas ou anteriores com falhas não são contadas como aprovadas. + +Os testes cobrem fila existente sem contagem duplicada, rejeição/correcção/reenvio, aprovação vinculada ao snapshot, decisão desactualizada, âmbito/MFA/contribuidores/auto-aprovação, perda de acesso, contacto alterado, reatribuição restrita, rollback conjunto, notificação repetida, email no transporte local em memória, revogação antes da entrega, vinculação explícita F1, indisponibilidade sem downgrade e recusa de publicação por percursos alternativos. O histórico recuperado conserva o título/preço e a decisão de cada versão. + +## Riscos e pendências + +1. Conferência visual manual continua pendente: a última prova da ferramenta registou host de navegador indisponível e falha de captura nativa GPU `0x887A0005`. Não foram repetidos reinícios ou desactivadas verificações. HTML e compilação não são apresentados como aceite visual. +2. A verificação global de formatação continua a falhar nos 61 ficheiros preexistentes. A lista foi novamente confrontada com este delta, sem intersecção. Não foi reformatado conteúdo exterior ao âmbito nem dispensada a falha global. +3. Email real, configuração operacional da fila, destinatários e entregabilidade não foram activados nem demonstrados. Os testes utilizam transporte em memória e os papéis sintéticos existentes no cenário. +4. Atribuição/reassociação exige coordenação já designada e candidatos elegíveis. A inexistência de uma designação não autoriza criar papéis; bloqueia apenas essa atribuição e mantém a fila por tratar. +5. O recorte aprovado é moderação dos campos comerciais comuns. NDA, comprovativos, fotografias, validade, catálogo público, destaque e restantes fases não são declarados concluídos. Não se atribui a Ofertas a designação jurídica da legislação. + +**Saldo:** Fase 2 implementada e verificada tecnicamente em QA; não instalada. O módulo integral/RF-MN-003 e Procuras continuam parciais. A Fase 3 e qualquer instalação exigem autorização expressa separada. Próximo passo: revisão desta entrega e conferência visual quando o ambiente permitir, sem reabrir as provas técnicas aceites. diff --git a/outputs/offers-private-phase2-20261114/qa-result.json b/outputs/offers-private-phase2-20261114/qa-result.json new file mode 100644 index 00000000..744836cd --- /dev/null +++ b/outputs/offers-private-phase2-20261114/qa-result.json @@ -0,0 +1,212 @@ +{ + "date": "2026-11-14", + "scope": "Ofertas - Fase 2 - Circuito Editorial privado", + "base_revision": "6e302db62e7c3b9f62ac417b8591dae4ff7e0b2c", + "phase1_revision": "1f2990e4", + "approved_plan_revision": "6e302db6", + "state": "implemented_and_technically_verified_in_qa_not_installed", + "qa_database": "offers_phase1_20261114_test:55441", + "new_tests": 11, + "results": { + "tests-final.log": { + "tool": "phpunit", + "result": "passed", + "tests": 35, + "passed": 35, + "assertions": 499, + "duration_ms": 207928 + }, + "tests-focused-final.log": { + "tool": "phpunit", + "result": "passed", + "tests": 11, + "passed": 11, + "assertions": 160, + "duration_ms": 119998 + }, + "contributor-final.log": { + "tool": "phpunit", + "result": "passed", + "tests": 1, + "passed": 1, + "assertions": 14, + "duration_ms": 15395 + }, + "html-final.log": { + "tool": "phpunit", + "result": "passed", + "tests": 1, + "passed": 1, + "assertions": 10, + "duration_ms": 9396 + } + }, + "runs_overlap": true, + "checks": { + "pint": "passed", + "phpstan_full": "passed", + "phpstan_final_workflow": "passed", + "typescript": "passed", + "focused_frontend_lint_format": "passed", + "qa_build": "passed", + "real_compiled_html": "passed", + "git_diff_check": "passed", + "global_npm_check": "failed: 61 unchanged pre-existing formatting files outside the delta; not waived" + }, + "global_format_files": [ + "resources/js/components/funding-classification.tsx", + "resources/js/components/institutional-source/source-text.tsx", + "resources/js/components/password-input.tsx", + "resources/js/components/sector-entry.tsx", + "resources/js/components/sector-resource-reference.tsx", + "resources/js/components/simulation-form.tsx", + "resources/js/components/simulation-irpc-annual.tsx", + "resources/js/components/structure-geography.tsx", + "resources/js/components/tempo-header-tools.tsx", + "resources/js/lib/investor-path-stage.ts", + "resources/js/lib/irpc-annual-settlement.ts", + "resources/js/pages/funding/assisted.tsx", + "resources/js/pages/funding/manage.tsx", + "resources/js/pages/funding/saved.tsx", + "resources/js/pages/investment/catalogue.tsx", + "resources/js/pages/investment/detail.tsx", + "resources/js/pages/investor-path/index.tsx", + "resources/js/pages/investor-path/preparation.tsx", + "resources/js/pages/investor-path/show.tsx", + "resources/js/pages/management/demography-install.tsx", + "resources/js/pages/portal/fiscal-forms.tsx", + "resources/js/pages/portal/search.tsx", + "resources/js/translation/page-translator.tsx", + "resources/js/translation/service.ts", + "resources/js/translation/types.ts", + "resources/js/translation/utils.ts", + "tests/frontend/cluster-editorial.test.mjs", + "tests/frontend/dashboard-navigation-sections.test.mjs", + "tests/frontend/editorial-completion.test.mjs", + "tests/frontend/editorial-dashboard-manifest.test.mjs", + "tests/frontend/editorial-dashboard-presentation.test.mjs", + "tests/frontend/editorial-edition.test.mjs", + "tests/frontend/education-review.test.mjs", + "tests/frontend/fiscal-form-entry.test.mjs", + "tests/frontend/funding-directory.test.mjs", + "tests/frontend/global-search.test.mjs", + "tests/frontend/helpers/irpc-advance-period-case.mjs", + "tests/frontend/helpers/irpc-annual-noncivil-case.mjs", + "tests/frontend/helpers/irpc-investment-combination-case.mjs", + "tests/frontend/investment-map-indicators.test.mjs", + "tests/frontend/investment-preparation.test.mjs", + "tests/frontend/irpc-advance-cashflow-ui.test.mjs", + "tests/frontend/irpc-advance-cashflow.test.mjs", + "tests/frontend/irpc-advance-period-ui.test.mjs", + "tests/frontend/irpc-advance-period.test.mjs", + "tests/frontend/irpc-annual-noncivil-ui.test.mjs", + "tests/frontend/irpc-annual-noncivil.test.mjs", + "tests/frontend/irpc-investment-carry-combination.test.mjs", + "tests/frontend/irpc-pec-correction.test.mjs", + "tests/frontend/irpc-pec-last-amount.test.mjs", + "tests/frontend/irpc-pec-obligation-ui.test.mjs", + "tests/frontend/irpc-pec-obligation.test.mjs", + "tests/frontend/irpc-ppc-obligation-ui.test.mjs", + "tests/frontend/licensing-stage.test.mjs", + "tests/frontend/navigation-sidebar-render.test.mjs", + "tests/frontend/opportunity-curation.test.mjs", + "tests/frontend/opportunity-information.test.mjs", + "tests/frontend/portal-institutional-contact.test.mjs", + "tests/frontend/portal-navigation.test.mjs", + "tests/frontend/portal-six-group-menu.test.mjs", + "tests/frontend/sector-resource-reference.test.mjs" + ], + "format_intersection_with_delta": [], + "capabilities": [ + "Existing dashboard and opportunity workflow", + "Scoped review and coordinator reassignment", + "Reasoned rejection and correction/resubmission", + "Exact snapshot/hash/version/cycle decisions", + "Private approval with all publication barriers preserved", + "Audit and immutable submission history", + "Scoped inbox and existing mail transport after commit", + "Rollback and no downgrade to phase one" + ], + "new_migrations": [], + "new_tables": [], + "production_data_modified": false, + "mne_modified": false, + "main_menu_modified": false, + "homologation_installed": false, + "production_installed": false, + "phase3_started": false, + "real_messages_sent": false, + "limits": [ + "Visual review pending: previously recorded browser-host and native GPU capture failures; no restart or security changes.", + "Global formatting failure remains.", + "Real email delivery and operational queue configuration not activated or tested.", + "Initial phase-one reference fingerprints were not recorded; explicit review binding records current fingerprint without backdating.", + "No exactly-once guarantee for an external email provider.", + "Reassignment requires an existing scoped validation coordinator; no permissions created." + ], + "files": [ + "app/Http/Controllers/PortalEditorialWorkflowController.php", + "app/Http/Controllers/PrivateOfferPreparationController.php", + "app/Services/OpportunityManagement.php", + "app/Services/OpportunityProfileWorkflow.php", + "app/Services/PortalEditorialWorkflow.php", + "app/Services/PortalProfileAccess.php", + "app/Services/PrivateOfferPreparation.php", + "docs/investmoz/unification/traceability-and-manual.md", + "docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv", + "resources/js/pages/management/editorial-workflow.tsx", + "resources/js/pages/offers/preparation-index.tsx", + "resources/js/pages/offers/preparation.tsx", + "routes/web.php", + "app/Jobs/NotifyPrivateOfferDecision.php", + "docs/investmoz/v2/auditoria-portal/ofertas-fase2-entrega-qa-20261114.md", + "resources/js/components/offer-editorial-review.tsx", + "resources/js/lib/offer-editorial.ts", + "tests/Feature/PrivateOfferEditorialTest.php" + ], + "source_sha256": { + "app/Http/Controllers/PortalEditorialWorkflowController.php": "9cb4be9a15f8d332376006b5bb9f6160461391e4ab28bce3b6f8512bb606e91c", + "app/Http/Controllers/PrivateOfferPreparationController.php": "d9d3cbb6e5b8ca42f508f836029021c5cc2feda38661a40e446b2057cfa36cc4", + "app/Services/OpportunityManagement.php": "4a837c99e7f029fbaa5e0421c5321dc8c113c70c9afc0c012497b2f313519864", + "app/Services/OpportunityProfileWorkflow.php": "89253fce194476516d3604e110f6315cf32e18c8cc1b1a1aaf00987a51c08995", + "app/Services/PortalEditorialWorkflow.php": "9d3a560ab01cb04a84386f1e7f28564c67199ed2bf2bb5ae518e2b37d854baf6", + "app/Services/PortalProfileAccess.php": "835eb53972391f4b790a2b0773b771bc0206f52dbf62952521885400dc2d44b7", + "app/Services/PrivateOfferPreparation.php": "ce6756986351a469286377e27af14e55a93fe9981e705625ab8fe646deff6f20", + "docs/investmoz/unification/traceability-and-manual.md": "3ddb72a1bad22bf776f4540d4d47d08e270524e248b293d800e3726679805d9e", + "docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv": "ffcb117e0b3d3ff54fa0ba77787c64989dcc839590726b4f901ae04429d6ae7a", + "resources/js/pages/management/editorial-workflow.tsx": "8d21d2f5d3453c26945a829423b1369d58999ee35714ca90c337f155a23beee0", + "resources/js/pages/offers/preparation-index.tsx": "6f2ca4f41e2b6d9579af353a4d96dc4f001be187c628ef611a605966be2c98d5", + "resources/js/pages/offers/preparation.tsx": "99abe86f7233741eff43836f3c07fca333212c81dc1f4a14cd8f2e3f7609691a", + "routes/web.php": "ece5b61bafff593da234838abdfddda08be161e111bad4331dd95d5c28aad53d", + "app/Jobs/NotifyPrivateOfferDecision.php": "15aa3ac867e9f6ca2569eff0ad3fc483dc41b3ca200efefae167d179c24888b5", + "docs/investmoz/v2/auditoria-portal/ofertas-fase2-entrega-qa-20261114.md": "a8d95ca260ac6be6e60284bf99584cbf72df9b69eed4ffa8233b3c7bc956e5cd", + "resources/js/components/offer-editorial-review.tsx": "c519fb77848d8760ea88556184201fbaef27bbf7f9b00adefcff04de191ba81e", + "resources/js/lib/offer-editorial.ts": "dc70169aaa746b9ae8063b0312a6601d0347d2927a7c0f4bfc58cfc1f31aae99", + "tests/Feature/PrivateOfferEditorialTest.php": "1fed1ca2c5241e886f76d37f7d5ac4410952eb4361fbfe03b4f76c009abd4b6d" + }, + "report": "docs/investmoz/v2/auditoria-portal/ofertas-fase2-entrega-qa-20261114.md", + "matrix": { + "id": "RF-MN-003", + "rows_changed": 1, + "columns_preserved": 49 + }, + "test_commands": [ + "php vendor/bin/phpunit --filter 'PrivateOfferEditorialTest|PrivateOfferPreparationTest|OpportunityManagementTest|PortalProfileAccessTest|test_phase_one_requires_a_new_legal_decision_for_each_content_and_attachment_version|test_notice_receipt_never_grants_access_and_revocation_blocks_publication' tests/Feature/PrivateOfferEditorialTest.php tests/Feature/PrivateOfferPreparationTest.php tests/Feature/OpportunityManagementTest.php tests/Feature/PortalProfileAccessTest.php tests/Feature/OpportunityCanonicalWorkflowTest.php tests/Feature/PortalEditorialWorkflowTest.php", + "php vendor/bin/phpunit tests/Feature/PrivateOfferEditorialTest.php", + "php vendor/bin/phpunit --filter test_author_contributor_wrong_scope_auditor_and_unassigned_reviewer_cannot_approve tests/Feature/PrivateOfferEditorialTest.php", + "php vendor/bin/phpunit --filter test_full_html_uses_real_compiled_private_page_entries tests/Feature/PrivateOfferPreparationTest.php" + ], + "test_environment": { + "APP_ENV": "testing", + "DB_HOST": "127.0.0.1", + "DB_PORT": "55441", + "DB_DATABASE": "offers_phase1_20261114_test", + "DB_USERNAME": "demography_qa", + "EDITORIAL_QA_BUILD_DIRECTORY": "build-qa-opportunities", + "synthetic_app_key": true, + "mail": "array", + "queue": "sync" + }, + "build_command": "INVESTMOZ_QA_BUILD=1 npm.cmd run build", + "failed_or_interrupted_development_runs_not_counted": true +} diff --git a/resources/js/components/offer-editorial-review.tsx b/resources/js/components/offer-editorial-review.tsx new file mode 100644 index 00000000..adbbd62f --- /dev/null +++ b/resources/js/components/offer-editorial-review.tsx @@ -0,0 +1,259 @@ +import { useState } from 'react'; +import { router } from '@inertiajs/react'; +import { + offerStates, + offerActions, + type OfferEditorialRecord, +} from '@/lib/offer-editorial'; +import { profileJson } from '@/lib/profile-json'; + +const fields: Record = { + title: 'Título', + description: 'Descrição', + opportunity_category: 'Categoria', + private_offer: 'Preço', + contact: 'Contacto', + location: 'Localização', +}; + +export default function OfferEditorialReview({ + record, + onChanged, +}: { + record: T; + onChanged: (record: T) => void; +}) { + const [reason, setReason] = useState(''); + const [corrections, setCorrections] = useState([]); + const [busy, setBusy] = useState(false); + const [message, setMessage] = useState(''); + const [assignee, setAssignee] = useState(''); + const actions = record.actions.filter((action) => action !== 'submit'); + async function decide(action: string) { + if (busy || record.reauthenticationRequired) return; + setBusy(true); + setMessage(''); + try { + const result = await profileJson( + `/minhas-oportunidades/ofertas/${record.id}/revisao`, + { + etag: record.etag, + action, + reason, + ...(action === 'assign_review' + ? { assignee_user_id: Number(assignee) } + : {}), + ...(action === 'request_correction' + ? { correction_items: corrections } + : {}), + }, + ); + onChanged(result.record); + setReason(''); + setCorrections([]); + setMessage(result.message); + if (action === 'request_revision') + router.visit(`/minhas-oportunidades/ofertas/${record.id}`, { + preserveScroll: true, + }); + } catch (error) { + setMessage( + error instanceof Error + ? error.message + : 'Não foi possível registar a decisão. O fundamento foi preservado.', + ); + } finally { + setBusy(false); + } + } + return ( +
+

Revisão da oferta

+

+ {offerStates[record.state] ?? record.state}. A publicação + pública permanece indisponível nesta fase. +

+ {record.approvalFresh === false && ( +

+ As referências mudaram desde a aprovação. Prepare nova + revisão; a decisão anterior fica no histórico. +

+ )} + {record.correctionItems.length > 0 && + record.state === 'needs_information' && ( +

+ Campos a rever:{' '} + {record.correctionItems + .map((field) => fields[field] ?? field) + .join(', ')} + . +

+ )} + {record.reauthenticationRequired && ( +

+ Renove o MFA pela autenticação habitual antes de registar + uma decisão. Os dados guardados permanecem disponíveis. +

+ )} + {actions.length > 0 && ( +
+