diff --git a/app/Http/Controllers/PrivateOfferPreparationController.php b/app/Http/Controllers/PrivateOfferPreparationController.php new file mode 100644 index 00000000..6f0d6d2a --- /dev/null +++ b/app/Http/Controllers/PrivateOfferPreparationController.php @@ -0,0 +1,59 @@ +scope(Opportunity::query(), $request->user())->whereNotNull('workflow_context->private_offer_preparation') + ->orderByDesc('id')->paginate(20); + + return Inertia::render('offers/preparation-index', ['records' => $records->through(fn ($r) => [ + 'id' => $r->id, 'title' => $r->title, 'reference' => $r->reference, + 'state' => data_get($r->workflow_context, 'private_offer_preparation.state'), + ]), 'canCreate' => count($access->navigation($request->user())['contexts']) > 0]); + } + + public function edit(Request $request, PrivateOfferPreparation $service, ?int $id = null): Response|JsonResponse + { + $record = $id ? $service->record($request->user(), $id) : null; + $contexts = app(PortalProfileAccess::class)->navigation($request->user())['contexts']; + abort_unless($record || count($contexts) > 0, 403, 'É necessária uma atribuição de promotor no âmbito da oferta.'); + $presentation = $record ? $service->presentation($request->user(), $record) : null; + if ($request->expectsJson() && ! $request->header('X-Inertia')) { + return response()->json(['record' => $presentation]); + } + $options = app(OpportunityCanonicalContract::class)->options($request->user(), $record); + + return Inertia::render('offers/preparation', ['record' => $presentation, 'contexts' => $contexts, + 'contacts' => $options['contacts'], 'contactInvestors' => $options['contactInvestors']]); + } + + public function save(Request $request, PrivateOfferPreparation $service, ?int $id = null): JsonResponse + { + if ($id) { + $service->record($request->user(), $id, true); + } + $record = $service->save($request->user(), $request->all(), $id); + + return response()->json(['record' => $service->presentation($request->user(), $record)], $id ? 200 : 201); + } + + public function submit(Request $request, PrivateOfferPreparation $service, int $id): JsonResponse + { + $record = $service->submit($request->user(), $id, $request->all()); + + return response()->json(['record' => $service->presentation($request->user(), $record), + 'message' => 'Submissão privada registada. A oferta não foi publicada.']); + } +} diff --git a/app/Http/Middleware/ProtectProfileOpportunity.php b/app/Http/Middleware/ProtectProfileOpportunity.php index f7ba25ac..7076d8e8 100644 --- a/app/Http/Middleware/ProtectProfileOpportunity.php +++ b/app/Http/Middleware/ProtectProfileOpportunity.php @@ -23,6 +23,14 @@ class ProtectProfileOpportunity } if ($request->user() && $id !== null && Schema::hasColumn('opportunities', 'owner_user_id')) { $record = Opportunity::withTrashed()->find($id); + if ($record?->isPrivateOfferPreparation()) { + app(PortalProfileAccess::class)->authorize($request->user(), $request->isMethod('GET') ? 'view' : 'edit', $record); + if ($request->isMethod('GET') && $request->is('minhas-oportunidades/*/edit', 'minhas-oportunidades/*/view', 'management/opportunities/*/edit', 'management/opportunities/*/view')) { + return redirect('/minhas-oportunidades/ofertas/'.$record->id)->header('Cache-Control', 'private, no-store'); + } + // Contact registration reuses the CRM master and its fresh-MFA gate. + abort_unless($request->isMethod('POST') && $request->is('minhas-oportunidades/*/contacts'), 403, 'Use a preparação privada da oferta.'); + } if ($record && $record->owner_user_id === null && ! $request->isMethod('GET')) { abort_unless(app(PortalProfileAccess::class)->legacyWriteAllowed($request->user()), 403, 'O papel v1 não contorna os controlos através de um registo legado.'); } diff --git a/app/Models/Opportunity.php b/app/Models/Opportunity.php index a8d0c2e4..5a7807f6 100644 --- a/app/Models/Opportunity.php +++ b/app/Models/Opportunity.php @@ -30,6 +30,23 @@ class Opportunity extends Model { use SoftDeletes; + public function isPrivateOfferPreparation(): bool + { + return isset($this->workflow_context['private_offer_preparation']); + } + + protected static function booted(): void + { + static::saving(function (self $record) { + $original = $record->getRawOriginal('workflow_context'); + $wasPrivate = is_string($original) && isset(json_decode($original, true)['private_offer_preparation']); + abort_if($wasPrivate && ! $record->isPrivateOfferPreparation(), 403, 'A preparação privada não pode ser convertida neste percurso.'); + if ($record->isPrivateOfferPreparation()) { + abort_unless($record->status === 'draft' && $record->published_at === null, 403, 'A publicação de Ofertas não está disponível nesta fase.'); + } + }); + } + protected $hidden = ['management_draft', 'management_creation_key', 'management_version', 'first_submitted_at', 'management_closed_once', 'owner_user_id', 'organization_id', 'workflow_context']; protected $appends = ['maturity_catalogue_version']; @@ -115,6 +132,7 @@ class Opportunity extends Model public function scopePublished(Builder $query): Builder { if (Schema::hasColumn('opportunities', 'workflow_context')) { + $query->whereNull('workflow_context->private_offer_preparation'); $query->where(fn ($q) => $q->whereNull('owner_user_id')->orWhere('workflow_context->publication->visibility', 'public')); } diff --git a/app/Services/OpportunityCanonicalContract.php b/app/Services/OpportunityCanonicalContract.php index 12efeef4..9ff7cc7a 100644 --- a/app/Services/OpportunityCanonicalContract.php +++ b/app/Services/OpportunityCanonicalContract.php @@ -279,14 +279,7 @@ class OpportunityCanonicalContract 'economic_activity_id', 'Sector, subsector e atividade devem corresponder à hierarquia/versão publicada; os cinco níveis originais são conservados.'); foreach (['internal_contact_id', 'public_contact_id'] as $field) { if (! empty($payload[$field])) { - $contacts = DB::table('contacts')->where('contacts.id', $payload[$field])->whereNull('contacts.deleted_at'); - if ($record->organization_id) { - $contacts->whereExists(fn ($q) => $q->selectRaw('1')->from('investor_contacts as ic')->join('investors as i', 'i.id', '=', 'ic.investor_id') - ->whereColumn('ic.contact_id', 'contacts.id')->where('i.organization_id', $record->organization_id)->whereNull('i.deleted_at')); - } else { - $contacts->where('source_system', 'portal-user-contact')->where('external_id', 'user:'.$record->owner_user_id); - } - $check($contacts->exists(), $field, 'Contacto inexistente ou sem relação com o Promotor.'); + $check(app(OpportunityContacts::class)->related($record, $payload[$field]), $field, 'Contacto inexistente ou sem relação com o Promotor.'); } } $resources = []; diff --git a/app/Services/OpportunityContacts.php b/app/Services/OpportunityContacts.php index 6e5c86dc..b9ac4245 100644 --- a/app/Services/OpportunityContacts.php +++ b/app/Services/OpportunityContacts.php @@ -14,6 +14,19 @@ use Illuminate\Validation\ValidationException; /** Reuse the CRM master without manufacturing an interest, lead or consent. */ class OpportunityContacts { + public function related(Opportunity $record, string $id): bool + { + $contacts = DB::table('contacts')->where('contacts.id', $id)->whereNull('contacts.deleted_at'); + if ($record->organization_id) { + $contacts->whereExists(fn ($q) => $q->selectRaw('1')->from('investor_contacts as ic')->join('investors as i', 'i.id', '=', 'ic.investor_id') + ->whereColumn('ic.contact_id', 'contacts.id')->where('i.organization_id', $record->organization_id)->whereNull('i.deleted_at')); + } else { + $contacts->where('source_system', 'portal-user-contact')->where('external_id', 'user:'.$record->owner_user_id); + } + + return $contacts->exists(); + } + /** * @param array $input * @return array diff --git a/app/Services/OpportunityManagement.php b/app/Services/OpportunityManagement.php index 2fc0ee00..ffccac47 100644 --- a/app/Services/OpportunityManagement.php +++ b/app/Services/OpportunityManagement.php @@ -21,7 +21,7 @@ class OpportunityManagement public const MATURITY_FIELDS = ['maturity_level', 'maturity_label', 'maturity_stage', 'maturity_catalogue_version']; - public const PRIVATE_DRAFT_FIELDS = [...self::MATURITY_FIELDS, 'real_estate', 'canonical', 'proactive']; + public const PRIVATE_DRAFT_FIELDS = [...self::MATURITY_FIELDS, 'real_estate', 'canonical', 'proactive', 'private_offer']; /** @return array */ public function values(Opportunity $record): array @@ -51,12 +51,13 @@ class OpportunityManagement } /** @param array $input */ - public function save(User $actor, array $input, ?int $id = null): Opportunity + public function save(User $actor, array $input, ?int $id = null, bool $privateOffer = false): Opportunity { $access = app(PortalProfileAccess::class); $profileCreation = $id === null && str_starts_with(request()->path(), 'minhas-oportunidades'); if ($id !== null) { $existingRecord = Opportunity::withTrashed()->findOrFail($id); + abort_if($existingRecord->isPrivateOfferPreparation() && ! $privateOffer, 403, 'Use a preparação privada da oferta.'); $access->authorize($actor, 'edit', $existingRecord); if ($existingRecord->owner_user_id !== null) { abort_unless($access->canEdit($actor, $existingRecord, $input['opportunity_category'] ?? null), 403); @@ -100,6 +101,9 @@ class OpportunityManagement 'owner_user_id' => 'prohibited', 'workflow_context' => 'prohibited', 'status' => 'prohibited', 'first_submitted_at' => 'prohibited', 'province_id' => ['nullable', 'uuid'], 'district_id' => ['nullable', 'uuid'], 'admin_post_id' => ['nullable', 'uuid'], 'locality_id' => ['nullable', 'uuid'], ])->validate(); + if ($privateOffer && ($id === null || array_key_exists('private_offer', $input))) { + $data['private_offer'] = app(PrivateOfferPreparation::class)->validate($input['private_offer'] ?? []); + } $data = app(OpportunityMaturity::class)->normalize($data); if (array_key_exists('real_estate', $data)) { $data['real_estate'] = app(OpportunityRealEstateDraft::class)->validate($data['real_estate']); @@ -108,10 +112,11 @@ class OpportunityManagement $data['canonical'] = app(OpportunityCanonicalContract::class)->draft($data['canonical']); } - return DB::transaction(function () use ($actor, $data, $id, $profileCreation, $access) { + return DB::transaction(function () use ($actor, $data, $id, $profileCreation, $access, $privateOffer) { DB::select('SELECT pg_advisory_xact_lock(91026, 100000)'); if (! $id && ($existing = Opportunity::withTrashed()->where('management_creation_key', $data['creation_key'])->first())) { $access->authorize($actor, 'view', $existing); + abort_unless($existing->isPrivateOfferPreparation() === $privateOffer, 409); abort_unless($profileCreation === ($existing->owner_user_id !== null) && (! $profileCreation || $existing->organization_id === ($data['organization_id'] ?? null)), 409); $snapshot = $this->values($existing); foreach (array_intersect_key($data, array_flip([...self::FIELDS, ...self::PRIVATE_DRAFT_FIELDS])) as $key => $value) { @@ -124,6 +129,7 @@ class OpportunityManagement } $record = $id ? Opportunity::withTrashed()->whereKey($id)->lockForUpdate()->firstOrFail() : new Opportunity; if ($id) { + abort_unless($record->isPrivateOfferPreparation() === $privateOffer, 409); $access->authorize($actor, 'edit', $record); $this->check($record, $data['etag']); abort_if($record->trashed() || $record->status === 'archived', 409, 'Restaure antes de editar.'); @@ -191,12 +197,12 @@ class OpportunityManagement $record->management_draft = json_encode([...$previous, ...$changes], JSON_THROW_ON_ERROR); } else { foreach ($changes as $key => $value) { - if (in_array($key, ['real_estate', 'canonical', 'proactive', 'maturity_stage', 'maturity_catalogue_version'], true) || (($changes['maturity_catalogue_version'] ?? null) === OpportunityMaturity::VERSION && in_array($key, ['maturity_level', 'maturity_label'], true))) { + if (in_array($key, ['real_estate', 'canonical', 'proactive', 'private_offer', 'maturity_stage', 'maturity_catalogue_version'], true) || (($changes['maturity_catalogue_version'] ?? null) === OpportunityMaturity::VERSION && in_array($key, ['maturity_level', 'maturity_label'], true))) { continue; } $record->{$key} = $value; } - if (isset($changes['maturity_catalogue_version']) || array_key_exists('real_estate', $changes) || array_key_exists('canonical', $changes) || array_key_exists('proactive', $changes)) { + if (isset($changes['maturity_catalogue_version']) || array_key_exists('real_estate', $changes) || array_key_exists('canonical', $changes) || array_key_exists('proactive', $changes) || array_key_exists('private_offer', $changes)) { // The existing draft JSON holds the versioned catalogue; legacy columns stay intact. $previous = $record->management_draft ? json_decode($record->management_draft, true, 512, JSON_THROW_ON_ERROR) : []; $record->management_draft = json_encode([...$previous, ...array_intersect_key($changes, array_flip(self::PRIVATE_DRAFT_FIELDS))], JSON_THROW_ON_ERROR); @@ -204,6 +210,9 @@ class OpportunityManagement } if ($record->owner_user_id !== null) { $context = $record->workflow_context ?? []; + if ($privateOffer) { + $context['private_offer_preparation'] ??= ['version' => 1, 'state' => 'draft']; + } unset($context['approval']); $context['content_revision'] = ($context['content_revision'] ?? 0) + 1; $context['contributors'] = array_values(array_unique([...($context['contributors'] ?? []), $actor->id])); diff --git a/app/Services/OpportunityProfileWorkflow.php b/app/Services/OpportunityProfileWorkflow.php index 4984042c..aefc9e29 100644 --- a/app/Services/OpportunityProfileWorkflow.php +++ b/app/Services/OpportunityProfileWorkflow.php @@ -24,7 +24,7 @@ class OpportunityProfileWorkflow /** @return list */ public function actions(User $actor, Opportunity $record): array { - if ($record->owner_user_id === null || ! $this->access->canView($actor, $record)) { + if ($record->isPrivateOfferPreparation() || $record->owner_user_id === null || ! $this->access->canView($actor, $record)) { return []; } $actions = []; @@ -520,6 +520,7 @@ class OpportunityProfileWorkflow private function authorizeAction(User $actor, Opportunity $record, string $action): void { + abort_if($record->isPrivateOfferPreparation(), 403, 'A oferta está em preparação privada; o circuito editorial não está disponível nesta fase.'); $this->access->authorize($actor, 'view', $record); $promoter = $this->access->promoterRecordScope($actor, $record) && ($record->organization_id !== null || (int) $record->owner_user_id === $actor->id); diff --git a/app/Services/OpportunityPromotion.php b/app/Services/OpportunityPromotion.php index 2c875a95..0d75bbe7 100644 --- a/app/Services/OpportunityPromotion.php +++ b/app/Services/OpportunityPromotion.php @@ -22,7 +22,7 @@ final class OpportunityPromotion public function allowed(User $actor, Opportunity $record): bool { $access = app(PortalProfileAccess::class); - if ($record->trashed() || $record->owner_user_id === null || ! $access->canView($actor, $record) + if ($record->isPrivateOfferPreparation() || $record->trashed() || $record->owner_user_id === null || ! $access->canView($actor, $record) || $access->has($actor, 'auditor') || in_array($record->status, ['archived', 'rejected'], true)) { return false; } diff --git a/app/Services/PortalProfileAccess.php b/app/Services/PortalProfileAccess.php index dae2bdbe..c8406dc8 100644 --- a/app/Services/PortalProfileAccess.php +++ b/app/Services/PortalProfileAccess.php @@ -241,6 +241,9 @@ class PortalProfileAccess public function canEdit(User $actor, Opportunity $record, ?string $newType = null): bool { + if ($record->isPrivateOfferPreparation() && data_get($record->workflow_context, 'private_offer_preparation.state') !== 'draft') { + return false; + } if ($record->owner_user_id === null) { return $this->legacyWriteAllowed($actor); } diff --git a/app/Services/PrivateOfferPreparation.php b/app/Services/PrivateOfferPreparation.php new file mode 100644 index 00000000..ad2419d0 --- /dev/null +++ b/app/Services/PrivateOfferPreparation.php @@ -0,0 +1,119 @@ + */ + 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', + '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', + ])->validate()['private_offer']; + } + + /** @param array $input */ + public function save(User $actor, array $input, ?int $id = null): Opportunity + { + foreach (['title', 'description'] as $field) { + if (is_string($input[$field] ?? null)) { + $input[$field] = trim($input[$field]); + } + } + $allowed = ['creation_key', 'etag', 'reason', 'title', 'description', 'opportunity_category', 'organization_id', 'private_offer', ...array_keys(InvestmentGeography::FIELDS)]; + if (array_diff(array_keys($input), $allowed)) { + throw ValidationException::withMessages(['offer' => 'O pedido contém campos fora da preparação privada.']); + } + Validator::make($input, ['opportunity_category' => ['sometimes', 'required', Rule::in(OpportunityManagement::CATEGORIES)]])->validate(); + + return DB::transaction(function () use ($actor, $input, $id) { + $record = app(OpportunityManagement::class)->save($actor, $input, $id, true); + $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.']); + } + + return $record; + }); + } + + public function record(User $actor, int $id, bool $edit = false): Opportunity + { + $record = Opportunity::findOrFail($id); + abort_unless($record->isPrivateOfferPreparation(), 404); + app(PortalProfileAccess::class)->authorize($actor, $edit ? 'edit' : 'view', $record); + + return $record; + } + + /** Register private submission only: no editorial task, approval or publication. + * @param array $input */ + public function submit(User $actor, int $id, array $input): Opportunity + { + $data = Validator::make($input, ['etag' => 'required|string|size:64', 'confirm' => 'required|accepted'])->validate(); + + return DB::transaction(function () use ($actor, $id, $data) { + $record = Opportunity::whereKey($id)->lockForUpdate()->firstOrFail(); + abort_unless($record->isPrivateOfferPreparation(), 404); + $access = app(PortalProfileAccess::class); + $access->authorize($actor, 'view', $record); + abort_unless($access->promoterRecordScope($actor, $record) + && $access->canCreate($actor, $record->organization_id, $record->opportunity_category) + && $access->operational($actor, 'edit', $record->id), 403); + $management = app(OpportunityManagement::class); + abort_unless(hash_equals($management->etag($record), $data['etag']), 409, 'A oferta mudou. Recupere a versão actual antes de submeter.'); + if (data_get($record->workflow_context, 'private_offer_preparation.state') === 'submitted') { + return $record; + } + $access->authorize($actor, 'edit', $record); + $values = $management->values($record); + Validator::make($values, ['title' => 'required|string|max:150', 'description' => 'required|string|max:50000', + 'opportunity_category' => 'required', 'private_offer.price_mode' => 'required|in:amount,negotiable', 'private_offer.contact_id' => 'required|uuid'])->validate(); + $this->validate($values['private_offer'], true); + if (! app(OpportunityContacts::class)->related($record, $values['private_offer']['contact_id'])) { + throw ValidationException::withMessages(['private_offer.contact_id' => 'O contacto já não está disponível neste âmbito.']); + } + // Recheck revoked grants and representation inside the same transaction. + abort_unless($access->canCreate($actor, $record->organization_id, $record->opportunity_category), 403); + $path = app(InvestmentGeography::class)->path($record->primary_territory_id, true); + if ($record->primary_territory_id && $path === []) { + throw ValidationException::withMessages(['province_id' => 'Reconcilie a localização antes de submeter.']); + } + if ($path !== []) { + app(InvestmentGeography::class)->selection(array_combine(array_slice(array_keys(InvestmentGeography::FIELDS), 0, count($path)), array_column($path, 'id')), true); + } + $context = $record->workflow_context; + $context['private_offer_preparation'] = ['version' => 1, 'state' => 'submitted', 'submitted_by' => $actor->id, + 'submitted_at' => now()->toIso8601String(), 'submitted_revision' => $record->management_version]; + $record->workflow_context = $context; + $record->management_version++; + $record->save(); + $access->audit($actor, 'private_offer_submitted', ['record_id' => $record->id, 'version' => $record->management_version, 'public' => false]); + + return $record; + }); + } + + /** @return array */ + public function presentation(User $actor, Opportunity $record): array + { + app(PortalProfileAccess::class)->authorize($actor, 'view', $record); + + return ['id' => $record->id, 'reference' => $record->reference, 'values' => app(OpportunityManagement::class)->values($record), + 'etag' => app(OpportunityManagement::class)->etag($record), 'state' => data_get($record->workflow_context, 'private_offer_preparation.state'), + 'canEdit' => app(PortalProfileAccess::class)->canEdit($actor, $record), 'organization_id' => $record->organization_id, + 'territory' => app(InvestmentGeography::class)->path($record->primary_territory_id, true)]; + } +} diff --git a/docs/investmoz/unification/traceability-and-manual.md b/docs/investmoz/unification/traceability-and-manual.md index 9b29aff1..eff519c3 100644 --- a/docs/investmoz/unification/traceability-and-manual.md +++ b/docs/investmoz/unification/traceability-and-manual.md @@ -10139,3 +10139,17 @@ Mudanças de datas/fontes não substituem conclusões ou valores. A correspondê QA:70testes frontend afectados distintos, incluindo nove novos de domínio/UI; sucesso, falta de fonte, divergência de titular/datas, período anterior curto/não contíguo, reconferência independente, preservação da distribuição, mês único e histórico. API nativa POST/PUT/GET:quatro cenários sintéticos,46asserções, rollback em investmoz_demography_test:55441, identidade PDO conferida. Cálculo→gravação→recuperação→relatório financeiro do mesmo dossier demonstrados. Tipos, lint focado, Pint e compilação aprovados. Edge com componentes reais compilados:112000MZN nos meses relativos correctos, concentração emJaneiro2026, total indeterminado sem fonte, PEC preservado quando PPC muda e193000MZN apenas após reconferência. Corrigido aviso contraditório de ano civil no caso documental incompleto; regressão acrescentada. Provas:outputs/irpc-noncivil-20261114/. Provas aceites não afectadas reutilizadas. Estado:implementado e verificado em QA, ainda sem instalação. Homologação mantém078f770d. Este lote encerra apenas as estimativas PPC/PEC e a projecção deste recorte anual não civil documentado; não encerra L6/L8, a liquidação final não civil, o simulador ou o portal. Resíduos L1–L7/restanteL8/L9–L16 e outros módulos preservados na matriz. Sem migrações, permissões, rotas, importações, pagamentos, declarações submetidas ou novas deduções. Instalação agrupada depende de autorização concreta, executor Git, backup/restauro fresco e reversão de release preservando edições posteriores. + +## 14/11/2026 — Ofertas, Fase 1: preparação privada em QA + +Autorização condicionada expressa da Tempo nesta sessão: exclusivamente desenvolvimento da Fase 1. Oportunidades é o cadastro principal das novas Ofertas; negócios e restantes percursos mantêm os seus identificadores, contratos e dados. Sem mudanças no menu, esquema mne, dados operacionais ou instalação. As migrações existentes foram usadas apenas para preparar uma base sintética nova, offers_phase1_20261114_test:55441, preservando as outras bases QA. + +Entrada privada em desenvolvimento: /minhas-oportunidades/ofertas. Requer autenticação, email verificado, perfis disponíveis e âmbito de Promotor já atribuído para criar/editar/submeter; não concede papéis. Criar → guardar → recuperar → editar → conferir → submeter em privado. Rascunhos incompletos são recuperáveis. A submissão exige título, descrição, categoria, contacto autorizado e preço MZN/USD ou «A negociar»; não converte moedas nem confunde preço pedido com investimento estimado. Localização usa os identificadores e a hierarquia existentes. Contactos usam o CRM e o seu controlo de MFA, sem novo cadastro ou publicação dos contactos. + +Gravação usa OpportunityManagement, chave de criação, etag, versões, management_draft e auditoria existentes. Campos inesperados são recusados; permissões, representação e contacto são reconferidos no servidor. Conflitos não sobrescrevem a versão guardada. A submissão regista autor, instante UTC e revisão nos metadados privados, sem criar revisão editorial, notificações, aprovação ou publicação da Fase 2. A oferta submetida fica recuperável e sem edição nesta fase. A marca privada bloqueia conversão/publicação pelo modelo, workflow e divulgação existentes; o catálogo público exclui-a mesmo perante um estado incoerente introduzido por outro escritor. + +Verificação: 14 testes novos. Bateria de regressão de Oportunidades/perfis/editorial: 35 testes e 548 asserções aprovados. Passagem final focada, após as últimas alterações, com 16 testes e 172 asserções aprovados; os grupos sobrepõem-se e não devem ser somados. Inclui criação idempotente, recuperação, conflito, contacto de outro titular/eliminado, perda de representação/permissão, preço incompleto/inválido, campos forjados, submissão confirmada, bloqueios de publicação e ausência nas consultas públicas. Duas páginas verificadas por HTML completo com entradas Vite reais. Pint, PHPStan, tipos, lint/formatação focados, build QA e revisão do delta aprovados. Recibo: outputs/offers-private-phase1-20261114/qa-result.json. + +Limitações: conferência visual manual não efectuada. npm run check global acusa formatação preexistente em 61 ficheiros alheios ao delta; esses ficheiros não foram alterados e a falha não foi dispensada. A bateria canónica extensa foi interrompida, não contabilizada como aprovada e substituída pela verificação do cenário afectado. Uma prova antiga de divulgação exige outra base pelo seu próprio controlo de segurança; esse controlo foi preservado, sem tocar nessa base, e a compatibilidade do âmbito nativo foi testada na nova preparação sintética. + +Saldo de RF-MN-003: Fase 1 de Ofertas implementada e verificada tecnicamente em QA, sem instalação. Não encerra o requisito integral nem Procuras. Fase 2 e seguintes permanecem por autorizar; NDA, comprovativos, fotografias, validade, moderação e publicação não são simulados nesta entrega. A autorização anterior de validador jurídico para legislação não é estendida a Ofertas. Instalação em homologação ou produção exige verificação e autorização expressa separada. \ No newline at end of file diff --git a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv index dccfcc2f..b9d13f7b 100644 --- a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv +++ b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv @@ -163,7 +163,7 @@ RF-MIN-005,"Controlar cessão, mudança de controlo e aprovações.",upgrade v4 RF-MIN-006,Pesquisar e corresponder oportunidades mineiras.,upgrade v4 SHA f0b9815…; p. 3275; requirements-audit.csv,oportunidades,/api/v1/business-opportunities; /api/v1/business-opportunities/{slug}; /investidor/terra-duat; /investidor/terra-duat/novo; /investidor/terra-duat/{id}; /investidor/terra-duat/{id}/actualizacoes; /investidor/terra-duat/{id}/documentos; /investidor/terra-duat/{id}/documentos/{attachment}; /investidor/terra-duat/{id}/editar; /investidor/terra-duat/{id}/imprimir; /investidor/terra-duat/{id}/solicitar-apoio; /management/opportunities,"OpportunityManagement, OpportunityCanonicalContract/Response, OpportunityReviewMateriality, PortalProfileAccess, OpportunityResources. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.",outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,Sem evidência suficiente,Reconciliar contrato/cláusula e provas por categoria; completar apenas falha/variante executável identificada. Response normativo não é completo. Critério individual original preservado; cobertura atual ainda não individualizada.,"030000→040000 já instalados; contratos/resources existentes, sessão/MFA, ClamD e worker para ficheiros; gates das seis categorias ON. Variantes modulares/offers continuam separadas. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.","RF-MIN-006: Filtros incluem recurso, fase, título, localização, valor, modalidade e infra-estrutura; recomendação é explicável e revista por gestor.",P2,RF-MIN-006: Relatório 25/09 conservado; estado histórico não promove conclusão,RF-MIN-006,1,"[{""id"":""RF-MIN-006"",""pedido"":""Pesquisar e corresponder oportunidades mineiras."",""fontes"":[""upgrade v4 SHA f0b9815…; p. 3275; requirements-audit.csv""],""aceitacao"":""Filtros incluem recurso, fase, título, localização, valor, modalidade e infra-estrutura; recomendação é explicável e revista por gestor."",""estado"":""parcial"",""variantes"":""Relatório 25/09 conservado; estado histórico não promove conclusão"",""modulo"":""oportunidades""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,public.opportunities: 6 registos; public.opportunity_images: tabela ausente; public.opportunity_documents: tabela ausente; public.opportunity_location_drafts: tabela ausente,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.",Reconciliar contrato/cláusula e provas por categoria; completar apenas falha/variante executável identificada. Response normativo não é completo. Critério individual original preservado; cobertura atual ainda não individualizada.,"OpportunityManagement, OpportunityCanonicalContract/Response, OpportunityReviewMateriality, PortalProfileAccess, OpportunityResources","030000→040000 já instalados; contratos/resources existentes, sessão/MFA, ClamD e worker para ficheiros; gates das seis categorias ON. Variantes modulares/offers continuam separadas.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: oportunidades. Esforço de código adicional só após lacuna reproduzida.,outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,oportunidades,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",app/Services/OpportunityManagement.php; app/Services/OpportunityResources.php; resources/js/pages/management/opportunity-draft.tsx — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"OpportunityManagementTest, OpportunityResourcesTest, OpportunityOriginReviewTest; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.",Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,, RF-MN-001,Registar utilizadores singulares e colectivos com verificação de contactos.,upgrade v4 SHA f0b9815…; p. 2957; requirements-audit.csv | portal do investidor\upgrade do portal de investimentos.docx; SHA 471012abb0f56377956f4ab822fe3186e2561bee0471508a5d807091f2e0154f; w:p 573 | portal do investidor\Portal V4\upgrade do portal de investimentos v6 programacao.docx; SHA e1169a7743b13ff6c950d3ffd4672a2afea32e81da84885e11154b7d8de9241e; w:p 3409 | portal do investidor\Portal V4\upgrade do portal de investimentos v1.docx; SHA f70ce3075523c6278e00dce15b9c7acf2b690679e16959ae5f7d007e754cfc6e; w:p 580 | portal do investidor\Portal V4\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 2214 | portal do investidor\Portal V4\upgrade do portal de investimentos v5 programacao.docx; SHA edece80c0519537244f8a8b86456af0aa92de1c526f791bb0ee9b85af7a7e8b2; w:p 3409,CRM/solicitações,//; /Simulador; /api/v1/documents/recommendations; /api/v1/investment/submissions; /api/v1/investment/submissions/{id}; /api/v1/investment/submissions/{id}/attachments; /api/v1/investment/submissions/{id}/attachments/{attachment}; /api/v1/investment/submissions/{id}/submit; /apoio; /balcao-virtual; /como-investir; /documentos,"investors/contacts/leads/tasks, OpportunityContacts, acompanhamento interno existente. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.",outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,Sem evidência suficiente,"Converter sem duplicar, tarefa/responsável/próxima ação recuperáveis; corrigir apenas falha comprovada. Critério individual original preservado; cobertura atual ainda não individualizada.",Relações mestre/contexto e autorizações; não depende de CRM externo. C131 sóQA. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,RF-MN-001: Conta só é activada após confirmação do correio electrónico ou telefone; tentativas e falhas ficam registadas. | MASTER-471012ab-0051: Registar utilizadores singulares e colectivos com verificação de contactos. | MASTER-e1169a77-0201: Registar utilizadores singulares e colectivos com verificação de contactos. | MASTER-f70ce307-0057: Registar utilizadores singulares e colectivos com verificação de contactos. | MASTER-51b8f0ad-0137: Registar utilizadores singulares e colectivos com verificação de contactos. | MASTER-edece80c-0201: Registar utilizadores singulares e colectivos com verificação de contactos.,P2,RF-MN-001: Relatório 25/09 conservado; estado histórico não promove conclusão | MASTER-471012ab-0051: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-e1169a77-0201: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-f70ce307-0057: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-51b8f0ad-0137: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-edece80c-0201: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,RF-MN-001 | MASTER-471012ab-0051 | MASTER-e1169a77-0201 | MASTER-f70ce307-0057 | MASTER-51b8f0ad-0137 | MASTER-edece80c-0201,6,"[{""id"":""RF-MN-001"",""pedido"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""fontes"":[""upgrade v4 SHA f0b9815…; p. 2957; requirements-audit.csv""],""aceitacao"":""Conta só é activada após confirmação do correio electrónico ou telefone; tentativas e falhas ficam registadas."",""estado"":""parcial"",""variantes"":""Relatório 25/09 conservado; estado histórico não promove conclusão"",""modulo"":""CRM/solicitações""},{""id"":""MASTER-471012ab-0051"",""pedido"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""fontes"":[""portal do investidor\\upgrade do portal de investimentos.docx; SHA 471012abb0f56377956f4ab822fe3186e2561bee0471508a5d807091f2e0154f; w:p 573""],""aceitacao"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""},{""id"":""MASTER-e1169a77-0201"",""pedido"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v6 programacao.docx; SHA e1169a7743b13ff6c950d3ffd4672a2afea32e81da84885e11154b7d8de9241e; w:p 3409""],""aceitacao"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""},{""id"":""MASTER-f70ce307-0057"",""pedido"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v1.docx; SHA f70ce3075523c6278e00dce15b9c7acf2b690679e16959ae5f7d007e754cfc6e; w:p 580""],""aceitacao"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""},{""id"":""MASTER-51b8f0ad-0137"",""pedido"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 2214""],""aceitacao"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""},{""id"":""MASTER-edece80c-0201"",""pedido"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v5 programacao.docx; SHA edece80c0519537244f8a8b86456af0aa92de1c526f791bb0ee9b85af7a7e8b2; w:p 3409""],""aceitacao"":""Registar utilizadores singulares e colectivos com verificação de contactos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,public.interest_requests: 4 registos; public.leads: 0 registos; public.investors: 3 registos,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.","Converter sem duplicar, tarefa/responsável/próxima ação recuperáveis; corrigir apenas falha comprovada. Critério individual original preservado; cobertura atual ainda não individualizada.","investors/contacts/leads/tasks, OpportunityContacts, acompanhamento interno existente",Relações mestre/contexto e autorizações; não depende de CRM externo. C131 sóQA.,Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: acompanhamento. Esforço de código adicional só após lacuna reproduzida.,outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,acompanhamento,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",app/Http/Controllers/CrmController.php; app/Services/DeclaredOriginStatistics.php — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"InterestSubmissionTest, InvestmentWorkspaceTest, PortalOriginCompatibilityTest; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.",Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,, RF-MN-002,"Gerir perfis, representantes, funções e organizações.",upgrade v4 SHA f0b9815…; p. 2961; requirements-audit.csv,governação/outros,/api/v1/investment/admin/interests/{interest}/status; /management/history; /management/interests; /management/interests/{interest}; /management/interests/{interest}/consent/contact; /management/interests/{interest}/consent/proponent; /management/opportunities; /management/opportunities/create; /management/opportunities/drafts; /management/opportunities/{id}/draft; /management/opportunities/{id}/edit; /management/opportunities/{id}/lifecycle,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.","outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual. | 02/11/2026: profile-json preserva erros por campo e reautenticacao em falhas HTML/419 e rejeita JSON invalido; contexto de investimento rejeita falso sucesso HTTP 200 e liberta keepalive, sem repetir escritas. 26 testes focados, TypeScript e build aprovados. Prova local parcial; sem dados/permissoes/publicacao. | Retoma 02/11/2026: profileJson GET independente de cookie CSRF malformado; POST recusa antes do transporte com mensagem de recuperacao. Preservadas validacao e credenciais. 14 testes focados aprovados, incluindo separadores de cookie sem espaco; sem mudanca de direitos.",Sem evidência suficiente,Localizar obrigação verificável e consumidores/contrato atuais antes de estimar implementação ou declarar conclusão. Critério individual original preservado; cobertura atual ainda não individualizada.,"Ler secção/versão correspondente; distinguir requisito funcional, decisão e exemplo. Fontes oficiais já aceites. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.",RF-MN-002: Uma organização pode ter vários utilizadores; cada acção identifica o utilizador e a função.,P2,RF-MN-002: Relatório 25/09 conservado; estado histórico não promove conclusão,RF-MN-002,1,"[{""id"":""RF-MN-002"",""pedido"":""Gerir perfis, representantes, funções e organizações."",""fontes"":[""upgrade v4 SHA f0b9815…; p. 2961; requirements-audit.csv""],""aceitacao"":""Uma organização pode ter vários utilizadores; cada acção identifica o utilizador e a função."",""estado"":""parcial"",""variantes"":""Relatório 25/09 conservado; estado histórico não promove conclusão"",""modulo"":""governação/outros""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,Requisito transversal; consultar estado-ativo-after.json,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.",Localizar obrigação verificável e consumidores/contrato atuais antes de estimar implementação ou declarar conclusão. Critério individual original preservado; cobertura atual ainda não individualizada.,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer","Ler secção/versão correspondente; distinguir requisito funcional, decisão e exemplo. Fontes oficiais já aceites.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: governação e demais cláusulas. Esforço de código adicional só após lacuna reproduzida.,outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,governação e demais cláusulas,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",AGENTS.md — componentes do domínio encontrados; correspondência integral deste requisito não presumida,; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.,Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,, -RF-MN-003,"Criar, guardar, editar e submeter ofertas e procuras.",upgrade v4 SHA f0b9815…; p. 2965; requirements-audit.csv | portal do investidor\upgrade do portal de investimentos.docx; SHA 471012abb0f56377956f4ab822fe3186e2561bee0471508a5d807091f2e0154f; w:p 581 | portal do investidor\Portal V4\upgrade do portal de investimentos v6 programacao.docx; SHA e1169a7743b13ff6c950d3ffd4672a2afea32e81da84885e11154b7d8de9241e; w:p 3417 | portal do investidor\Portal V4\upgrade do portal de investimentos v1.docx; SHA f70ce3075523c6278e00dce15b9c7acf2b690679e16959ae5f7d007e754cfc6e; w:p 588 | portal do investidor\Portal V4\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 2222 | portal do investidor\Portal V4\upgrade do portal de investimentos v5 programacao.docx; SHA edece80c0519537244f8a8b86456af0aa92de1c526f791bb0ee9b85af7a7e8b2; w:p 3417,governação/outros,/api/v1/investment/admin/interests/{interest}/status; /management/history; /management/interests; /management/interests/{interest}; /management/interests/{interest}/consent/contact; /management/interests/{interest}/consent/proponent; /management/opportunities; /management/opportunities/create; /management/opportunities/drafts; /management/opportunities/{id}/draft; /management/opportunities/{id}/edit; /management/opportunities/{id}/lifecycle,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","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-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""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,Requisito transversal; consultar estado-ativo-after.json,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.",Localizar obrigação verificável e consumidores/contrato atuais antes de estimar implementação ou declarar conclusão. Critério individual original preservado; cobertura atual ainda não individualizada.,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer","Ler secção/versão correspondente; distinguir requisito funcional, decisão e exemplo. Fontes oficiais já aceites.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: governação e demais cláusulas. Esforço de código adicional só após lacuna reproduzida.,outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,governação e demais cláusulas,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",AGENTS.md — componentes do domínio encontrados; correspondência integral deste requisito não presumida,; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.,Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,, +RF-MN-003,"Criar, guardar, editar e submeter ofertas e procuras.",upgrade v4 SHA f0b9815…; p. 2965; requirements-audit.csv | portal do investidor\upgrade do portal de investimentos.docx; SHA 471012abb0f56377956f4ab822fe3186e2561bee0471508a5d807091f2e0154f; w:p 581 | portal do investidor\Portal V4\upgrade do portal de investimentos v6 programacao.docx; SHA e1169a7743b13ff6c950d3ffd4672a2afea32e81da84885e11154b7d8de9241e; w:p 3417 | portal do investidor\Portal V4\upgrade do portal de investimentos v1.docx; SHA f70ce3075523c6278e00dce15b9c7acf2b690679e16959ae5f7d007e754cfc6e; w:p 588 | portal do investidor\Portal V4\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 2222 | portal do investidor\Portal V4\upgrade do portal de investimentos v5 programacao.docx; SHA edece80c0519537244f8a8b86456af0aa92de1c526f791bb0ee9b85af7a7e8b2; w:p 3417,governação/outros,/api/v1/investment/admin/interests/{interest}/status; /management/history; /management/interests; /management/interests/{interest}; /management/interests/{interest}/consent/contact; /management/interests/{interest}/consent/proponent; /management/opportunities; /management/opportunities/create; /management/opportunities/drafts; /management/opportunities/{id}/draft; /management/opportunities/{id}/edit; /management/opportunities/{id}/lifecycle,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual. | 14/11/2026 - Ofertas F1: oportunidades como cadastro principal; criar, guardar, recuperar, editar e submeter em privado; autorizacao existente, CRM e localizacao reutilizados; sem menu, publicacao ou instalacao.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.","outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual. | QA: PrivateOfferPreparationTest.php; regressao 35 testes/548 assercoes e passagem final focada 16/172 (grupos sobrepostos, nao somar). outputs/offers-private-phase1-20261114/qa-result.json.",Parcial - Ofertas Fase 1 implementada e verificada tecnicamente em QA; nao instalada,"Ofertas: Fase 2 e seguintes nao autorizadas nem implementadas neste delta; moderacao, publicacao, NDA/comprovativos, fotografias e validade continuam separados. Procuras nao abrangidas. Conferencia visual por efectuar. Check global de formatacao acusa 61 ficheiros preexistentes fora do delta; verificacao focada aprovada.","Autorizacao expressa para Fase 2 e, separadamente, instalacao; decisoes restantes da Fase 0 aplicam-se apenas as fases dependentes.","RF-MN-003: Rascunho persiste; submissão bloqueia campos essenciais e gera número de protocolo. | MASTER-471012ab-0052: Criar, guardar, editar e submeter ofertas e procuras. | MASTER-e1169a77-0202: Criar, guardar, editar e submeter ofertas e procuras. | MASTER-f70ce307-0058: Criar, guardar, editar e submeter ofertas e procuras. | MASTER-51b8f0ad-0138: Criar, guardar, editar e submeter ofertas e procuras. | MASTER-edece80c-0202: Criar, guardar, editar e submeter ofertas e procuras.",P2,RF-MN-003: Relatório 25/09 conservado; estado histórico não promove conclusão | MASTER-471012ab-0052: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-e1169a77-0202: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-f70ce307-0058: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-51b8f0ad-0138: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-edece80c-0202: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,RF-MN-003 | MASTER-471012ab-0052 | MASTER-e1169a77-0202 | MASTER-f70ce307-0058 | MASTER-51b8f0ad-0138 | MASTER-edece80c-0202,6,"[{""id"":""RF-MN-003"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""upgrade v4 SHA f0b9815…; p. 2965; requirements-audit.csv""],""aceitacao"":""Rascunho persiste; submissão bloqueia campos essenciais e gera número de protocolo."",""estado"":""parcial"",""variantes"":""Relatório 25/09 conservado; estado histórico não promove conclusão"",""modulo"":""governação/outros""},{""id"":""MASTER-471012ab-0052"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\upgrade do portal de investimentos.docx; SHA 471012abb0f56377956f4ab822fe3186e2561bee0471508a5d807091f2e0154f; w:p 581""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-e1169a77-0202"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v6 programacao.docx; SHA e1169a7743b13ff6c950d3ffd4672a2afea32e81da84885e11154b7d8de9241e; w:p 3417""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-f70ce307-0058"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v1.docx; SHA f70ce3075523c6278e00dce15b9c7acf2b690679e16959ae5f7d007e754cfc6e; w:p 588""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-51b8f0ad-0138"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 2222""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-edece80c-0202"",""pedido"":""Criar, guardar, editar e submeter ofertas e procuras."",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v5 programacao.docx; SHA edece80c0519537244f8a8b86456af0aa92de1c526f791bb0ee9b85af7a7e8b2; w:p 3417""],""aceitacao"":""Criar, guardar, editar e submeter ofertas e procuras."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""}]",QA - formulario e lista privados; HTML compilado real conferido; visual manual pendente,QA - rotas autenticadas /minhas-oportunidades/ofertas; autorizacao no servidor; sem rota publica nova,"QA - oportunidades, management_draft, workflow_context e auditoria existentes; sem novo cadastro",QA - recuperacao por identidade existente e controlo de concorrencia por etag,"QA - rascunho ou submissao privada; sem aprovacao, publicacao ou exposicao publica",Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,Requisito transversal; consultar estado-ativo-after.json,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.",Localizar obrigação verificável e consumidores/contrato atuais antes de estimar implementação ou declarar conclusão. Critério individual original preservado; cobertura atual ainda não individualizada.,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer","Ler secção/versão correspondente; distinguir requisito funcional, decisão e exemplo. Fontes oficiais já aceites.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: governação e demais cláusulas. Esforço de código adicional só após lacuna reproduzida.,outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,governação e demais cláusulas,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",AGENTS.md — componentes do domínio encontrados; correspondência integral deste requisito não presumida,; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.,Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,, RF-MN-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/outputs/offers-private-phase1-20261114/qa-result.json b/outputs/offers-private-phase1-20261114/qa-result.json new file mode 100644 index 00000000..0eb3750f --- /dev/null +++ b/outputs/offers-private-phase1-20261114/qa-result.json @@ -0,0 +1,119 @@ +{ + "date": "2026-11-14", + "scope": "Ofertas - Fase 1 - Preparacao Privada", + "base_revision": "e8408c27d9aeb17f534f62f18f80a28ba7f9aca5", + "state": "implemented_and_technically_verified_in_qa_not_installed", + "qa_database": "offers_phase1_20261114_test:55441", + "new_tests": 14, + "regression_run": { + "tests": 35, + "assertions": 548, + "result": "passed" + }, + "final_focused_run": { + "tool": "phpunit", + "result": "passed", + "tests": 16, + "passed": 16, + "assertions": 172, + "duration_ms": 66152 + }, + "runs_overlap": true, + "checks": { + "pint": "passed", + "phpstan": "passed", + "typescript": "passed", + "focused_frontend_format_lint": "passed", + "qa_build": "passed", + "full_html_real_page_entries": "passed", + "git_diff_check": "passed", + "global_npm_check": "failed: 61 pre-existing formatting files outside this delta; not changed or waived" + }, + "limits": [ + "No manual browser visual review performed.", + "An extensive canonical run was interrupted and is not counted as approved; the affected canonical scenario passed in the final focused run.", + "The legacy OpportunityPromotionTest pins another QA database; its guard was preserved. Equivalent native-scope and private-offer blocking assertions passed in the new isolated fixture." + ], + "new_migrations": [], + "production_data_modified": false, + "mne_code_modified": false, + "menu_modified": false, + "homologation_installed": false, + "phase2_implemented": false, + "private_entry": "/minhas-oportunidades/ofertas", + "files": [ + "app/Http/Middleware/ProtectProfileOpportunity.php", + "app/Models/Opportunity.php", + "app/Services/OpportunityCanonicalContract.php", + "app/Services/OpportunityContacts.php", + "app/Services/OpportunityManagement.php", + "app/Services/OpportunityProfileWorkflow.php", + "app/Services/OpportunityPromotion.php", + "app/Services/PortalProfileAccess.php", + "docs/investmoz/unification/traceability-and-manual.md", + "docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv", + "routes/web.php", + "app/Http/Controllers/PrivateOfferPreparationController.php", + "app/Services/PrivateOfferPreparation.php", + "resources/js/pages/offers/preparation-index.tsx", + "resources/js/pages/offers/preparation.tsx", + "tests/Feature/PrivateOfferPreparationTest.php" + ], + "pending_decisions": [ + "Express authorization before Phase 2 or installation.", + "Photo, legal/NDA, proof and expiry decisions remain for their later phases." + ], + "source_sha256": { + "app/Http/Middleware/ProtectProfileOpportunity.php": "30aa2a4b864a2bbfa536f134d93876f6ab2d03194854a846190f46847f2d2215", + "app/Models/Opportunity.php": "b2fe01b408922f13ab2b0aef48c5ec4c07c7d692138093365059d8f8289d6560", + "app/Services/OpportunityCanonicalContract.php": "c954840e726fa81feddc0dcf32bb85078c48bae2c9dd3b50df54b549ab4d18fa", + "app/Services/OpportunityContacts.php": "a5b378bc35d71ca67153819abc87a4047e23db9e19c74ec12fc25e19cc9a4379", + "app/Services/OpportunityManagement.php": "6cf600269c03f64555f978dc80076e9163fd07e17077aa4f3b222fab433779f2", + "app/Services/OpportunityProfileWorkflow.php": "ce3ad0f52ba3ef8ac1845f300ab633b1050bbcd052b564d0ddc1fd3de2c6dcbb", + "app/Services/OpportunityPromotion.php": "4a2d170988ad85e9af52746d7b2435eeba6efe7f815b9b4cb152c7cd537d06d0", + "app/Services/PortalProfileAccess.php": "6388a263bfd4ef32f6ae3ce13b0ac099a7258412a1923358210a8e07394be9bc", + "docs/investmoz/unification/traceability-and-manual.md": "4c86ea3393965b447b47c5acf51ec30d21274584d3b7b52256a6000d81dc4835", + "docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv": "3c07628a9f8157fc11b6f81169669e9ea397e83b4db3335240cab6c67abee81e", + "routes/web.php": "5fdf1a7b6f49b56028a0d17b8ae8befcda9617f485b4d593e2daf60c2166f13c", + "app/Http/Controllers/PrivateOfferPreparationController.php": "b94e133f3ab0684c8360969a38c77208fb57f23dd9e0aebd5224d4a1fcab932d", + "app/Services/PrivateOfferPreparation.php": "abb728e904e22b7c830efa07502507a183159051d738418ae9372fe43ca675b9", + "resources/js/pages/offers/preparation-index.tsx": "5d092e2179f021c5d10997e51dbbd8181ca285a8cde743484131580f4616d3cb", + "resources/js/pages/offers/preparation.tsx": "58cbc6379c8526bfa68506634ac9b34d1fa95193c71efa178da5d937e52ff016", + "tests/Feature/PrivateOfferPreparationTest.php": "49756c500ae102460b7b8678725720a1e346d7d738273aea7cce633145e8ef0d" + }, + "matrix_preservation": { + "rows_changed": 1, + "id": "RF-MN-003", + "other_rows_unchanged": true, + "columns_preserved": 49 + }, + "html_build_portability_run": { + "tests": 1, + "assertions": 10, + "result": "passed", + "directory_env": "EDITORIAL_QA_BUILD_DIRECTORY=build-qa-opportunities", + "default_ci_directory": "build" + }, + "commands": [ + "php vendor/bin/phpunit tests/Feature/PrivateOfferPreparationTest.php tests/Feature/OpportunityManagementTest.php tests/Feature/PortalProfileAccessTest.php tests/Feature/PortalEditorialWorkflowTest.php", + "php vendor/bin/phpunit --filter PrivateOfferPreparationTest|test_phase_one_requires_a_new_legal_decision_for_each_content_and_attachment_version|test_approval_restricted_and_public_labels_follow_actual_catalog_policy tests/Feature/PrivateOfferPreparationTest.php tests/Feature/OpportunityCanonicalWorkflowTest.php tests/Feature/OpportunityPublicationScopeTest.php", + "php vendor/bin/phpunit --filter test_full_html_uses_real_compiled_private_page_entries tests/Feature/PrivateOfferPreparationTest.php", + "php vendor/bin/pint --dirty", + "php vendor/bin/phpstan analyse --no-progress --memory-limit=1G", + "npm.cmd run types:check", + "npx.cmd vp check resources/js/pages/offers/preparation.tsx resources/js/pages/offers/preparation-index.tsx", + "INVESTMOZ_QA_BUILD=1 npm.cmd run build", + "npm.cmd run check", + "git diff --check" + ], + "test_environment": { + "APP_ENV": "testing", + "DB_HOST": "127.0.0.1", + "DB_PORT": "55441", + "DB_DATABASE": "offers_phase1_20261114_test", + "DB_USERNAME": "demography_qa", + "synthetic_app_key": true, + "mail": "array", + "queue": "sync" + } +} diff --git a/resources/js/pages/offers/preparation-index.tsx b/resources/js/pages/offers/preparation-index.tsx new file mode 100644 index 00000000..8a5ce767 --- /dev/null +++ b/resources/js/pages/offers/preparation-index.tsx @@ -0,0 +1,71 @@ +import { Head, Link } from '@inertiajs/react'; + +export default function OfferPreparations({ + records, + canCreate, +}: { + records: { + data: { + id: number; + title: string | null; + reference: string; + state: string; + }[]; + prev_page_url: string | null; + next_page_url: string | null; + }; + canCreate: boolean; +}) { + return ( +
+ +
+
+

+ As minhas ofertas +

+

+ Prepare e guarde as suas ofertas num espaço privado. +

+
+ {canCreate && ( + + Preparar oferta + + )} +
+ {!records.data.length && ( +

Ainda não existem ofertas neste âmbito.

+ )} +
    + {records.data.map((record) => ( +
  • + + {record.title || 'Oferta por completar'} + +

    + {record.reference} ·{' '} + {record.state === 'submitted' + ? 'Submissão privada registada' + : 'Rascunho privado'} +

    +
  • + ))} +
+ +
+ ); +} diff --git a/resources/js/pages/offers/preparation.tsx b/resources/js/pages/offers/preparation.tsx new file mode 100644 index 00000000..cf19623e --- /dev/null +++ b/resources/js/pages/offers/preparation.tsx @@ -0,0 +1,464 @@ +import { Head, Link, router } from '@inertiajs/react'; +import { useState } from 'react'; +import { + TerritoryFields, + type TerritoryOption, +} from '@/components/investment-territory'; +import OpportunityContactForm from '@/components/opportunity-contact-form'; +import { modalityLabels } from '@/components/opportunity-search-filters'; +import { createOperationKey } from '@/lib/operation-key'; +import { profileJson, ProfileValidationError } from '@/lib/profile-json'; + +type Offer = { + price_mode?: string | null; + amount?: string | null; + currency?: string | null; + contact_id?: string | null; +}; +type Context = { + organization_id: string | null; + organization_name: string | null; + type: string; +}; +type RecordData = { + id: number; + reference: string; + etag: string; + state: string; + canEdit: boolean; + organization_id: string | null; + territory: TerritoryOption[]; + values: { + title?: string; + description?: string; + opportunity_category: string; + private_offer?: Offer; + }; +}; +type Contact = { id: string; full_name: string }; +const control = 'w-full rounded-md border bg-background p-2 text-foreground'; +const button = + 'rounded-md bg-primary px-4 py-2 text-primary-foreground disabled:opacity-50'; +const emptyPath: TerritoryOption[] = []; + +export default function OfferPreparation({ + record, + contexts, + contacts, + contactInvestors, +}: { + record: RecordData | null; + contexts: Context[]; + contacts: Contact[]; + contactInvestors: { id: string; legal_name: string }[]; +}) { + const [saved, setSaved] = useState(record); + const [creationKey] = useState(createOperationKey); + const [context, setContext] = useState( + record + ? { + organization_id: record.organization_id, + organization_name: null, + type: record.values.opportunity_category, + } + : contexts[0], + ); + const [title, setTitle] = useState(record?.values.title ?? ''); + const [description, setDescription] = useState( + record?.values.description ?? '', + ); + const [offer, setOffer] = useState( + record?.values.private_offer ?? { + price_mode: null, + amount: null, + currency: null, + contact_id: null, + }, + ); + const [territory, setTerritory] = useState(record?.territory ?? emptyPath); + const [contactChoices, setContactChoices] = useState(contacts); + const [dirty, setDirty] = useState(!record); + const [busy, setBusy] = useState(false); + const [confirmed, setConfirmed] = useState(false); + const [errors, setErrors] = useState>({}); + const [message, setMessage] = useState(''); + const readOnly = saved !== null && !saved.canEdit; + function updateOffer(next: Offer) { + setOffer(next); + setDirty(true); + setConfirmed(false); + } + async function execute(submit: boolean) { + if (busy || readOnly || (submit && (!saved || dirty || !confirmed))) + return; + setBusy(true); + setErrors({}); + setMessage(''); + try { + const base = '/minhas-oportunidades/ofertas'; + const result = await profileJson( + submit + ? `${base}/${saved!.id}/submeter` + : saved + ? `${base}/${saved.id}/guardar` + : `${base}/rascunhos`, + submit + ? { etag: saved!.etag, confirm: confirmed } + : { + ...(saved + ? { etag: saved.etag } + : { creation_key: creationKey }), + title, + description, + opportunity_category: context.type, + organization_id: context.organization_id, + private_offer: offer, + province_id: territory[0]?.id ?? null, + district_id: territory[1]?.id ?? null, + admin_post_id: territory[2]?.id ?? null, + locality_id: territory[3]?.id ?? null, + reason: 'Preparação privada da oferta pelo promotor autorizado.', + }, + ); + setSaved(result.record); + setDirty(false); + setConfirmed(false); + setMessage( + submit + ? 'Submissão privada registada. A oferta não está publicada.' + : 'Oferta guardada. Pode retomá-la mais tarde.', + ); + if (!saved) router.visit(`${base}/${result.record.id}`); + } catch (error) { + setMessage( + error instanceof Error + ? error.message + : 'Não foi possível concluir. Os campos preenchidos foram preservados.', + ); + if (error instanceof ProfileValidationError) + setErrors(error.fields); + } finally { + setBusy(false); + } + } + return ( +
+ + + As minhas ofertas + +
+

Preparar oferta

+

+ {saved?.state === 'submitted' + ? 'Submissão privada registada. A oferta permanece sem publicação.' + : 'Complete os dados ao seu ritmo. A gravação e a submissão mantêm a oferta privada.'} +

+ {saved &&

{saved.reference}

} +
+
+ {message} +
+ {Object.keys(errors).length > 0 && ( +
+

Confira os seguintes campos:

+
    + {Object.entries(errors).map(([key, value]) => ( +
  • {value}
  • + ))} +
+
+ )} +
{ + event.preventDefault(); + void execute(false); + }} + className="space-y-6" + > +
+ + 1. Identificação + + + +