From e431de8b423a4b64b9875ca7a8627e3e4b3c9c35 Mon Sep 17 00:00:00 2001 From: nala Date: Sat, 14 Nov 2026 17:28:12 +0200 Subject: [PATCH] feat(offers): implement phase four private details and confidentiality in QA --- .../PrivateOfferPreparationController.php | 73 +++- app/Services/OpportunityLibrarySupport.php | 1 + app/Services/OpportunityResources.php | 19 +- app/Services/PrivateOfferConfidentiality.php | 216 +++++++++++ app/Services/PrivateOfferDetails.php | 83 +++++ app/Services/PrivateOfferPreparation.php | 22 +- app/Services/VerifiedDocumentBinary.php | 15 +- config/private_offers.php | 6 + .../unification/traceability-and-manual.md | 12 + .../v2/auditoria-portal/matriz-requisitos.csv | 2 +- .../ofertas-fase4-entrega-qa-20261114.md | 88 +++++ .../offers-private-phase4-20261114/final.log | 1 + .../offers-private-phase4-20261114/final.xml | 24 ++ .../focused.log | 1 + .../focused.xml | 25 ++ .../html-latest.log | 1 + .../html-latest.xml | 14 + .../qa-result.json | 184 +++++++++ .../regression.xml | 333 +++++++++++++++++ .../withdrawal-latest.log | 1 + .../withdrawal-latest.xml | 6 + .../components/offer-confidential-access.tsx | 279 ++++++++++++++ resources/js/components/offer-particulars.tsx | 193 ++++++++++ .../js/components/offer-private-resources.tsx | 332 +++++++++++++++++ resources/js/pages/offers/confidentiality.tsx | 11 + resources/js/pages/offers/preparation.tsx | 46 ++- routes/web.php | 9 + .../PrivateOfferConfidentialityTest.php | 348 ++++++++++++++++++ tests/Support/OpportunitySearchFixture.php | 23 +- 29 files changed, 2356 insertions(+), 12 deletions(-) create mode 100644 app/Services/PrivateOfferConfidentiality.php create mode 100644 app/Services/PrivateOfferDetails.php create mode 100644 config/private_offers.php create mode 100644 docs/investmoz/v2/auditoria-portal/ofertas-fase4-entrega-qa-20261114.md create mode 100644 outputs/offers-private-phase4-20261114/final.log create mode 100644 outputs/offers-private-phase4-20261114/final.xml create mode 100644 outputs/offers-private-phase4-20261114/focused.log create mode 100644 outputs/offers-private-phase4-20261114/focused.xml create mode 100644 outputs/offers-private-phase4-20261114/html-latest.log create mode 100644 outputs/offers-private-phase4-20261114/html-latest.xml create mode 100644 outputs/offers-private-phase4-20261114/qa-result.json create mode 100644 outputs/offers-private-phase4-20261114/regression.xml create mode 100644 outputs/offers-private-phase4-20261114/withdrawal-latest.log create mode 100644 outputs/offers-private-phase4-20261114/withdrawal-latest.xml create mode 100644 resources/js/components/offer-confidential-access.tsx create mode 100644 resources/js/components/offer-particulars.tsx create mode 100644 resources/js/components/offer-private-resources.tsx create mode 100644 resources/js/pages/offers/confidentiality.tsx create mode 100644 tests/Feature/PrivateOfferConfidentialityTest.php diff --git a/app/Http/Controllers/PrivateOfferPreparationController.php b/app/Http/Controllers/PrivateOfferPreparationController.php index 2c8457f4..3e010977 100644 --- a/app/Http/Controllers/PrivateOfferPreparationController.php +++ b/app/Http/Controllers/PrivateOfferPreparationController.php @@ -6,15 +6,84 @@ use App\Models\Opportunity; use App\Services\OpportunityCanonicalContract; use App\Services\OpportunityManagement; use App\Services\OpportunityProfileWorkflow; +use App\Services\OpportunityResources; use App\Services\PortalProfileAccess; +use App\Services\PrivateOfferConfidentiality; use App\Services\PrivateOfferPreparation; +use App\Services\VerifiedDocumentBinary; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; use Inertia\Inertia; use Inertia\Response; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; final class PrivateOfferPreparationController extends Controller { + public function resources(Request $request, PrivateOfferPreparation $service, int $id): JsonResponse + { + $record = $service->record($request->user(), $id); + $resources = []; + foreach (['images', 'documents'] as $kind) { + $resources[$kind] = array_map(fn ($row) => ['id' => $row->id, 'version' => $row->version, 'deleted' => $row->deleted_at !== null, + 'caption' => $row->payload['caption'] ?? null, 'digitally_edited' => $row->payload['digitally_edited'] ?? null, + 'edit_description' => $row->payload['edit_description'] ?? null, + 'scan' => $row->file->malware_scan_status ?? 'pending'], app(OpportunityResources::class)->listing($id, $kind)); + } + + return response()->json(['resources' => $resources, 'etag' => app(OpportunityManagement::class)->etag($record), + 'internalProofAccess' => app(PrivateOfferConfidentiality::class)->internal($request->user(), $record)]); + } + + public function attachment(Request $request, PrivateOfferPreparation $service, int $id, string $kind, string $resourceId): \Symfony\Component\HttpFoundation\Response + { + $record = $service->record($request->user(), $id); + abort_unless(in_array($kind, ['documents', 'images'], true), 404); + $link = DB::table('opportunity_'.$kind)->where('opportunity_id', $id)->where('id', $resourceId)->whereNull('deleted_at')->first(); + abort_unless($link !== null, 404); + $document = DB::table('documents')->where('id', $link->document_id)->whereNull('deleted_at')->first(); + abort_unless($document !== null, 404); + + return app(VerifiedDocumentBinary::class)->respond($document); + } + + public function confidentiality(Request $request, PrivateOfferConfidentiality $privacy, int $id): JsonResponse|Response + { + $overview = $privacy->overview($request->user(), $id); + + return $request->expectsJson() ? response()->json($overview) : Inertia::render('offers/confidentiality', ['offerId' => $id]); + } + + public function confidentialityAction(Request $request, PrivateOfferConfidentiality $privacy, int $id): JsonResponse + { + return response()->json($privacy->act($request->user(), $id, $request->all())); + } + + public function confidentialRead(Request $request, PrivateOfferConfidentiality $privacy, int $id): JsonResponse + { + try { + $result = $privacy->read($request->user(), $id); + } catch (HttpExceptionInterface $error) { + app(PortalProfileAccess::class)->audit($request->user(), 'offer_confidential_denied', ['record_id' => $id, 'http_status' => $error->getStatusCode()]); + throw $error; + } + abort_if(isset($result['denied']), 403, 'É necessária aceitação do NDA e autorização expressa do vendedor para esta versão.'); + + return response()->json($result); + } + + public function nda(Request $request, PrivateOfferConfidentiality $privacy, int $id): \Symfony\Component\HttpFoundation\Response + { + $overview = $privacy->overview($request->user(), $id); + abort_unless($overview['nda'], 409, 'O NDA aprovado ainda não foi disponibilizado.'); + app(PortalProfileAccess::class)->audit($request->user(), 'offer_nda_access', ['record_id' => $id, 'version_hash' => $overview['version_hash'], 'nda' => $overview['nda']]); + + $document = DB::table('documents')->where('id', $overview['nda']['document_id'])->firstOrFail(); + abort_unless((int) $document->version_no === $overview['nda']['version'] && hash_equals(trim($document->sha256), $overview['nda']['sha256']), 409, 'O exemplar do NDA mudou; actualize antes de continuar.'); + + return app(VerifiedDocumentBinary::class)->respond($document); + } + public function index(Request $request, PortalProfileAccess $access): Response { $filters = $request->validate(['state' => 'nullable|in:draft,submitted,in_review,needs_information,rejected,approved']); @@ -40,7 +109,9 @@ final class PrivateOfferPreparationController extends Controller $options = app(OpportunityCanonicalContract::class)->options($request->user(), $record); return Inertia::render('offers/preparation', ['record' => $presentation, 'contexts' => $contexts, - 'contacts' => $options['contacts'], 'contactInvestors' => $options['contactInvestors']]); + 'contacts' => $options['contacts'], 'contactInvestors' => $options['contactInvestors'], + 'sectors' => DB::table('sectors')->where('is_active', true)->orderBy('name')->get(['id', 'name']), + 'propertyTypes' => array_values(array_filter($options['definitions']['realEstatePatch']['properties']['property_type']['enum'], fn ($type) => is_string($type)))]); } public function save(Request $request, PrivateOfferPreparation $service, ?int $id = null): JsonResponse diff --git a/app/Services/OpportunityLibrarySupport.php b/app/Services/OpportunityLibrarySupport.php index 2c3e91d4..1280ba84 100644 --- a/app/Services/OpportunityLibrarySupport.php +++ b/app/Services/OpportunityLibrarySupport.php @@ -45,6 +45,7 @@ class OpportunityLibrarySupport */ public function attach(User $actor, Opportunity $opportunity, array $input): \stdClass { + abort_if($opportunity->isPrivateOfferPreparation(), 422, 'Os comprovativos de Ofertas usam originais reservados à validação interna.'); app(PortalProfileAccess::class)->authorize($actor, 'edit', $opportunity); $data = Validator::make($input, ['resource_id' => 'required|integer|min:1', 'creation_key' => 'required|uuid', 'etag' => 'required|string|size:64', 'reason' => 'required|string|min:20|max:1000', 'section' => 'nullable|string|max:255', 'supported_statement' => 'nullable|string|max:2000'])->validate(); diff --git a/app/Services/OpportunityResources.php b/app/Services/OpportunityResources.php index 234a6629..9b2f1b75 100644 --- a/app/Services/OpportunityResources.php +++ b/app/Services/OpportunityResources.php @@ -92,7 +92,7 @@ class OpportunityResources } abort_if(! $id && DB::table($table)->where('id', $resourceId)->exists(), 409, 'Esta criação já foi registada; recupere o recurso existente.'); $active = DB::table($table)->where('opportunity_id', $opportunityId)->whereNull('deleted_at'); - abort_if(! $id && $active->count() >= ($kind === 'locations' ? 50 : ($kind === 'images' ? 20 : 100)), 422, 'Limite de recursos atingido.'); + abort_if(! $id && $active->count() >= $this->limit($opportunity, $kind), 422, 'Limite de recursos atingido.'); if ($kind === 'locations') { $others = (clone $active)->when($id, fn ($q) => $q->where('id', '!=', $id))->get(); foreach ($others as $other) { @@ -172,7 +172,7 @@ class OpportunityResources abort_if(($data['action'] === 'delete') === ($record->deleted_at !== null), 409, 'Estado já alterado.'); $payload = json_decode($record->payload, true); if ($data['action'] === 'restore') { - abort_if(DB::table($table)->where('opportunity_id', $opportunityId)->whereNull('deleted_at')->count() >= ($kind === 'locations' ? 50 : ($kind === 'images' ? 20 : 100)), 422, 'Limite de recursos ativos atingido; reconcilie antes de restaurar.'); + abort_if(DB::table($table)->where('opportunity_id', $opportunityId)->whereNull('deleted_at')->count() >= $this->limit($opportunity, $kind), 422, 'Limite de recursos ativos atingido; reconcilie antes de restaurar.'); $this->validatePayload($kind, $payload, $opportunityId); // Restoration never silently reclaims a primary/cover/order occupied by another resource. if ($kind === 'images') { @@ -221,12 +221,22 @@ class OpportunityResources */ private function validatePayload(string $kind, array $payload, int $opportunityId): array { + $offer = Opportunity::findOrFail($opportunityId); + if ($offer->isPrivateOfferPreparation()) { + Validator::make($payload, ['visibility' => 'required|in:internal', + ...($kind === 'images' ? ['digitally_edited' => 'required|boolean', 'edit_description' => 'nullable|required_if:digitally_edited,true|string|max:1000'] : []), + ...($kind === 'documents' ? ['category' => 'required|in:real_estate_proof', 'classification' => 'required|in:restricted'] : [])])->validate(); + abort_if($kind === 'documents' && $offer->opportunity_category !== 'real_estate', 422, 'Este percurso aceita comprovativos imobiliários.'); + } $rules = ['title' => 'nullable|string|max:120', 'source_reference' => 'nullable|string|max:2000', 'source_date' => 'nullable|date_format:Y-m-d', 'visibility' => 'nullable|in:internal,authenticated,public']; if ($kind === 'locations') { $rules += ['country_code' => ['nullable', app(CountryCatalogue::class)->rule()]]; $rules += ['province_id' => 'nullable|uuid', 'district_id' => 'nullable|uuid', 'admin_post_id' => 'nullable|uuid', 'locality_id' => 'nullable|uuid', 'role' => 'nullable|in:headquarters,production,raw_material,storage,logistics,market,support,other', 'role_description' => 'nullable|string|max:1000', 'operational_unit' => 'nullable|string|max:120', 'is_primary' => 'nullable|boolean', 'address' => 'nullable|string|max:300', 'geometry' => 'nullable|array', 'area_value' => 'nullable|numeric|gt:0', 'area_unit' => ['nullable', Rule::in(self::AREA_UNITS)], 'allocation_percentage' => 'nullable|numeric|between:0,100', 'territorial_purpose' => 'nullable|in:implementation,context,impact,coverage', 'multi_district_rationale' => 'nullable|string|max:1000']; } elseif ($kind === 'images') { $rules += ['role' => 'nullable|in:cover,gallery,location,asset,product,plan,rendering,evidence,other', 'role_description' => 'nullable|string|max:1000', 'is_cover' => 'nullable|boolean', 'caption' => 'nullable|string|max:300', 'alt_text' => 'nullable|string|max:250', 'location_id' => ['nullable', 'uuid', Rule::exists('opportunity_location_drafts', 'id')->where('opportunity_id', $opportunityId)->whereNull('deleted_at')], 'captured_at' => 'nullable|date_format:Y-m-d|before_or_equal:today', 'author_source' => 'nullable|string|max:150', 'rights_status' => 'nullable|in:owned,licensed,authorized,public_domain', 'rights_document_id' => ['nullable', 'uuid', Rule::exists('opportunity_documents', 'id')->where('opportunity_id', $opportunityId)->whereNull('deleted_at')], 'rights_valid_until' => 'nullable|date_format:Y-m-d', 'sort_order' => 'nullable|integer|min:0']; + if ($offer->isPrivateOfferPreparation()) { + $rules += ['digitally_edited' => 'boolean', 'edit_description' => 'nullable|string|max:1000']; + } } else { $rules += ['category' => 'nullable|string|max:100', 'issuer' => 'nullable|string|max:255', 'edition' => 'nullable|string|max:120', 'valid_until' => 'nullable|date_format:Y-m-d', 'classification' => 'nullable|in:public,internal,confidential,restricted', 'section' => 'nullable|string|max:255', 'supported_statement' => 'nullable|string|max:2000']; } @@ -257,6 +267,11 @@ class OpportunityResources return $data; } + private function limit(Opportunity $record, string $kind): int + { + return $kind === 'locations' ? 50 : ($kind === 'images' ? ($record->isPrivateOfferPreparation() ? 6 : 20) : 100); + } + /** @return array{string,string} */ private function store(string $kind, UploadedFile $file): array { diff --git a/app/Services/PrivateOfferConfidentiality.php b/app/Services/PrivateOfferConfidentiality.php new file mode 100644 index 00000000..a20f02cf --- /dev/null +++ b/app/Services/PrivateOfferConfidentiality.php @@ -0,0 +1,216 @@ +canView($actor, $record) && $access->mfa($actor) && ! $access->has($actor, 'auditor') && ! $access->conflict($actor, $record) + && $access->scoped($actor, 'validator', $record->organization_id, $record->opportunity_category); + } + + public function seller(User $actor, Opportunity $record): bool + { + $access = app(PortalProfileAccess::class); + + return (int) $record->owner_user_id === $actor->id && $access->promoterRecordScope($actor, $record) + && $access->canCreate($actor, $record->organization_id, $record->opportunity_category) && $access->operational($actor, 'edit', $record->id); + } + + /** @return array|null */ + public function nda(): ?array + { + $definition = config('private_offers.nda'); + if (! is_array($definition) || empty($definition['approval_reference']) || empty($definition['document_id']) || empty($definition['version']) || empty($definition['sha256'])) { + return null; + } + $doc = DB::table('documents')->where('id', $definition['document_id'])->whereNull('deleted_at')->first(); + if (! $doc || $doc->status !== 'active' || $doc->malware_scan_status !== 'clean' || (int) $doc->version_no !== (int) $definition['version'] + || ! hash_equals(trim($doc->sha256), $definition['sha256']) || ! hash_equals(trim($doc->sha256), trim((string) $doc->malware_scanned_sha256))) { + return null; + } + try { + app(VerifiedDocumentBinary::class)->respond($doc); + } catch (HttpExceptionInterface $error) { + return null; + } + $result = ['document_id' => $doc->id, 'version' => (int) $doc->version_no, 'sha256' => trim($doc->sha256), 'approval_reference' => $definition['approval_reference']]; + + return [...$result, 'fingerprint' => hash('sha256', json_encode($result, JSON_THROW_ON_ERROR))]; + } + + private function company(int $id, bool $lock = false): Opportunity + { + $record = Opportunity::query()->when($lock, fn ($q) => $q->lockForUpdate())->findOrFail($id); + abort_unless($record->isPrivateOfferPreparation() && $record->opportunity_category === 'business_sale', 404); + + return $record; + } + + private function approvedHash(Opportunity $record): string + { + $hash = $record->workflow_context['approval']['hash'] ?? null; + abort_unless(is_string($hash) && app(PortalProfileAccess::class)->state($record) === 'approved', 409, 'Esta versão não tem aprovação editorial actual.'); + try { + $snapshot = app(PrivateOfferPreparation::class)->snapshot($record); + } catch (ValidationException $error) { + abort(409, 'A versão ou os suportes exigem reconferência editorial.'); + } + abort_unless(hash_equals($hash, $snapshot['hash']), 409, 'A versão mudou; é necessária reconferência editorial.'); + + return $hash; + } + + /** PostgreSQL jsonb reorders object keys; identity is the version and checksum, not array order. + * @param array $nda */ + private function sameNda(mixed $accepted, array $nda): bool + { + if (! is_array($accepted)) { + return false; + } + foreach (['document_id', 'version', 'sha256', 'approval_reference', 'fingerprint'] as $key) { + if (($accepted[$key] ?? null) !== $nda[$key]) { + return false; + } + } + + return true; + } + + /** @return array */ + public function overview(User $actor, int $id): array + { + $record = $this->company($id); + // An interested party learns no private title, contacts, documents or financial values. + $seller = $this->seller($actor, $record); + $nda = $this->nda(); + $context = $record->workflow_context['offer_confidentiality'] ?? []; + $currentHash = null; + try { + $currentHash = $this->approvedHash($record); + } catch (HttpExceptionInterface $error) { + // A previous acceptance remains withdrawable even if the offer was revised. + abort_unless($seller || isset($context['acceptances'][(string) $actor->id]), 404); + } + $history = array_values(array_filter($context['events'] ?? [], fn ($event) => $seller || $event['interested_user_id'] === $actor->id)); + + return ['offer_id' => $id, 'nda' => $nda, 'nda_pending' => $nda === null, 'version_hash' => $record->workflow_context['approval']['hash'] ?? null, + 'etag' => $seller ? app(OpportunityManagement::class)->etag($record) : null, + 'can_accept' => ! $seller && $nda !== null && $currentHash !== null && ! app(PortalProfileAccess::class)->conflict($actor, $record), + 'can_authorize' => $seller && $nda !== null && $currentHash !== null, + 'seller' => $seller, 'requests' => $seller ? array_values($context['acceptances'] ?? []) : [], + 'grants' => $seller ? array_values($context['grants'] ?? []) : [], + 'history' => array_map(fn ($event) => array_intersect_key($event, array_flip(['id', 'action', 'actor_id', 'at', 'version_hash', 'nda'])), $history), + 'acceptance' => $context['acceptances'][(string) $actor->id] ?? null]; + } + + /** @param array $input + * @return array */ + public function act(User $actor, int $id, array $input): array + { + $data = validator($input, ['action' => 'required|in:accept,authorize,revoke,withdraw', 'version_hash' => 'required|string|size:64', + 'nda_sha256' => 'required_unless:action,revoke,withdraw|string|size:64', 'acceptance_id' => 'required_if:action,authorize|uuid', + 'nda_fingerprint' => 'required_if:action,accept,authorize|string|size:64', + 'interested_user_id' => 'required_if:action,authorize,revoke|integer|min:1', 'confirm' => 'required|accepted', + 'reason' => ['nullable', 'required_if:action,authorize,revoke', 'string', 'max:1000', ...(in_array($input['action'] ?? null, ['authorize', 'revoke'], true) ? ['min:20'] : [])]])->validate(); + + return DB::transaction(function () use ($actor, $id, $data) { + $record = $this->company($id, true); + $context = $record->workflow_context; + $privacy = $context['offer_confidentiality'] ?? ['acceptances' => [], 'grants' => [], 'events' => []]; + $action = $data['action']; + $target = in_array($action, ['authorize', 'revoke'], true) ? (string) $data['interested_user_id'] : (string) $actor->id; + $before = ['acceptance' => $privacy['acceptances'][$target] ?? null, 'grant' => $privacy['grants'][$target] ?? null]; + if (in_array($action, ['authorize', 'revoke'], true)) { + abort_unless($this->seller($actor, $record), 403); + } elseif ($action === 'accept') { + abort_if($this->seller($actor, $record) || app(PortalProfileAccess::class)->conflict($actor, $record), 403); + } + if (in_array($action, ['accept', 'authorize'], true)) { + $nda = $this->nda(); + abort_unless($nda !== null, 409, 'O exemplar aprovado do NDA ainda não foi disponibilizado.'); + abort_unless(hash_equals($this->approvedHash($record), $data['version_hash']) && hash_equals($nda['sha256'], $data['nda_sha256']) + && hash_equals($nda['fingerprint'], $data['nda_fingerprint']), 409); + $subject = User::findOrFail((int) $target); + abort_unless($subject->hasVerifiedEmail() && ! app(PortalProfileAccess::class)->conflict($subject, $record), 403); + if ($action === 'accept') { + $privacy['acceptances'][$target] = ['id' => (string) Str::uuid(), 'actor_id' => $actor->id, 'at' => now()->toIso8601String(), + 'version_hash' => $data['version_hash'], 'nda' => $nda, 'access_revision' => (int) $subject->portal_access_revision]; + unset($privacy['grants'][$target]); // A fresh acceptance never reuses a previous seller decision. + } else { + $acceptance = $privacy['acceptances'][$target] ?? null; + abort_unless($acceptance && $acceptance['id'] === $data['acceptance_id'] && $acceptance['version_hash'] === $data['version_hash'] + && $this->sameNda($acceptance['nda'], $nda) && $acceptance['access_revision'] === (int) $subject->portal_access_revision, 409); + $privacy['grants'][$target] = ['interested_user_id' => (int) $target, 'seller_id' => $actor->id, 'at' => now()->toIso8601String(), + 'version_hash' => $data['version_hash'], 'acceptance_id' => $acceptance['id'], 'nda' => $nda]; + } + } else { + $previous = $privacy[$action === 'withdraw' ? 'acceptances' : 'grants'][$target] ?? null; + abort_unless($previous && $previous['version_hash'] === $data['version_hash'], 409); + unset($privacy['grants'][$target]); + if ($action === 'withdraw') { + unset($privacy['acceptances'][$target]); + } + } + $event = ['id' => (string) Str::uuid(), 'action' => $action, 'actor_id' => $actor->id, 'interested_user_id' => (int) $target, + 'version_hash' => $data['version_hash'], 'at' => now()->toIso8601String(), 'nda_sha256' => $data['nda_sha256'] ?? null, + 'nda' => $nda ?? ($before['acceptance']['nda'] ?? $before['grant']['nda'] ?? null)]; + $privacy['events'][] = [...$event, 'reason' => Crypt::encryptString($data['reason'] ?? ''), 'before' => $before, + 'after' => ['acceptance' => $privacy['acceptances'][$target] ?? null, 'grant' => $privacy['grants'][$target] ?? null]]; + $context['offer_confidentiality'] = $privacy; + $record->workflow_context = $context; + $record->management_version++; + $record->save(); + app(PortalProfileAccess::class)->audit($actor, 'offer_confidentiality_'.$action, ['record_id' => $id, ...$event]); + + return ['recorded' => true, 'event_id' => $event['id']]; + }); + } + + /** @return array */ + public function read(User $actor, int $id): array + { + return DB::transaction(function () use ($actor, $id) { + $record = $this->company($id, true); + $nda = $this->nda(); + $privacy = $record->workflow_context['offer_confidentiality'] ?? []; + $accept = $privacy['acceptances'][(string) $actor->id] ?? null; + $grant = $privacy['grants'][(string) $actor->id] ?? null; + $hash = $this->approvedHash($record); + $seller = User::find($record->owner_user_id); + $subject = User::find($actor->id); + $allowed = $nda && $accept && $grant && $seller && $subject && $this->seller($seller, $record) && $subject->hasVerifiedEmail() + && $accept['access_revision'] === (int) $subject->portal_access_revision && $grant['seller_id'] === $seller->id + && $grant['acceptance_id'] === $accept['id'] && $grant['version_hash'] === $hash && $accept['version_hash'] === $hash + && $this->sameNda($grant['nda'], $nda) && $this->sameNda($accept['nda'], $nda) && ! app(PortalProfileAccess::class)->conflict($actor, $record); + app(PortalProfileAccess::class)->audit($actor, $allowed ? 'offer_confidential_access' : 'offer_confidential_denied', ['record_id' => $id, 'version_hash' => $hash, + 'nda' => $nda, 'acceptance_id' => $accept['id'] ?? null]); + // Return the refusal after transaction commit so the denied-access audit survives. + if (! $allowed) { + return ['denied' => true]; + } + + $business = $record->workflow_context['submission']['payload']['private_offer']['business'] ?? []; + foreach (['sector_id' => 'sector', 'secondary_sector_id' => 'secondary_sector'] as $field => $label) { + if (isset($business[$field])) { + $business[$label] = DB::table('sectors')->where('id', $business[$field])->value('name'); + } + unset($business[$field]); + } + + return ['offer_id' => $id, 'version_hash' => $hash, 'business' => $business]; + }); + } +} diff --git a/app/Services/PrivateOfferDetails.php b/app/Services/PrivateOfferDetails.php new file mode 100644 index 00000000..12197e86 --- /dev/null +++ b/app/Services/PrivateOfferDetails.php @@ -0,0 +1,83 @@ + $input + * @return array */ + public function validate(array $input): array + { + $bytes = file_get_contents(resource_path('data/investmoz-opportunity-modular.schema.json')); + if ($bytes === false) { + throw new \RuntimeException('Opportunity schema unavailable.'); + } + $schema = json_decode($bytes, true, 512, JSON_THROW_ON_ERROR); + $property = $schema['$defs']['realEstatePatch']['properties']; + + return Validator::make($input, [ + 'business' => 'sometimes|nullable|array:sector_id,secondary_sector_id,revenue,cash_flow,currency,reason,availability', + 'business.sector_id' => ['nullable', 'integer', Rule::exists('sectors', 'id')->where('is_active', true)], + 'business.secondary_sector_id' => ['nullable', 'integer', Rule::exists('sectors', 'id')->where('is_active', true)], + 'business.revenue' => ['nullable', 'string', 'regex:/^\d{1,16}(\.\d{1,2})?$/'], + 'business.cash_flow' => ['nullable', 'string', 'regex:/^-?\d{1,16}(\.\d{1,2})?$/'], + 'business.currency' => 'nullable|required_with:business.revenue,business.cash_flow|in:MZN,USD', + 'business.reason' => 'nullable|string|max:1000', 'business.availability' => 'nullable|in:available,sold', + 'property' => 'sometimes|nullable|array:transaction_type,property_type,land_area,built_area,area_unit,bedrooms,bathrooms,amenities,availability', + 'property.transaction_type' => 'nullable|in:sale,lease', + 'property.property_type' => ['nullable', Rule::in($property['property_type']['enum'])], + 'property.land_area' => 'nullable|numeric|gt:0', 'property.built_area' => 'nullable|numeric|gt:0', + 'property.area_unit' => ['nullable', Rule::in(OpportunityResources::AREA_UNITS)], + 'property.bedrooms' => 'nullable|integer|min:0|max:10000', 'property.bathrooms' => 'nullable|integer|min:0|max:10000', + 'property.amenities' => 'nullable|array|max:100', 'property.amenities.*' => 'string|max:250', + 'property.availability' => 'nullable|in:available,sold,leased', + ])->validate(); + } + + /** @param array $offer */ + public function assertCategory(array $offer, string $category): void + { + Validator::make($offer, [ + 'business' => $category === 'business_sale' ? 'nullable|array' : 'prohibited', + 'property' => $category === 'real_estate' ? 'nullable|array' : 'prohibited', + ])->validate(); + } + + /** Snapshot all active linked originals and metadata in the existing editorial version. + * @return array> */ + public function resources(Opportunity $record): array + { + $result = ['documents' => [], 'images' => [], 'locations' => []]; + foreach (array_keys($result) as $kind) { + foreach (app(OpportunityResources::class)->listing($record->id, $kind) as $row) { + if ($row->deleted_at !== null) { + continue; + } + if ($kind !== 'locations') { + $file = $row->file; + $doc = DB::table('documents')->where('id', $row->document_id)->first(); + if (! $doc || $doc->deleted_at !== null || $doc->status !== 'active' || $doc->malware_scan_status !== 'clean' + || ! hash_equals(trim($doc->sha256), trim((string) $doc->malware_scanned_sha256))) { + throw ValidationException::withMessages(['attachments' => 'Aguarde uma análise de segurança válida dos anexos.']); + } + $result[$kind][$row->id] = ['version' => $row->version, 'document_id' => $row->document_id, 'payload' => $row->payload, 'file' => $file]; + } else { + $result[$kind][$row->id] = ['version' => $row->version, 'payload' => $row->payload]; + } + } + ksort($result[$kind]); + } + if (count($result['images']) > 6) { + throw ValidationException::withMessages(['images' => 'A oferta admite no máximo seis fotografias.']); + } + + return $result; + } +} diff --git a/app/Services/PrivateOfferPreparation.php b/app/Services/PrivateOfferPreparation.php index f655bf1e..931a9553 100644 --- a/app/Services/PrivateOfferPreparation.php +++ b/app/Services/PrivateOfferPreparation.php @@ -15,13 +15,17 @@ final class PrivateOfferPreparation /** @return array */ public function validate(mixed $input, bool $complete = false): array { - return Validator::make(['private_offer' => $input], [ - 'private_offer' => 'present|array:price_mode,amount,currency,contact_id', + $validated = Validator::make(['private_offer' => $input], [ + 'private_offer' => 'present|array:price_mode,amount,currency,contact_id,business,property', 'private_offer.price_mode' => 'nullable|in:amount,negotiable', 'private_offer.amount' => ['nullable', 'string', ...($complete ? ['required_if:private_offer.price_mode,amount'] : []), 'prohibited_unless:private_offer.price_mode,amount', 'regex:/^\d{1,16}(\.\d{1,2})?$/'], 'private_offer.currency' => ['nullable', ...($complete ? ['required_if:private_offer.price_mode,amount'] : []), 'prohibited_unless:private_offer.price_mode,amount', 'in:MZN,USD'], 'private_offer.contact_id' => 'nullable|uuid', + 'private_offer.business' => 'sometimes|nullable|array', + 'private_offer.property' => 'sometimes|nullable|array', ])->validate()['private_offer']; + + return array_replace($validated, app(PrivateOfferDetails::class)->validate(array_intersect_key($validated, array_flip(['business', 'property'])))); } /** @param array $input */ @@ -40,6 +44,8 @@ final class PrivateOfferPreparation return DB::transaction(function () use ($actor, $input, $id) { $record = app(OpportunityManagement::class)->save($actor, $input, $id, true); + $values = app(OpportunityManagement::class)->values($record); + app(PrivateOfferDetails::class)->assertCategory($values['private_offer'] ?? [], $values['opportunity_category']); $contactId = app(OpportunityManagement::class)->values($record)['private_offer']['contact_id'] ?? null; if ($contactId && ! app(OpportunityContacts::class)->related($record, $contactId)) { throw ValidationException::withMessages(['private_offer.contact_id' => 'Seleccione um contacto autorizado do promotor.']); @@ -128,6 +134,8 @@ final class PrivateOfferPreparation 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); + app(PrivateOfferDetails::class)->assertCategory($offer, $values['opportunity_category']); + $resources = app(PrivateOfferDetails::class)->resources($record); 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.']); } @@ -142,6 +150,10 @@ final class PrivateOfferPreparation $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]; + $extended = isset($offer['business']) || isset($offer['property']) || array_filter($resources) !== []; + if ($extended) { + $payload['resources'] = $resources; + } $normalize = function (mixed $value) use (&$normalize): mixed { if (is_object($value)) { $value = get_object_vars($value); @@ -157,8 +169,10 @@ final class PrivateOfferPreparation }; $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' => []]]; + $contract = $extended ? 'private-offer-editorial-v3' : 'private-offer-editorial-v2'; + + return ['payload' => $payload, 'hash' => hash('sha256', json_encode(['contract' => $contract, 'payload' => $payload], JSON_THROW_ON_ERROR)), + 'record_version' => $record->management_version, 'contract_version' => $contract, 'resources' => $resources]; } /** @return array */ diff --git a/app/Services/VerifiedDocumentBinary.php b/app/Services/VerifiedDocumentBinary.php index 53183298..8e1a1260 100644 --- a/app/Services/VerifiedDocumentBinary.php +++ b/app/Services/VerifiedDocumentBinary.php @@ -61,7 +61,11 @@ class VerifiedDocumentBinary if ($table === 'opportunity_images') { $query->orWhereRaw("payload->'derivatives' @> ?::jsonb", [json_encode([['document_id' => $document->id]], JSON_THROW_ON_ERROR)]); } - $links = $links->concat($query->get(['opportunity_id', 'deleted_at', 'payload'])); + $rows = $query->get(['opportunity_id', 'deleted_at', 'payload']); + foreach ($rows as $link) { + $link->resource_kind = $table; + } + $links = $links->concat($rows); } $privateOrigin = in_array($document->source_system ?? null, ['opportunity-resources', 'opportunity-image-derivatives'], true); if (! $privateOrigin) { @@ -72,6 +76,15 @@ class VerifiedDocumentBinary } $actor = request()->user(); abort_unless($actor instanceof User, 403, 'Anexo privado: é necessário acesso autorizado à oportunidade.'); + foreach ($links as $link) { + $offer = Opportunity::withTrashed()->whereKey($link->opportunity_id)->first(); + if ($offer?->isPrivateOfferPreparation() && $link->resource_kind === 'opportunity_documents') { + $allowed = $link->deleted_at === null && ! $offer->trashed() && app(PrivateOfferConfidentiality::class)->internal($actor, $offer); + app(PortalProfileAccess::class)->audit($actor, $allowed ? 'offer_proof_access_authorized' : 'offer_proof_access_denied', + ['record_id' => $offer->id, 'document_id' => $document->id, 'record_version' => $offer->management_version, 'sha256' => trim($document->sha256)]); + abort_unless($allowed, 403, 'Comprovativo reservado à validação interna autorizada.'); + } + } foreach ($links as $link) { $opportunity = Opportunity::whereKey($link->opportunity_id)->first(); if ($link->deleted_at === null && $opportunity && $opportunity->status !== 'archived' diff --git a/config/private_offers.php b/config/private_offers.php new file mode 100644 index 00000000..4b52c37d --- /dev/null +++ b/config/private_offers.php @@ -0,0 +1,6 @@ + null, +]; diff --git a/docs/investmoz/unification/traceability-and-manual.md b/docs/investmoz/unification/traceability-and-manual.md index 3b1e39dd..74f95514 100644 --- a/docs/investmoz/unification/traceability-and-manual.md +++ b/docs/investmoz/unification/traceability-and-manual.md @@ -10173,3 +10173,15 @@ A publicação efectiva permanece bloqueada, incluindo rotas alternativas. O cat QA final: 10 testes/133 asserções aprovados; inclui acessos directos, estados inválidos, isolamento, preservação, hierarquia, decimais/moedas, mapa sem localização exacta, CSV, HTML compilado e regressão nativa autocontida. Passagem anterior: 36/41 aprovados; cinco casos nativos impedidos por referência Inhambane ausente na base sintética. Tipos, lint focado, Pint, PHPStan e build aprovados. Formatação global: os mesmos 61 ficheiros exteriores ao delta. Conferência visual/móvel pendente: `The browser access host is unavailable`; não substituída por aprovação técnica. Email real F2 mantém pendência. Estado: implementado em QA com barreira fechada, não instalado; módulo integral e Procuras parciais. Limites representados: 1.000 candidatos e contexto de 30 territórios, assinalados quando truncados. Relatório: `docs/investmoz/v2/auditoria-portal/ofertas-fase3-entrega-qa-20261114.md`. Recibo: `outputs/offers-public-phase3-20261114/qa-result.json`. Próximo passo: conferência desktop/móvel e apreciação da entrega QA; sem instalação ou avanço de fase por inferência. + +## 14/11/2026 — Ofertas Fase 4, particularidades e confidencialidade em QA + +Base F3 7691cab3 preservada, tal como F1/F2 e plano aceite. A Tempo confirmou os campos propostos como opcionais na preparação; confirmou que o NDA real continua pendente. Empresas: sectores do cadastro existente, receita/fluxo de caixa, moeda, motivo e disponibilidade declarada. Imóveis: modalidade, tipo existente, áreas/unidade, divisões, comodidades e disponibilidade. Reutiliza localização, rascunho, versões, submissão e revisão existentes. + +Fotografias: máximo de seis activas, envio/alteração/retirada/restauro sob autorização e bloqueio do registo. Originais cifrados no disco privado; MIME, conteúdo, tamanho e quarentena no serviço existente. Declaração de edição digital conservada. Comprovativos imobiliários só podem ser lidos por validador interno com âmbito, MFA e sem conflito; dono/interessado/auditor/outro âmbito recusados. O controlo comum de binários impede atalhos; o envio não certifica titularidade. + +Controlo empresarial exige aceitação do exemplar exacto do NDA e autorização expressa do vendedor para a mesma versão aprovada. Cada consulta revalida ambas, direitos e revisão de acesso. Retirada/revogação e alteração de versão fecham a consulta seguinte; nova aceitação exige nova autorização. História e auditoria preservam actores, instantes, hashes, versões e antes/depois; fundamentos cifrados. A leitura autorizada só entrega os campos empresariais conferidos, sem contactos, morada exacta ou comprovativos. NDA configurado nulo, sem texto inventado ou publicação. Fotografias originais continuam privadas. + +Os cinco testes territoriais foram investigados: dependiam de Inhambane/turismo previamente importados numa base vazia. O fixture cria apenas os mínimos sintéticos quando faltam, com guarda testing/loopback/*_test:55441 e transacção de rollback. Os seis casos passaram; nenhum cadastro operacional foi alterado. Os snapshots F4 vinculam particularidades/recursos ao contrato v3; Ofertas comuns anteriores conservam v2. + +Estado: implementado em QA, sem instalação, publicação efectiva, pagamentos, destaque comercial, migrações ou alterações em mne. Fase 5 não iniciada; módulo integral/Procuras parciais. Pendências: exemplar/versão/aprovação do NDA, identificação jurídica dos comprovativos admitidos, visual desktop/móvel e email real anteriores. GD ausente: derivadas do processador nativo continuam bloqueadas, sem simular processamento. Formatação global mantém 61 ficheiros exteriores ao delta. Relatório: docs/investmoz/v2/auditoria-portal/ofertas-fase4-entrega-qa-20261114.md. Recibo: outputs/offers-private-phase4-20261114/qa-result.json; contém resultados e hashes finais, sem somar execuções sobrepostas. Próximo passo: apreciação QA e disponibilização do NDA aprovado, mantendo o bloqueio de publicação. diff --git a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv index 5ac4e8e5..357391e4 100644 --- a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv +++ b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv @@ -163,7 +163,7 @@ RF-MIN-005,"Controlar cessão, mudança de controlo e aprovações.",upgrade v4 RF-MIN-006,Pesquisar e corresponder oportunidades mineiras.,upgrade v4 SHA f0b9815…; p. 3275; requirements-audit.csv,oportunidades,/api/v1/business-opportunities; /api/v1/business-opportunities/{slug}; /investidor/terra-duat; /investidor/terra-duat/novo; /investidor/terra-duat/{id}; /investidor/terra-duat/{id}/actualizacoes; /investidor/terra-duat/{id}/documentos; /investidor/terra-duat/{id}/documentos/{attachment}; /investidor/terra-duat/{id}/editar; /investidor/terra-duat/{id}/imprimir; /investidor/terra-duat/{id}/solicitar-apoio; /management/opportunities,"OpportunityManagement, OpportunityCanonicalContract/Response, OpportunityReviewMateriality, PortalProfileAccess, OpportunityResources. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.",outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,Sem evidência suficiente,Reconciliar contrato/cláusula e provas por categoria; completar apenas falha/variante executável identificada. Response normativo não é completo. Critério individual original preservado; cobertura atual ainda não individualizada.,"030000→040000 já instalados; contratos/resources existentes, sessão/MFA, ClamD e worker para ficheiros; gates das seis categorias ON. Variantes modulares/offers continuam separadas. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.","RF-MIN-006: Filtros incluem recurso, fase, título, localização, valor, modalidade e infra-estrutura; recomendação é explicável e revista por gestor.",P2,RF-MIN-006: Relatório 25/09 conservado; estado histórico não promove conclusão,RF-MIN-006,1,"[{""id"":""RF-MIN-006"",""pedido"":""Pesquisar e corresponder oportunidades mineiras."",""fontes"":[""upgrade v4 SHA f0b9815…; p. 3275; requirements-audit.csv""],""aceitacao"":""Filtros incluem recurso, fase, título, localização, valor, modalidade e infra-estrutura; recomendação é explicável e revista por gestor."",""estado"":""parcial"",""variantes"":""Relatório 25/09 conservado; estado histórico não promove conclusão"",""modulo"":""oportunidades""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,public.opportunities: 6 registos; public.opportunity_images: tabela ausente; public.opportunity_documents: tabela ausente; public.opportunity_location_drafts: tabela ausente,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.",Reconciliar contrato/cláusula e provas por categoria; completar apenas falha/variante executável identificada. Response normativo não é completo. Critério individual original preservado; cobertura atual ainda não individualizada.,"OpportunityManagement, OpportunityCanonicalContract/Response, OpportunityReviewMateriality, PortalProfileAccess, OpportunityResources","030000→040000 já instalados; contratos/resources existentes, sessão/MFA, ClamD e worker para ficheiros; gates das seis categorias ON. Variantes modulares/offers continuam separadas.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: oportunidades. Esforço de código adicional só após lacuna reproduzida.,outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,oportunidades,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",app/Services/OpportunityManagement.php; app/Services/OpportunityResources.php; resources/js/pages/management/opportunity-draft.tsx — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"OpportunityManagementTest, OpportunityResourcesTest, OpportunityOriginReviewTest; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.",Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,, RF-MN-001,Registar utilizadores singulares e colectivos com verificação de contactos.,upgrade v4 SHA f0b9815…; p. 2957; requirements-audit.csv | portal do investidor\upgrade do portal de investimentos.docx; SHA 471012abb0f56377956f4ab822fe3186e2561bee0471508a5d807091f2e0154f; w:p 573 | portal do investidor\Portal V4\upgrade do portal de investimentos v6 programacao.docx; SHA e1169a7743b13ff6c950d3ffd4672a2afea32e81da84885e11154b7d8de9241e; w:p 3409 | portal do investidor\Portal V4\upgrade do portal de investimentos v1.docx; SHA f70ce3075523c6278e00dce15b9c7acf2b690679e16959ae5f7d007e754cfc6e; w:p 580 | portal do investidor\Portal V4\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 2214 | portal do investidor\Portal V4\upgrade do portal de investimentos v5 programacao.docx; SHA edece80c0519537244f8a8b86456af0aa92de1c526f791bb0ee9b85af7a7e8b2; w:p 3409,CRM/solicitações,//; /Simulador; /api/v1/documents/recommendations; /api/v1/investment/submissions; /api/v1/investment/submissions/{id}; /api/v1/investment/submissions/{id}/attachments; /api/v1/investment/submissions/{id}/attachments/{attachment}; /api/v1/investment/submissions/{id}/submit; /apoio; /balcao-virtual; /como-investir; /documentos,"investors/contacts/leads/tasks, OpportunityContacts, acompanhamento interno existente. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.",outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,Sem evidência suficiente,"Converter sem duplicar, tarefa/responsável/próxima ação recuperáveis; corrigir apenas falha comprovada. Critério individual original preservado; cobertura atual ainda não individualizada.",Relações mestre/contexto e autorizações; não depende de CRM externo. C131 sóQA. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,RF-MN-001: Conta só é activada após confirmação do correio electrónico ou telefone; tentativas e falhas ficam registadas. | MASTER-471012ab-0051: Registar utilizadores singulares e colectivos com verificação de contactos. | MASTER-e1169a77-0201: Registar utilizadores singulares e colectivos com verificação de contactos. | MASTER-f70ce307-0057: Registar utilizadores singulares e colectivos com verificação de contactos. | MASTER-51b8f0ad-0137: Registar utilizadores singulares e colectivos com verificação de contactos. | MASTER-edece80c-0201: Registar utilizadores singulares e colectivos com verificação de contactos.,P2,RF-MN-001: Relatório 25/09 conservado; estado histórico não promove conclusão | MASTER-471012ab-0051: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-e1169a77-0201: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-f70ce307-0057: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-51b8f0ad-0137: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-edece80c-0201: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,RF-MN-001 | MASTER-471012ab-0051 | MASTER-e1169a77-0201 | MASTER-f70ce307-0057 | MASTER-51b8f0ad-0137 | MASTER-edece80c-0201,6,"[{""id"":""RF-MN-001"",""pedido"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""fontes"":[""upgrade v4 SHA f0b9815…; p. 2957; requirements-audit.csv""],""aceitacao"":""Conta só é activada após confirmação do correio electrónico ou telefone; tentativas e falhas ficam registadas."",""estado"":""parcial"",""variantes"":""Relatório 25/09 conservado; estado histórico não promove conclusão"",""modulo"":""CRM/solicitações""},{""id"":""MASTER-471012ab-0051"",""pedido"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""fontes"":[""portal do investidor\\upgrade do portal de investimentos.docx; SHA 471012abb0f56377956f4ab822fe3186e2561bee0471508a5d807091f2e0154f; w:p 573""],""aceitacao"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""},{""id"":""MASTER-e1169a77-0201"",""pedido"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v6 programacao.docx; SHA e1169a7743b13ff6c950d3ffd4672a2afea32e81da84885e11154b7d8de9241e; w:p 3409""],""aceitacao"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""},{""id"":""MASTER-f70ce307-0057"",""pedido"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v1.docx; SHA f70ce3075523c6278e00dce15b9c7acf2b690679e16959ae5f7d007e754cfc6e; w:p 580""],""aceitacao"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""},{""id"":""MASTER-51b8f0ad-0137"",""pedido"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 2214""],""aceitacao"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""},{""id"":""MASTER-edece80c-0201"",""pedido"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v5 programacao.docx; SHA edece80c0519537244f8a8b86456af0aa92de1c526f791bb0ee9b85af7a7e8b2; w:p 3409""],""aceitacao"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,public.interest_requests: 4 registos; public.leads: 0 registos; public.investors: 3 registos,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.","Converter sem duplicar, tarefa/responsável/próxima ação recuperáveis; corrigir apenas falha comprovada. Critério individual original preservado; cobertura atual ainda não individualizada.","investors/contacts/leads/tasks, OpportunityContacts, acompanhamento interno existente",Relações mestre/contexto e autorizações; não depende de CRM externo. C131 sóQA.,Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: acompanhamento. Esforço de código adicional só após lacuna reproduzida.,outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,acompanhamento,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",app/Http/Controllers/CrmController.php; app/Services/DeclaredOriginStatistics.php — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"InterestSubmissionTest, InvestmentWorkspaceTest, PortalOriginCompatibilityTest; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.",Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,, RF-MN-002,"Gerir perfis, representantes, funções e organizações.",upgrade v4 SHA f0b9815…; p. 2961; requirements-audit.csv,governação/outros,/api/v1/investment/admin/interests/{interest}/status; /management/history; /management/interests; /management/interests/{interest}; /management/interests/{interest}/consent/contact; /management/interests/{interest}/consent/proponent; /management/opportunities; /management/opportunities/create; /management/opportunities/drafts; /management/opportunities/{id}/draft; /management/opportunities/{id}/edit; /management/opportunities/{id}/lifecycle,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.","outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual. | 02/11/2026: profile-json preserva erros por campo e reautenticacao em falhas HTML/419 e rejeita JSON invalido; contexto de investimento rejeita falso sucesso HTTP 200 e liberta keepalive, sem repetir escritas. 26 testes focados, TypeScript e build aprovados. Prova local parcial; sem dados/permissoes/publicacao. | Retoma 02/11/2026: profileJson GET independente de cookie CSRF malformado; POST recusa antes do transporte com mensagem de recuperacao. Preservadas validacao e credenciais. 14 testes focados aprovados, incluindo separadores de cookie sem espaco; sem mudanca de direitos.",Sem evidência suficiente,Localizar obrigação verificável e consumidores/contrato atuais antes de estimar implementação ou declarar conclusão. Critério individual original preservado; cobertura atual ainda não individualizada.,"Ler secção/versão correspondente; distinguir requisito funcional, decisão e exemplo. Fontes oficiais já aceites. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.",RF-MN-002: Uma organização pode ter vários utilizadores; cada acção identifica o utilizador e a função.,P2,RF-MN-002: Relatório 25/09 conservado; estado histórico não promove conclusão,RF-MN-002,1,"[{""id"":""RF-MN-002"",""pedido"":""Gerir perfis, representantes, funções e organizações."",""fontes"":[""upgrade v4 SHA f0b9815…; p. 2961; requirements-audit.csv""],""aceitacao"":""Uma organização pode ter vários utilizadores; cada acção identifica o utilizador e a função."",""estado"":""parcial"",""variantes"":""Relatório 25/09 conservado; estado histórico não promove conclusão"",""modulo"":""governação/outros""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,Requisito transversal; consultar estado-ativo-after.json,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.",Localizar obrigação verificável e consumidores/contrato atuais antes de estimar implementação ou declarar conclusão. Critério individual original preservado; cobertura atual ainda não individualizada.,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer","Ler secção/versão correspondente; distinguir requisito funcional, decisão e exemplo. Fontes oficiais já aceites.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: governação e demais cláusulas. Esforço de código adicional só após lacuna reproduzida.,outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,governação e demais cláusulas,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",AGENTS.md — componentes do domínio encontrados; correspondência integral deste requisito não presumida,; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.,Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,, -RF-MN-003,"Criar, guardar, editar e submeter ofertas e procuras.",upgrade v4 SHA f0b9815…; p. 2965; requirements-audit.csv | portal do investidor\upgrade do portal de investimentos.docx; SHA 471012abb0f56377956f4ab822fe3186e2561bee0471508a5d807091f2e0154f; w:p 581 | portal do investidor\Portal V4\upgrade do portal de investimentos v6 programacao.docx; SHA e1169a7743b13ff6c950d3ffd4672a2afea32e81da84885e11154b7d8de9241e; w:p 3417 | portal do investidor\Portal V4\upgrade do portal de investimentos v1.docx; SHA f70ce3075523c6278e00dce15b9c7acf2b690679e16959ae5f7d007e754cfc6e; w:p 588 | portal do investidor\Portal V4\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 2222 | portal do investidor\Portal V4\upgrade do portal de investimentos v5 programacao.docx; SHA edece80c0519537244f8a8b86456af0aa92de1c526f791bb0ee9b85af7a7e8b2; w:p 3417,governação/outros,/api/v1/investment/admin/interests/{interest}/status; /management/history; /management/interests; /management/interests/{interest}; /management/interests/{interest}/consent/contact; /management/interests/{interest}/consent/proponent; /management/opportunities; /management/opportunities/create; /management/opportunities/drafts; /management/opportunities/{id}/draft; /management/opportunities/{id}/edit; /management/opportunities/{id}/lifecycle; /ofertas; /ofertas/{id}; /api/v1/offers; /api/v1/offers/{id}; /api/v1/offers/map; /api/v1/offers/export,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual. | 14/11/2026 - Ofertas F1: oportunidades como cadastro principal; criar, guardar, recuperar, editar e submeter em privado; autorizacao existente, CRM e localizacao reutilizados; sem menu, publicacao ou instalacao. | 14/11/2026 F2 autorizada: circuito editorial privado no workflow e Dashboard existentes; revisao/atribuicao/rejeicao/correccao/reenvio/aprovacao interna; decisoes por snapshot/hash/ciclo, tarefas, auditoria e notificacoes. Sem publicacao ou instalacao. | 14/11/2026 F3 autorizada: consulta read-only da projeccao publica separada, filtros por categoria/localizacao/preco/moeda, grelha/lista/mapa administrativo e CSV. Barreira efectiva fechada; apenas amostras sinteticas em QA isolado podem ser consumidas por injeccao de teste.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.","outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual. | QA: PrivateOfferPreparationTest.php; regressao 35 testes/548 assercoes e passagem final focada 16/172 (grupos sobrepostos, nao somar). outputs/offers-private-phase1-20261114/qa-result.json. | F2 QA: 35 testes/499 assercoes; final focado 11/160; complemento contribuidores 1/14 e HTML compilado 1/10 aprovados, grupos sobrepostos. tests/Feature/PrivateOfferEditorialTest.php; outputs/offers-private-phase2-20261114/qa-result.json. | F3 QA: tests/Feature/OfferPublicCatalogueTest.php; outputs/offers-public-phase3-20261114/qa-result.json; sem instalacao ou publicacao real.",Parcial - Ofertas F1/F2 aceites tecnicamente; F3 consulta implementada em QA com barreira fechada; visual/movel pendente; nao instalada,"Ofertas: publicacao efectiva depende dos mecanismos de confidencialidade aplicaveis; F3 visual/movel pendente. F4/F5, NDA, comprovativos, fotografias, validade e restantes ramos nao iniciados. Procuras permanece por implementar. Email real F2 pendente.",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 | F3: projecao publica anterior preservada durante edicao/rejeicao da nova revisao; sem contacto, anexos ou coordenadas do negocio; moeda sem conversao.",Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,Requisito transversal; consultar estado-ativo-after.json,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.",Localizar obrigação verificável e consumidores/contrato atuais antes de estimar implementação ou declarar conclusão. Critério individual original preservado; cobertura atual ainda não individualizada.,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer","Ler secção/versão correspondente; distinguir requisito funcional, decisão e exemplo. Fontes oficiais já aceites.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: governação e demais cláusulas. Esforço de código adicional só após lacuna reproduzida.,outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,governação e demais cláusulas,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",AGENTS.md — componentes do domínio encontrados; correspondência integral deste requisito não presumida,; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.,Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,, +RF-MN-003,"Criar, guardar, editar e submeter ofertas e procuras.",upgrade v4 SHA f0b9815…; p. 2965; requirements-audit.csv | portal do investidor\upgrade do portal de investimentos.docx; SHA 471012abb0f56377956f4ab822fe3186e2561bee0471508a5d807091f2e0154f; w:p 581 | portal do investidor\Portal V4\upgrade do portal de investimentos v6 programacao.docx; SHA e1169a7743b13ff6c950d3ffd4672a2afea32e81da84885e11154b7d8de9241e; w:p 3417 | portal do investidor\Portal V4\upgrade do portal de investimentos v1.docx; SHA f70ce3075523c6278e00dce15b9c7acf2b690679e16959ae5f7d007e754cfc6e; w:p 588 | portal do investidor\Portal V4\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 2222 | portal do investidor\Portal V4\upgrade do portal de investimentos v5 programacao.docx; SHA edece80c0519537244f8a8b86456af0aa92de1c526f791bb0ee9b85af7a7e8b2; w:p 3417,governação/outros,/api/v1/investment/admin/interests/{interest}/status; /management/history; /management/interests; /management/interests/{interest}; /management/interests/{interest}/consent/contact; /management/interests/{interest}/consent/proponent; /management/opportunities; /management/opportunities/create; /management/opportunities/drafts; /management/opportunities/{id}/draft; /management/opportunities/{id}/edit; /management/opportunities/{id}/lifecycle; /ofertas; /ofertas/{id}; /api/v1/offers; /api/v1/offers/{id}; /api/v1/offers/map; /api/v1/offers/export; /minhas-oportunidades/ofertas/{id}/recursos; /minhas-oportunidades/ofertas/{id}/confidencialidade; /minhas-oportunidades/ofertas/{id}/confidencial; /minhas-oportunidades/ofertas/{id}/nda,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual. | 14/11/2026 - Ofertas F1: oportunidades como cadastro principal; criar, guardar, recuperar, editar e submeter em privado; autorizacao existente, CRM e localizacao reutilizados; sem menu, publicacao ou instalacao. | 14/11/2026 F2 autorizada: circuito editorial privado no workflow e Dashboard existentes; revisao/atribuicao/rejeicao/correccao/reenvio/aprovacao interna; decisoes por snapshot/hash/ciclo, tarefas, auditoria e notificacoes. Sem publicacao ou instalacao. | 14/11/2026 F3 autorizada: consulta read-only da projeccao publica separada, filtros por categoria/localizacao/preco/moeda, grelha/lista/mapa administrativo e CSV. Barreira efectiva fechada; apenas amostras sinteticas em QA isolado podem ser consumidas por injeccao de teste. | 14/11/2026 F4 autorizada, campos propostos confirmados: particularidades opcionais de empresas/imoveis no mesmo rascunho; seis fotografias activas com edicao/retirada/restauro; originais cifrados e scan existente; comprovativos exclusivamente para validadores internos com ambito/MFA. NDA e autorizacao expressa do vendedor vinculados a versao/hash, revogacao e auditoria; exemplar juridico real pendente, configuracao nula. Sem publicacao ou instalacao.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.","outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual. | QA: PrivateOfferPreparationTest.php; regressao 35 testes/548 assercoes e passagem final focada 16/172 (grupos sobrepostos, nao somar). outputs/offers-private-phase1-20261114/qa-result.json. | F2 QA: 35 testes/499 assercoes; final focado 11/160; complemento contribuidores 1/14 e HTML compilado 1/10 aprovados, grupos sobrepostos. tests/Feature/PrivateOfferEditorialTest.php; outputs/offers-private-phase2-20261114/qa-result.json. | F3 QA: tests/Feature/OfferPublicCatalogueTest.php; outputs/offers-public-phase3-20261114/qa-result.json; sem instalacao ou publicacao real. | F4: tests/Feature/PrivateOfferConfidentialityTest.php; outputs/offers-private-phase4-20261114/qa-result.json. Cinco casos territoriais desbloqueados com fixture sintetico guardado em transaccao QA; sem dados operacionais.",Parcial - Ofertas F1/F2/F3 aceites tecnicamente; F4 implementada em QA com NDA real pendente; publicacao bloqueada; nao instalada,"NDA aprovado: exemplar, versao e referencia de aprovacao por disponibilizar. Tipos juridicos de comprovativos por identificar; sem certificacao de titularidade. Visual/movel e email real F2 pendentes. Derivadas: GD/WebP indisponivel no PHP QA. Publicacao efectiva bloqueada; F5 nao iniciada. Procuras permanece por implementar.",Autorizacao expressa para Fase 3 e para instalacao; designacoes operacionais e configuracao de entrega reais apenas quando aplicaveis. Sem novos papeis ou permissoes por inferencia.,"RF-MN-003: Rascunho persiste; submissão bloqueia campos essenciais e gera número de protocolo. | MASTER-471012ab-0052: Criar, guardar, editar e submeter ofertas e procuras. | MASTER-e1169a77-0202: Criar, guardar, editar e submeter ofertas e procuras. | MASTER-f70ce307-0058: Criar, guardar, editar e submeter ofertas e procuras. | MASTER-51b8f0ad-0138: Criar, guardar, editar e submeter ofertas e procuras. | MASTER-edece80c-0202: Criar, guardar, editar e submeter ofertas e procuras.",P2,RF-MN-003: Relatório 25/09 conservado; estado histórico não promove conclusão | MASTER-471012ab-0052: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-e1169a77-0202: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-f70ce307-0058: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-51b8f0ad-0138: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-edece80c-0202: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,RF-MN-003 | MASTER-471012ab-0052 | MASTER-e1169a77-0202 | MASTER-f70ce307-0058 | MASTER-51b8f0ad-0138 | MASTER-edece80c-0202,6,"[{""id"":""RF-MN-003"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""upgrade v4 SHA f0b9815…; p. 2965; requirements-audit.csv""],""aceitacao"":""Rascunho persiste; submissão bloqueia campos essenciais e gera número de protocolo."",""estado"":""parcial"",""variantes"":""Relatório 25/09 conservado; estado histórico não promove conclusão"",""modulo"":""governação/outros""},{""id"":""MASTER-471012ab-0052"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\upgrade do portal de investimentos.docx; SHA 471012abb0f56377956f4ab822fe3186e2561bee0471508a5d807091f2e0154f; w:p 581""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-e1169a77-0202"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v6 programacao.docx; SHA e1169a7743b13ff6c950d3ffd4672a2afea32e81da84885e11154b7d8de9241e; w:p 3417""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-f70ce307-0058"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v1.docx; SHA f70ce3075523c6278e00dce15b9c7acf2b690679e16959ae5f7d007e754cfc6e; w:p 588""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-51b8f0ad-0138"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 2222""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-edece80c-0202"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v5 programacao.docx; SHA edece80c0519537244f8a8b86456af0aa92de1c526f791bb0ee9b85af7a7e8b2; w:p 3417""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""}]","QA - preparacao, revisao e historico privados; filtros e acesso pelo Dashboard existente; visual manual pendente",QA - operacoes autenticadas /minhas-oportunidades/ofertas e /{id}/revisao; MFA/ambito/conflitos verificados no servidor; publicacao recusada,"QA - cadastro Oportunidades, tarefas/auditoria/notificacoes nativos; snapshots e hashes por ciclo; sem tabelas ou migracoes novas","QA - recuperar oferta, correccao, decisao e versoes anteriores; conflitos, rollback e indisponibilidade sem downgrade | F4: guardar/recuperar particularidades; fotografias e provas no recurso existente; historico de aceitacao/autorizacao/retirada/revogacao por exemplar e versao, sem substituir dados ou decisoes.","QA - aprovacao interna privada distinta de publicacao; versao publica nativa anterior preservada | F3: projecao publica anterior preservada durante edicao/rejeicao da nova revisao; sem contacto, anexos ou coordenadas do negocio; moeda sem conversao. | F4: dados empresariais reservados so depois de NDA + autorizacao expressa do vendedor; comprovativos nunca entregues ao interessado.",Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,Requisito transversal; consultar estado-ativo-after.json,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.",Localizar obrigação verificável e consumidores/contrato atuais antes de estimar implementação ou declarar conclusão. Critério individual original preservado; cobertura atual ainda não individualizada.,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer","Ler secção/versão correspondente; distinguir requisito funcional, decisão e exemplo. Fontes oficiais já aceites.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: governação e demais cláusulas. Esforço de código adicional só após lacuna reproduzida.,outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,governação e demais cláusulas,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",AGENTS.md — componentes do domínio encontrados; correspondência integral deste requisito não presumida,; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.,Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,, RF-MN-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-fase4-entrega-qa-20261114.md b/docs/investmoz/v2/auditoria-portal/ofertas-fase4-entrega-qa-20261114.md new file mode 100644 index 00000000..1e727ac3 --- /dev/null +++ b/docs/investmoz/v2/auditoria-portal/ofertas-fase4-entrega-qa-20261114.md @@ -0,0 +1,88 @@ +# Ofertas — Fase 4: particularidades e confidencialidade em QA + +14/11/2026. Base aceite `7691cab3`, preservando F1 `1f2990e4`, F2 `91c18573` e o plano `6e302db6`. Desenvolvimento exclusivo em QA autorizado pela Tempo. Os campos propostos no documento original foram confirmados nesta sessão como opcionais durante a preparação. A Tempo confirmou também que o NDA permanece pendente. + +**Implementado em QA, com dependência documental para o NDA real. Publicação efectiva bloqueada. Sem instalação, alterações de dados operacionais, migrações ou modificações em `mne`. Fase 5 não iniciada.** + +## Percursos implementados + +Na mesma preparação privada de Ofertas, o promotor guarda, recupera e altera: + +- **Venda de empresa:** sectores principal/secundário do cadastro existente; receita, lucro/fluxo de caixa, moeda independente, motivo da venda e disponibilidade declarada. Decimais são conservados como texto, incluindo fluxo de caixa negativo; não há conversão cambial. Estes dados não entram na projecção pública. +- **Imóvel:** venda/arrendamento, tipo do catálogo existente, áreas e unidade, quartos/casas de banho, características/comodidades e disponibilidade declarada. Os campos de quartos e casas de banho aparecem nos tipos habitacionais aplicáveis. A localização continua no selector existente. As categorias de direitos de terra e restantes produtos conservam os seus contratos; não se fundiu terreno com imóvel nem se inventou uma regra de titularidade. +- **Fotografias:** envio, legenda, declaração e descrição de edição digital, alteração de metadados/original, retirada e restauro lógico. Máximo de **seis fotografias activas**, verificado no servidor sob bloqueio da mesma Oferta, tanto na criação como no restauro. Os recursos nativos dos outros módulos mantêm o limite anterior. +- **Comprovativos imobiliários:** envio privado, estado de análise e consulta pelo validador interno autorizado. O anunciante recebe o estado do envio, mas não recebe uma ligação de leitura do comprovativo no editor. O suporte enviado não é apresentado como título certificado nem aprova o negócio. + +Os ficheiros usam `OpportunityResources`, `documents`, ligações e histórico existentes, sem outro armazenamento documental. Originais são cifrados no disco privado `local`, com identificadores gerados pelo servidor, checksum e quarentena. Reutilizam-se validação MIME/conteúdo, limites de 10 MB para fotografia/20 MB para documento, mínimo de 400 × 400 píxeis e rejeição de imagens animadas/executáveis. Os formatos documentais são limites técnicos do serviço existente; não constituem uma lista jurídica de títulos admitidos. O scanner existente decide a disponibilidade. Nenhum envio passa automaticamente a limpo ou a publicado. + +## Comprovativos e caminhos alternativos + +A autorização comum de binários (`VerifiedDocumentBinary`) verifica também os comprovativos de Ofertas. Exige papel existente de validador com âmbito no registo, acesso efectivo, MFA recente e ausência de conflito; o promotor, auditor, outra categoria ou sessão sem MFA não obtêm os bytes. A verificação ocorre também nos consumidores gerais que reutilizam esse serviço, antes de qualquer entrega. Regista documento, hash, actor e versão da Oferta, sem incluir o original na auditoria. + +As rotas nativas de edição de Ofertas que já estavam bloqueadas mantêm a recusa. A associação de originais públicos da biblioteca a uma Oferta é recusada neste percurso: um comprovativo reservado não é convertido em documento público por essa associação. Fotografias e localizações desta preparação só admitem visibilidade interna. Os comprovativos só admitem classificação restrita. + +## Duas condições independentes para dados da empresa + +O novo adaptador usa a mesma Oportunidade e a autorização existente. Não cria cadastro de empresas, utilizadores, permissões ou dashboard. A configuração `private_offers.nda` está **nula**; não contém texto jurídico nem exemplar fictício. A sua futura referência exige um documento existente com versão, SHA256 e referência da aprovação da Tempo. Estado, scan, versão e integridade dos bytes são reconferidos pelo serviço comum. Não se reutilizou a designação jurídica da legislação como aprovação automática de NDA. + +Com um exemplar efectivamente aprovado, o percurso técnico é: + +1. Interessado autenticado e com email verificado consulta o exemplar exacto e aceita-o expressamente para o hash da versão aprovada da empresa. Não é exigida uma justificação para aceitar ou retirar a aceitação. +2. O vendedor registado (dono da Oferta), com atribuição de promotor e representação ainda efectivas, autoriza expressamente aquele interessado e **aquela aceitação**. Esta autorização no portal não certifica titularidade sobre a empresa. Um validador não substitui o vendedor. Autorizações e revogações conservam fundamento. +3. Em cada consulta, o servidor reconfirma as duas condições, o exemplar/versão do NDA, a aprovação editorial actual, o hash dos dados, a identidade do vendedor, os direitos efectivos e a revisão de acesso do interessado. +4. O interessado recebe apenas os campos empresariais da submissão aprovada. Não recebe a descrição privada integral, contactos, morada exacta, comprovativos, caminhos de armazenamento ou ficheiros originais. As fotografias continuam no percurso privado do promotor e da equipa; não se acrescentou partilha externa de originais. +5. Revogar a autorização ou retirar a aceitação fecha o acesso seguinte. A retirada de uma aceitação anterior continua disponível enquanto a Oferta está em revisão, sem abrir os dados dessa revisão. Uma aceitação nova elimina a autorização anterior: exige nova decisão do vendedor. Mudar o NDA, a Oferta, os seus suportes, as referências abrangidas ou os direitos efectivos impede a reutilização silenciosa do acesso anterior. + +Aceitações, autorizações, retiradas e revogações ficam na história da Oportunidade, com antes/depois, actor, instante, hash da Oferta e identidade/versão/checksum do NDA. Os fundamentos ficam cifrados. A auditoria existente regista operações e consultas autorizadas/recusadas, sem valores financeiros. O vendedor consulta as suas decisões; o interessado vê apenas as ocorrências que lhe respeitam. As respostas privadas mantêm `no-store`. Uma revogação não apaga informação que o interessado já tenha legitimamente recebido; não foi prometida recolha retroactiva de bytes. + +## Continuidade editorial e protecção pública + +Os campos específicos e os suportes activos são incorporados no snapshot editorial `private-offer-editorial-v3`. Hashes incluem identidades e versões dos recursos, conteúdo/metadados e checksums; alterações exigem nova submissão/decisão. Ofertas anteriores sem extensão mantêm o contrato v2. Identificadores, originais, rejeições, aprovações e histórico não são substituídos. Um suporte que volte a quarentena sinaliza aprovação desactualizada, conservando a possibilidade de recuperar e preparar nova revisão. + +A projecção pública da Fase 3 e os bloqueios do modelo/workflow não foram abertos. Não existe publicador, pagamentos ou destaque comercial activo. Não se acrescentaram campos sensíveis a pesquisas, APIs públicas, mapas, CSV, notificações ou email. As provas anteriores da Fase 2 para notificações sem conteúdo privado são reutilizadas; a entrega real de email permanece pendente. + +## Cinco testes territoriais antes bloqueados + +A causa estava em `OpportunitySearchFixture`: exigia uma referência pública de Inhambane e o sector turismo previamente importados, apesar de o ensaio usar uma base de testes vazia. O produto não perdeu os dados territoriais. O fixture passa a criar os mínimos sintéticos apenas quando ausentes, **depois da guarda de ambiente testing/loopback/base `_test`/porta 55441 e dentro de uma transacção de rollback**. Fora dessa combinação recusa a criação. Não foi executada importação, reposição ou alteração no cadastro operacional. + +Os seis casos de `OpportunitySearchTest`, incluindo os cinco impedidos, passaram. Não se alteraram as suas asserções ou os filtros do produto para obter aprovação. + +## Ficheiros alterados + +| Grupo | Finalidade | +| --- | --- | +| `PrivateOfferDetails`, `PrivateOfferPreparation` | Campos específicos e snapshots/versionamento existentes | +| `PrivateOfferConfidentiality`, `config/private_offers.php` | Duas condições, revogação, versões e auditoria; NDA inactivo | +| `OpportunityResources`, `OpportunityLibrarySupport`, `VerifiedDocumentBinary` | Limite de seis, metadados, visibilidade e protecção central dos comprovativos | +| `PrivateOfferPreparationController`, `routes/web.php` | Operações privadas no percurso existente | +| `offer-particulars`, `offer-private-resources`, `offer-confidential-access`, páginas `offers/preparation` e `offers/confidentiality` | Preparação progressiva, recursos e controlo privado de acesso | +| `PrivateOfferConfidentialityTest`, `OpportunitySearchFixture` | Segurança/isolamento e independência das importações operacionais | +| Matriz RF-MN-003, manual e recibo existentes | Estado, testes e dependências concretas | + +## Verificação e saldo + +Resultados finais, comandos, base isolada e hashes do delta: `outputs/offers-private-phase4-20261114/qa-result.json`. Grupos de testes sobrepostos não devem ser somados. A passagem alargada intermédia teve 48/50 aprovados; uma falha expôs uma referência de sessão desactualizada no cenário de revogação e a outra resultou de reconstruir assets durante a prova HTML. Foram corrigidas as causas e as verificações afectadas foram repetidas com o build estabilizado. Não se enfraqueceram asserções, permissões, scanner ou guardas. + +| Verificação | Resultado | +| --- | --- | +| Passagem F4 e HTML das fases afectadas | 13 testes / 174 asserções aprovados | +| Delta final: retirada durante revisão, versões NDA e revogação de sessão | 1 teste / 35 asserções aprovados | +| HTML privado, público e de confidencialidade com o último build estabilizado | 3 testes / 29 asserções aprovados | +| Regressão da preparação privada F1 | 14 testes / 146 asserções aprovados | +| Regressão editorial F2 | 11 testes / 161 asserções aprovados | +| Pesquisa territorial, incluindo os cinco casos antes impedidos | 6 testes / 112 asserções aprovados | +| Recursos nativos: binários, uploads, autorização e processamento condicionado | 5 testes / 69 asserções aprovados | +| Pint, PHPStan, TypeScript, lint/formatação focados e build QA | Aprovados; PHPStan sem erros | +| Formatação global | Os mesmos 61 ficheiros, fora do delta e com conteúdo Git inalterado | + +As regressões das Fases 1/2 e da pesquisa foram extraídas da passagem alargada; os cinco casos de recursos constam da passagem focada de 16 testes/217 asserções. O complemento final cobre a alteração de retirada sem repetir os restantes casos aprovados. O caso adicional de suporte novamente em quarentena teve a expectativa ajustada ao contrato **já existente** de reabertura (`needs_information`, editável), preservando o produto. + +**Pendências concretas:** + +- Exemplar, versão e referência da aprovação do NDA da Tempo. A ausência foi confirmada pelo proprietário; sem estes elementos o acesso real continua fechado. Os testes usam bytes explicitamente marcados como sintéticos e não jurídicos, por injecção local ao teste. +- Identificação jurídica dos comprovativos imobiliários admitidos. Implementaram-se segurança e revisão interna, sem fabricar um tipo de título ou regra de propriedade. +- Conferência visual desktop/móvel das fases aceites, ainda não concluída. O conector registou `The browser access host is unavailable`; não foram alterados navegadores ou controlos de segurança. +- GD não está instalado neste PHP QA. Uploads, validação estrutural, armazenamento e autorização foram provados com PNGs válidos sem GD. O processador nativo mantém a geração de derivadas bloqueada quando GD/WebP não está disponível; não se declarou processamento de galerias públicas demonstrado. +- A formatação global mantém os 61 ficheiros preexistentes fora do delta. Email real F2 e a abertura efectiva de publicação continuam com o seu estado anterior. + +Próxima tarefa: apreciação desta entrega QA e disponibilização do exemplar aprovado do NDA, mantendo a publicação fechada. Qualquer instalação ou avanço para a Fase 5 exige autorização própria. diff --git a/outputs/offers-private-phase4-20261114/final.log b/outputs/offers-private-phase4-20261114/final.log new file mode 100644 index 00000000..79c2a19f --- /dev/null +++ b/outputs/offers-private-phase4-20261114/final.log @@ -0,0 +1 @@ +{"tool":"phpunit","result":"passed","tests":13,"passed":13,"assertions":174,"duration_ms":131026} diff --git a/outputs/offers-private-phase4-20261114/final.xml b/outputs/offers-private-phase4-20261114/final.xml new file mode 100644 index 00000000..280c96e4 --- /dev/null +++ b/outputs/offers-private-phase4-20261114/final.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/outputs/offers-private-phase4-20261114/focused.log b/outputs/offers-private-phase4-20261114/focused.log new file mode 100644 index 00000000..31b19b2a --- /dev/null +++ b/outputs/offers-private-phase4-20261114/focused.log @@ -0,0 +1 @@ +{"tool":"phpunit","result":"passed","tests":16,"passed":16,"assertions":217,"duration_ms":133539} diff --git a/outputs/offers-private-phase4-20261114/focused.xml b/outputs/offers-private-phase4-20261114/focused.xml new file mode 100644 index 00000000..52972ba6 --- /dev/null +++ b/outputs/offers-private-phase4-20261114/focused.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/outputs/offers-private-phase4-20261114/html-latest.log b/outputs/offers-private-phase4-20261114/html-latest.log new file mode 100644 index 00000000..1965e2be --- /dev/null +++ b/outputs/offers-private-phase4-20261114/html-latest.log @@ -0,0 +1 @@ +{"tool":"phpunit","result":"passed","tests":3,"passed":3,"assertions":29,"duration_ms":15844} diff --git a/outputs/offers-private-phase4-20261114/html-latest.xml b/outputs/offers-private-phase4-20261114/html-latest.xml new file mode 100644 index 00000000..ddbfa489 --- /dev/null +++ b/outputs/offers-private-phase4-20261114/html-latest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/outputs/offers-private-phase4-20261114/qa-result.json b/outputs/offers-private-phase4-20261114/qa-result.json new file mode 100644 index 00000000..32895349 --- /dev/null +++ b/outputs/offers-private-phase4-20261114/qa-result.json @@ -0,0 +1,184 @@ +{ + "phase": 4, + "base": "7691cab3bc3ab8215c6feab1a2396119f07a55b5", + "state": "implemented in QA; real NDA and visual review pending; publication blocked", + "authorization": { + "qa_only": true, + "optional_proposed_fields_confirmed": true, + "real_nda_pending_confirmed": true + }, + "database": { + "name": "offers_phase1_20261114_test", + "port": 55441, + "host": "127.0.0.1", + "test_data": "synthetic with rollback" + }, + "verified_runs": { + "final.log": { + "tool": "phpunit", + "result": "passed", + "tests": 13, + "passed": 13, + "assertions": 174, + "duration_ms": 131026 + }, + "withdrawal-latest.log": { + "tool": "phpunit", + "result": "passed", + "tests": 1, + "passed": 1, + "assertions": 35, + "duration_ms": 27438 + }, + "html-latest.log": { + "tool": "phpunit", + "result": "passed", + "tests": 3, + "passed": 3, + "assertions": 29, + "duration_ms": 15844 + }, + "focused.log": { + "tool": "phpunit", + "result": "passed", + "tests": 16, + "passed": 16, + "assertions": 217, + "duration_ms": 133539 + } + }, + "accepted_regressions": { + "PrivateOfferPreparationTest": { + "tests": 14, + "assertions": 146, + "errors": 0, + "failures": 0, + "evidence": "regression.xml" + }, + "PrivateOfferEditorialTest": { + "tests": 11, + "assertions": 161, + "errors": 0, + "failures": 0, + "evidence": "regression.xml" + }, + "OpportunitySearchTest": { + "tests": 6, + "assertions": 112, + "errors": 0, + "failures": 0, + "evidence": "regression.xml" + }, + "OpportunityResourcesTest": { + "tests": 5, + "assertions": 69, + "errors": 0, + "failures": 0, + "evidence": "focused.xml" + } + }, + "intermediate_run": { + "tests": 50, + "passed": 48, + "assertions": 663, + "failures": 2, + "resolution": [ + "fresh access revision read from database and fresh synthetic actor", + "HTML checked after build stabilized; no concurrent rebuild" + ] + }, + "checks": { + "pint": "passed", + "phpstan": "passed; zero errors", + "typescript": "passed", + "frontend_focused": "five files passed", + "build": "passed; build-latest.log", + "git_diff_check": "passed", + "global_format": { + "failed_files": 61, + "outside_delta": true, + "unchanged_git_content": true, + "same_list_as_accepted_baseline": true + } + }, + "limits": { + "photos_active_per_offer": 6, + "original_images_kib": 10240, + "documents_kib": 20480, + "image_min_pixels": [ + 400, + 400 + ] + }, + "confidentiality": { + "nda_configuration": null, + "publication_enabled": false, + "read_requires": [ + "current approved offer hash", + "verified exact NDA bytes, version and approval reference", + "interested acceptance", + "express decision by registered offer owner", + "current seller and interested permissions/access revision" + ], + "proof_reader": "existing scoped internal validator with MFA and no conflict", + "external_disclosure": "approved business fields only, no originals/contacts/exact coordinates/proof", + "history": "offer context, versioned before/after; existing audit; encrypted reasons" + }, + "territorial_tests": { + "cause": "fixture depended on pre-existing imports in empty isolated database", + "resolved_cases": 6, + "previously_blocked": 5, + "guard": "verified testing loopback *_test:55441 plus active rollback transaction", + "operational_data_changed": false + }, + "pending": [ + "real approved NDA exemplar/version/approval reference", + "legal identification of acceptable property evidence; no title certification", + "desktop/mobile visual acceptance: browser access host unavailable", + "GD/WebP image derivatives not demonstrated in this PHP environment", + "real email delivery from phase two", + "61 pre-existing formatting failures" + ], + "installed": false, + "operational_data_modified": false, + "migrations_added": false, + "mne_modified": false, + "phase5_started": false, + "new_roles_or_authentication": false, + "payments_or_paid_highlights": false, + "source_hash_encoding": "SHA256 of UTF-8 files with Git LF normalization", + "source_sha256": { + "app/Http/Controllers/PrivateOfferPreparationController.php": "b684b14f85bd569c09dbd5f16d6e7f210c1045637d1e4c4c9229ffe124937e6c", + "app/Services/OpportunityLibrarySupport.php": "26a08e3a358fe97f2609e275af72dd7e70ba777d2ad9ea791a3be2c1a4c08853", + "app/Services/OpportunityResources.php": "e8bcd6f4f5b994b466005aace3601fcfc642550d6ebda227c6de7a498f95f1b5", + "app/Services/PrivateOfferConfidentiality.php": "95bc76b9beb72924a4efdcbea6e8d36670f1ff15dac02baf0f761f489837e10b", + "app/Services/PrivateOfferDetails.php": "1253c5d3cb9e3a2ae422f813afc4eb5ee8ed3859b07e15a3b32b678d3a97a015", + "app/Services/PrivateOfferPreparation.php": "7b277a049442f90fcb15e913b43c551f2ba3716d54cfcfdcaf0b621fbf367a18", + "app/Services/VerifiedDocumentBinary.php": "9a03b62643e767ef7d23da9faf235c491629d61201755ff8391b5cc7f534aaeb", + "config/private_offers.php": "8414d6ffb362d5590d075a9c4205cd700f60b99f0cb5bd7e7f925b8638c60531", + "docs/investmoz/unification/traceability-and-manual.md": "9cb4949a9279e3132a60069ff3cea21c12a4d5c90b8e44f0c1ed68428a07d656", + "docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv": "c4715f857e750a4d11438284ee2fe7592af3d16b72862ba947f4539b5cb3fd35", + "docs/investmoz/v2/auditoria-portal/ofertas-fase4-entrega-qa-20261114.md": "b4a5a8f06cb650885eb5af4de7e80d322d94f51af43509f893740bf5830fda61", + "resources/js/components/offer-confidential-access.tsx": "5723e5e8eda7500fe24670e7676749a4f36ad294bd284224cf7b6919675bddc1", + "resources/js/components/offer-particulars.tsx": "ef78a2a493705b6f4828addd0227fc7e91f511a48b2c7b12fbbe35c610fe5de0", + "resources/js/components/offer-private-resources.tsx": "cb2070aeb4fd8ebe4eac28b41501223d2a87a2b1213538ab66c9520ab244d9c9", + "resources/js/pages/offers/confidentiality.tsx": "b31d2cd72b55eaa9d04a3e7ff03fcb0d4a18696c403f89f5b791f9dc050c4d68", + "resources/js/pages/offers/preparation.tsx": "238cf223eb88f2725b65c3700b977dd984e190a5e890f7b208e40fd34dcf1d8d", + "routes/web.php": "1291c0ef1d7cfdef16c83f6444df62b03eff28e3193bb47cf21c1aac1aa51b13", + "tests/Feature/PrivateOfferConfidentialityTest.php": "77eb38a9512914cea5e2554e00ef960abb3c861f6a3037a7504ca73db9c39161", + "tests/Support/OpportunitySearchFixture.php": "1bab1218a1122f6a7713d1a256f8576f32dc03a2eff08601a1e356b840681364" + }, + "evidence_sha256": { + "final.log": "3a6a9976cabe1820641845e1546f626347636e999909e0a88e79798960a445f5", + "final.xml": "38f4a99af98fb595dce559bc4a9e9bc0c06a4b6f92ec7b687631b8284aeacc28", + "withdrawal-latest.log": "d01eff68d96bc8fe73d564e2aafc357a692676b4adeaf685a29c3221612b0a81", + "withdrawal-latest.xml": "a51078aafee220a4134b20d3e864b59d54c96e95508d5fa5c9ceb267104bfafd", + "html-latest.log": "99d084019996a9b8a3079e281c160e10a81afde635ba8e09011106579d955447", + "html-latest.xml": "923911794057826eb12f4e0074ec283a9062d49037065ac3059d6b225dfe5c3c", + "focused.log": "1e0789c1219d42f1d8f1952b64fdad6354261ec252826e79b8d35bd0db2c278d", + "focused.xml": "a497520ddc934ac650f08ceb9d20f84dc32a9da8d0ec40e8bb99b3605aba10eb", + "regression.xml": "2809cfa01d935ad0e332791dbc74be947ecf4c1ccfad03bbd52f633cdb8d8ce7" + }, + "report": "docs/investmoz/v2/auditoria-portal/ofertas-fase4-entrega-qa-20261114.md", + "overlapping_runs_not_summed": true +} diff --git a/outputs/offers-private-phase4-20261114/regression.xml b/outputs/offers-private-phase4-20261114/regression.xml new file mode 100644 index 00000000..126726d1 --- /dev/null +++ b/outputs/offers-private-phase4-20261114/regression.xml @@ -0,0 +1,333 @@ + + + + + + + + + + + + + Tests\Feature\PrivateOfferConfidentialityTest::test_nda_version_changes_withdrawal_session_revocation_and_private_revision_close_access +Expected response status code [200] but received 409. +Failed asserting that 409 is identical to 200. + +C:\laragon\www\portal-investidor-qa-editorial-20261113\vendor\laravel\framework\src\Illuminate\Testing\TestResponseAssert.php:46 +C:\laragon\www\portal-investidor-qa-editorial-20261113\vendor\laravel\framework\src\Illuminate\Testing\TestResponse.php:183 +C:\laragon\www\portal-investidor-qa-editorial-20261113\vendor\laravel\framework\src\Illuminate\Testing\Concerns\AssertsStatusCodes.php:16 +C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php:254 +C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php:270 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tests\Feature\OfferPublicCatalogueTest::test_public_html_resolves_the_real_compiled_pages_without_private_fields +Failed asserting that '<!DOCTYPE html>\n +<html lang="pt" class="">\n + <head>\n + <meta charset="utf-8">\n + <meta name="viewport" content="width=device-width, initial-scale=1">\n +\n + \n + <script>\n + (function() {\n + const appearance = 'system';\n +\n + if (appearance === 'system') {\n + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;\n +\n + if (prefersDark) {\n + document.documentElement.classList.add('dark');\n + }\n + }\n + })();\n + </script>\n +\n + \n + <style>\n + html {\n + background-color: oklch(1 0 0);\n + }\n +\n + html.dark {\n + background-color: oklch(0.145 0 0);\n + }\n + </style>\n +\n + <link rel="icon" href="/favicon.ico" sizes="any">\n + <link rel="icon" href="/favicon.svg" type="image/svg+xml">\n + <link rel="apple-touch-icon" href="/apple-touch-icon.png">\n +\n + <link rel="preload" as="font" href="http://localhost/build-qa-opportunities/assets/instrument-sans-400-normal-DRC__1Mx.woff2" type="font/woff2" crossorigin="anonymous" />\n +<link rel="preload" as="font" href="http://localhost/build-qa-opportunities/assets/instrument-sans-500-normal-Dk9ku72i.woff2" type="font/woff2" crossorigin="anonymous" />\n +<link rel="preload" as="font" href="http://localhost/build-qa-opportunities/assets/instrument-sans-600-normal-B7fBEWYG.woff2" type="font/woff2" crossorigin="anonymous" />\n +<link rel="preload" as="font" href="http://localhost/build-qa-opportunities/assets/noto-sans-400-normal-BTkUljjl.woff2" type="font/woff2" crossorigin="anonymous" />\n +<link rel="preload" as="font" href="http://localhost/build-qa-opportunities/assets/noto-sans-500-normal-DCA2Z9Cj.woff2" type="font/woff2" crossorigin="anonymous" />\n +<link rel="preload" as="font" href="http://localhost/build-qa-opportunities/assets/noto-sans-600-normal-Dkgu5AE5.woff2" type="font/woff2" crossorigin="anonymous" />\n +<link rel="preload" as="font" href="http://localhost/build-qa-opportunities/assets/noto-sans-700-normal-CrvTEWhy.woff2" type="font/woff2" crossorigin="anonymous" />\n +<link rel="preload" as="font" href="http://localhost/build-qa-opportunities/assets/noto-serif-400-normal-BV_iYGc9.woff2" type="font/woff2" crossorigin="anonymous" />\n +<link rel="preload" as="font" href="http://localhost/build-qa-opportunities/assets/noto-serif-600-normal-BiuDA9L7.woff2" type="font/woff2" crossorigin="anonymous" />\n +<link rel="preload" as="font" href="http://localhost/build-qa-opportunities/assets/noto-serif-700-normal-CIw17UNa.woff2" type="font/woff2" crossorigin="anonymous" />\n +<style>\n +@font-face {\n + font-family: "Instrument Sans";\n + font-style: normal;\n + font-weight: 400;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/instrument-sans-400-normal-DRC__1Mx.woff2") format("woff2");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Instrument Sans";\n + font-style: normal;\n + font-weight: 400;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/instrument-sans-400-normal-D1W7dsQl.woff") format("woff");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Instrument Sans";\n + font-style: normal;\n + font-weight: 500;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/instrument-sans-500-normal-Dk9ku72i.woff2") format("woff2");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Instrument Sans";\n + font-style: normal;\n + font-weight: 500;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/instrument-sans-500-normal-Z6ESRlEs.woff") format("woff");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Instrument Sans";\n + font-style: normal;\n + font-weight: 600;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/instrument-sans-600-normal-B7fBEWYG.woff2") format("woff2");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Instrument Sans";\n + font-style: normal;\n + font-weight: 600;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/instrument-sans-600-normal-B9e8oLYv.woff") format("woff");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Noto Sans";\n + font-style: normal;\n + font-weight: 400;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/noto-sans-400-normal-BTkUljjl.woff2") format("woff2");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Noto Sans";\n + font-style: normal;\n + font-weight: 400;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/noto-sans-400-normal-DPpKfCgK.woff") format("woff");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Noto Sans";\n + font-style: normal;\n + font-weight: 500;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/noto-sans-500-normal-DCA2Z9Cj.woff2") format("woff2");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Noto Sans";\n + font-style: normal;\n + font-weight: 500;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/noto-sans-500-normal-CDPngY5C.woff") format("woff");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Noto Sans";\n + font-style: normal;\n + font-weight: 600;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/noto-sans-600-normal-Dkgu5AE5.woff2") format("woff2");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Noto Sans";\n + font-style: normal;\n + font-weight: 600;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/noto-sans-600-normal-DVJMj_t0.woff") format("woff");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Noto Sans";\n + font-style: normal;\n + font-weight: 700;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/noto-sans-700-normal-CrvTEWhy.woff2") format("woff2");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Noto Sans";\n + font-style: normal;\n + font-weight: 700;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/noto-sans-700-normal-ZyrKABVF.woff") format("woff");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Noto Serif";\n + font-style: normal;\n + font-weight: 400;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/noto-serif-400-normal-BV_iYGc9.woff2") format("woff2");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Noto Serif";\n + font-style: normal;\n + font-weight: 400;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/noto-serif-400-normal-DT0cwSa_.woff") format("woff");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Noto Serif";\n + font-style: normal;\n + font-weight: 600;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/noto-serif-600-normal-BiuDA9L7.woff2") format("woff2");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Noto Serif";\n + font-style: normal;\n + font-weight: 600;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/noto-serif-600-normal-RZopCp-J.woff") format("woff");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Noto Serif";\n + font-style: normal;\n + font-weight: 700;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/noto-serif-700-normal-CIw17UNa.woff2") format("woff2");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +@font-face {\n + font-family: "Noto Serif";\n + font-style: normal;\n + font-weight: 700;\n + font-display: swap;\n + src: url("/build-qa-opportunities/assets/noto-serif-700-normal-mX-AlFMT.woff") format("woff");\n + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n +}\n +\n +:root {\n + --font-instrument-sans: "Instrument Sans";\n + --font-noto-sans: "Noto Sans";\n + --font-noto-serif: "Noto Serif";\n +}\n +\n +.font-instrument-sans {\n + font-family: var(--font-instrument-sans);\n +}\n +\n +.font-noto-sans {\n + font-family: var(--font-noto-sans);\n +}\n +\n +.font-noto-serif {\n + font-family: var(--font-noto-serif);\n +}\n +</style>\n + <link rel="preload" as="style" href="http://localhost/build-qa-opportunities/assets/app-UznX-FqS.css" /><link rel="preload" as="style" href="http://localhost/build-qa-opportunities/assets/interactive-investment-map-Bm3WjoQm.css" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/app-BPqa0WnF.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/rolldown-runtime-Dd_uD5pT.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/dist-BRfsww6z.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/jsx-runtime-BpzPEenQ.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/compiler-runtime-sKsOXjDo.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/dist-Ddn_hEUd.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/dist-BobFCjoG.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/dist-qw3FPjRO.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/Combination-BBDI-RbM.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/utils-CCDJ05Ll.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/createLucideIcon-oMeAlXbs.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/wayfinder-Dp7PqxvV.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/catalogue-dPWuPV9C.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/search-state-RHLy6yI7.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/shared-dS3P3an9.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/opportunity-search-filters-D6l6oeyA.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/search-BRvPeo47.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/sliders-horizontal-D-4FqAHA.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/investment-territory-Cnp2648A.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/territory-geometry-preview-BSCKCw5m.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/interactive-investment-map-soic1493.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/catalogue-investment-map-DSAPtwWw.js" /><link rel="modulepreload" as="script" href="http://localhost/build-qa-opportunities/assets/public-offer-card-Byw0miCA.js" /><link rel="stylesheet" href="http://localhost/build-qa-opportunities/assets/app-UznX-FqS.css" /><link rel="stylesheet" href="http://localhost/build-qa-opportunities/assets/interactive-investment-map-Bm3WjoQm.css" /><script type="module" src="http://localhost/build-qa-opportunities/assets/app-BPqa0WnF.js"></script><script type="module" src="http://localhost/build-qa-opportunities/assets/catalogue-dPWuPV9C.js"></script> <title>Laravel</title>\n + </head>\n + <body class="font-sans antialiased">\n + <script data-page="app" type="application/json">{"component":"offers\/catalogue","props":{"errors":{},"name":"Laravel","administrationNavigation":[],"dashboardNavigation":[],"procurementEnabled":false,"fundingEnabled":true,"homologationReserved":false,"profileAccess":null,"contentManagementHref":null,"contentManagementAccess":[],"portalOperator":"Tempo, Lda.","portalInstitutionalContactEmail":"comercial@tempo.co.mz","legacyContentEnabled":false,"educationEnabled":false,"aiEnabled":false,"existingBusinessEnabled":false,"businessReviewUrl":null,"investorPathEnabled":false,"opportunityManagement":null,"flash":{"success":null,"warning":null,"uploaded_document_id":null},"scopedAdministration":[],"auth":{"user":null,"canManagePortal":false,"legacyManagementAccess":false},"sidebarOpen":true,"results":{"current_page":1,"data":[{"id":51,"title":"Synthetic public offer","summary":"Approved public summary.","category":"business_sale","location":[],"price":{"mode":"amount","amount":"12345.67","currency":"MZN"},"status":"published","published_at":"2026-11-13T15:10:01+00:00","revision":1,"url":"\/ofertas\/51"}],"first_page_url":"http:\/\/localhost\/ofertas?page=1","from":1,"last_page":1,"last_page_url":"http:\/\/localhost\/ofertas?page=1","links":[{"url":null,"label":"&laquo; Previous","page":null,"active":false},{"url":"http:\/\/localhost\/ofertas?page=1","label":"1","page":1,"active":true},{"url":null,"label":"Next &raquo;","page":null,"active":false}],"next_page_url":null,"path":"http:\/\/localhost\/ofertas","per_page":12,"prev_page_url":null,"to":1,"total":1},"truncated":false,"filters":[],"mapData":null,"territorySelection":[],"references":{"categories":["investment_project","real_estate","land_rights","business_sale","business_partnership","franchise","concession","financing","supply","other"],"options":[],"clusters":[],"corridors":[],"projects":[],"locations":[],"sectors":[]}},"url":"\/ofertas","version":"da4443275cc023299b6be2756e75d511","sharedProps":["errors","name","administrationNavigation","dashboardNavigation","procurementEnabled","fundingEnabled","homologationReserved","profileAccess","contentManagementHref","contentManagementAccess","portalOperator","portalInstitutionalContactEmail","legacyContentEnabled","educationEnabled","aiEnabled","existingBusinessEnabled","businessReviewUrl","investorPathEnabled","opportunityManagement","flash","scopedAdministration","auth","sidebarOpen"]}</script><div id="app"></div>\n + </body>\n +</html>\n +' [ASCII](length: 16604) contains "assets/catalogue-QoLYHhHv.js" [ASCII](length: 28). + +C:\laragon\www\portal-investidor-qa-editorial-20261113\vendor\laravel\framework\src\Illuminate\Testing\TestResponseAssert.php:46 +C:\laragon\www\portal-investidor-qa-editorial-20261113\vendor\laravel\framework\src\Illuminate\Testing\TestResponse.php:720 +C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferPublicCatalogueTest.php:198 + + + + + + + + + + + + + + diff --git a/outputs/offers-private-phase4-20261114/withdrawal-latest.log b/outputs/offers-private-phase4-20261114/withdrawal-latest.log new file mode 100644 index 00000000..31bce5c3 --- /dev/null +++ b/outputs/offers-private-phase4-20261114/withdrawal-latest.log @@ -0,0 +1 @@ +{"tool":"phpunit","result":"passed","tests":1,"passed":1,"assertions":35,"duration_ms":27438} diff --git a/outputs/offers-private-phase4-20261114/withdrawal-latest.xml b/outputs/offers-private-phase4-20261114/withdrawal-latest.xml new file mode 100644 index 00000000..0b1821be --- /dev/null +++ b/outputs/offers-private-phase4-20261114/withdrawal-latest.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/js/components/offer-confidential-access.tsx b/resources/js/components/offer-confidential-access.tsx new file mode 100644 index 00000000..103264e7 --- /dev/null +++ b/resources/js/components/offer-confidential-access.tsx @@ -0,0 +1,279 @@ +import { useEffect, useState } from 'react'; +import { profileJson } from '@/lib/profile-json'; +type Acceptance = { id: string; actor_id: number; version_hash: string }; +type Grant = { interested_user_id: number; version_hash: string }; +type Overview = { + seller: boolean; + nda_pending: boolean; + can_accept: boolean; + can_authorize: boolean; + version_hash: string | null; + nda: { sha256: string; version: number; fingerprint: string } | null; + requests: Acceptance[]; + grants: Grant[]; + acceptance: Acceptance | null; + etag: string | null; + history: { id: string; action: string; at: string; version_hash: string }[]; +}; +export default function OfferConfidentialAccess({ + id, + onChanged, +}: { + id: number; + onChanged?: (etag: string) => void; +}) { + const [overview, setOverview] = useState(null); + const [confirm, setConfirm] = useState(false); + const [reason, setReason] = useState(''); + const [message, setMessage] = useState(''); + const [busy, setBusy] = useState(false); + const [business, setBusiness] = useState | null>(null); + const base = `/minhas-oportunidades/ofertas/${id}`; + useEffect(() => { + let active = true; + profileJson(`${base}/confidencialidade`, {}, 'GET') + .then((result) => { + if (active) setOverview(result); + }) + .catch((error) => { + if (active) setMessage(error.message); + }); + return () => { + active = false; + }; + }, [base]); + async function act(action: string, target?: Acceptance | Grant) { + if (!overview || !confirm || busy) return; + setBusy(true); + setBusiness(null); + try { + await profileJson(`${base}/confidencialidade`, { + action, + confirm, + reason, + version_hash: target?.version_hash ?? overview.version_hash, + nda_sha256: overview.nda?.sha256, + nda_fingerprint: overview.nda?.fingerprint, + interested_user_id: + target && + ('actor_id' in target + ? target.actor_id + : target.interested_user_id), + acceptance_id: target && 'id' in target ? target.id : undefined, + }); + const next = await profileJson( + `${base}/confidencialidade`, + {}, + 'GET', + ); + setOverview(next); + if (typeof next.etag === 'string') onChanged?.(next.etag); + setConfirm(false); + setMessage('Decisão registada para a versão indicada.'); + } catch (error) { + setMessage( + error instanceof Error + ? error.message + : 'Não foi possível registar.', + ); + } finally { + setBusy(false); + } + } + async function read() { + setBusy(true); + setBusiness(null); + try { + const result = await profileJson(`${base}/confidencial`, {}, 'GET'); + setBusiness(result.business); + setMessage('Acesso autorizado e registado para esta versão.'); + } catch (error) { + setMessage( + error instanceof Error ? error.message : 'Acesso indisponível.', + ); + } finally { + setBusy(false); + } + } + const labels: Record = { + sector: 'Sector principal', + secondary_sector: 'Sector secundário', + revenue: 'Receita', + cash_flow: 'Lucro / fluxo de caixa', + currency: 'Moeda', + reason: 'Motivo da venda', + availability: 'Disponibilidade declarada', + }; + return ( +
+

Acesso confidencial à empresa

+

+ O acesso depende da aceitação do NDA e da autorização expressa + do vendedor para a mesma versão. +

+ {overview?.nda_pending && ( +

+ O exemplar aprovado do NDA ainda não foi disponibilizado. O + acesso confidencial mantém-se fechado. +

+ )} + {overview?.nda && ( + + Consultar NDA — versão {overview.nda.version} + + )} + {overview && ( +
+