fix(sector): use effective creation access and preserve corridor presentation contract
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-28 21:35:21 +02:00
parent 502e3ec4d5
commit 789fffd098
12 changed files with 73 additions and 16 deletions
+1 -1
View File
@@ -17,8 +17,8 @@ final class InvestmentCorridorContext
/** Reuse Tempo's approved provincial context; never infer a district or master link. */
public function catalogueProvinces(Opportunity $record): array
{
if ($record->primary_territory_id || $record->additionalTerritories()->exists()) return [];
if (data_get($record->workflow_context, 'corridor_agribusiness.source_sha256') !== CorridorAgribusinessOpportunities::SOURCE_HASH) return [];
if ($record->primary_territory_id || $record->additionalTerritories()->exists()) return [];
$key = $this->key($record);
return $key ? $this->provinces($key) : [];
}
+3 -1
View File
@@ -47,6 +47,7 @@ final class SectorInvestment
public function publicView(Sector $sector): array
{
$creation = app(ContentManagement::class)->access(request()->user())['opportunities']['create'] ?? null;
$query = SectorOrganization::opportunities($sector);
$articles = $this->guides($sector);
$relations = [];
@@ -67,7 +68,8 @@ final class SectorInvestment
return ['articles' => $articles, 'relations' => collect($relations)->unique('id')->values()->all(), 'locations' => $locations,
'start_href' => app(InvestorPath::class)->available() ? '/investimentos/a-partir-do-setor/'.$sector->slug : null,
'start_notice' => app(InvestorPath::class)->available() ? null : 'O percurso para guardar a ideia está temporariamente indisponível. A consulta e o apoio continuam disponíveis.',
'register_href' => app(PortalProfileAccess::class)->available() ? '/minhas-oportunidades/create?sector_id='.$sector->id : null,
'register_href' => app(PortalProfileAccess::class)->available() ? ($creation ? $creation.'?sector_id='.$sector->id : '/perfil/promotor') : null,
'register_label' => $creation ? 'Cadastrar oportunidade — escolher modalidade' : 'Pedir acesso para cadastrar oportunidades',
'register_notice' => app(PortalProfileAccess::class)->available() ? null : 'O cadastro por perfis está temporariamente indisponível; pode preparar a ideia e contactar o apoio.',
'specialization_href' => in_array($sector->slug, ['tecnologias-de-informacao', 'mercado-financeiro'], true) ? SectorOrganization::href('fintech') : null,
'content_notice' => $sector->slug === 'mercado-financeiro' ? 'O artigo geral de Mercado Financeiro permanece vazio e não foi publicado. A preparação de negócios neste setor está disponível.' : null];
@@ -1799,3 +1799,10 @@ Formulários: entradas directas para os campos já existentes da preparação do
Provas delimitadas: 8 testes frontend de contrato/SSR e 3 testes isolados/12 verificações, zero falhas; tipos, compilação e sintaxe PHP passaram. Dados sintéticos revertidos em QA. Provas nacionais/selector/catálogo anteriores reutilizadas. Turismo: leitura anónima activa HTTP200 e QA visitante/conta sem Promotor HTTP200; a recusa da sessão real relatada não foi reproduzida, nem atribuída a uma permissão sem prova. A conferência autenticada e visual continua pendente. Este registo não instala o candidato nem encerra a implementação transversal. L8 permanece parcial.
Evidência: `investor-usability-qa-20261028.json`. Actualizados V2-01,V2-07,V2-10 na matriz existente; sem nova matriz ou backlog.
### 28/10 — botão de cadastro e compatibilidade do pipeline
A Tempo esclareceu que a recusa em Turismo ocorreu depois de clicar num botão; não foi possível recuperar o nome. A rota pública continuou HTTP200. Conferência de forma pública/origem não identificou anexos privados nos documentos públicos de Turismo; não se atribui a recusa à perda de publicação. Encontrada uma falha delimitada no consumidor: o botão de cadastro dependia da disponibilidade do módulo, sem a capacidade efectiva da conta. Corrigido para reutilizar o destino autorizado de criação de ContentManagement; sem essa capacidade, mostra expressamente o pedido de acesso já existente. Não cria papéis nem restringe a consulta pública ou a preparação de ideias. A confirmação visual da acção real permanece pendente.
O candidato 502e3ec4 falhou na Unit de agrupamento de corredores antes da troca de release; 6e8de43a ficou servido. Causa: a simulação do catálogo não representava a nova apresentação de localização e uma consulta era feita antes de conferir se a fonte era aplicável. Correcção: conferir primeiro a fonte e simular a nova dependência no teste, conservando as verificações anteriores de identidade, grupos e contagens. Regressão Unit1/13 passou; não foi alterado o pipeline, o contrato de schema ou a exigência dos testes. Frontend9, Feature3/14, tipos e compilação passaram. Rótulos conhecidos de temas, categorias e idiomas do acervo traduzidos para apresentação; valores dos filtros preservados.
@@ -18,13 +18,14 @@
"profile_request": "Portuguese visible representation/modality labels;owned clean-file criteria unchanged;file search and explicit selected proofs replace the unbounded list;availability does not prove evidentiary suitability",
"zones": "Territory and other filter changes apply search;old records hidden while loading/error;all and filtered totals plus pagination explicit;existing public visibility preserved;validated legal coverage includes its child area,while influence is not treated as a site",
"my_investments": "Next action remains prominent;other phases/stages expandable;source/code history and internal follow-up remain consultable rather than filling the main page",
"forms": "Direct entries to existing private investment preparation fields and Terra/DUAT;support request separately labelled;originals/guides optional;no law is converted into a form or executable calculation"
"forms": "Direct entries to existing private investment preparation fields and Terra/DUAT;support request separately labelled;originals/guides optional;no law is converted into a form or executable calculation",
"document_taxonomy": "Known topic/category/language codes have Portuguese display labels;original query values preserved"
},
"QA": {
"frontend_contract_and_SSR_tests": 8,
"frontend_contract_and_SSR_tests": 9,
"frontend_failures": 0,
"isolated_feature_tests": 3,
"feature_assertions": 12,
"feature_assertions": 14,
"feature_failures": 0,
"database": "proactive_investment_20261027_test",
"port": 55441,
@@ -32,7 +33,9 @@
"types": "passed",
"build": "passed",
"PHP_syntax": "passed",
"accepted_national_selector_and_catalogue_proofs": "reused"
"accepted_national_selector_and_catalogue_proofs": "reused",
"unit_regression_tests": 1,
"unit_regression_assertions": 13
},
"Tourism_incident": {
"reported_by_Tempo": "authenticated session saw access denial at /sectores/turismo",
@@ -40,7 +43,11 @@
"isolated_guest_and_no_promoter_account_HTTP": 200,
"exact_real_session_cause": "not demonstrated;do not infer missing role or claim correction",
"private_permissions_changed": false,
"next": "Confer the actual authenticated session and related logs;public read is not real-session proof"
"next": "Confer the actual authenticated session and related logs;public read is not real-session proof",
"reported_action": "Tempo clarified denial followed a button click;exact label could not be recalled",
"bounded_code_finding": "Sector registration action depended on module availability rather than effective account creation capability",
"QA_correction": "Reuse ContentManagement effective create destination;otherwise explicitly link to existing promoter request with a readable label;public consultation and idea preparation remain separate",
"real_session_reconference": "pending;the exact original button is not demonstrated"
},
"limits": [
"Not installed in this QA report",
@@ -54,5 +61,13 @@
"data_and_permissions_preservation": "permanent project instructions",
"grouping_and_collapsed_details": "implementation choice,not new organisational requirement",
"document_ownership_scan_and_existing_workflows": "prior contracts preserved"
},
"delivery_gate": {
"candidate": "502e3ec4",
"result": "failed before runtime switch;6e8de43a preserved",
"reason": "Unit mock did not represent new public location presentation;unnecessary relation query on non-source records",
"correction": "Short-circuit source applicability before reading relations;mock the new presentation dependency and retain prior identity/count assertions",
"unit_regression": "1 test/13 assertions passed",
"pipeline_guardrails": "unchanged;no schema migration or test-gate weakening"
}
}
@@ -7357,3 +7357,10 @@ Formulários: entradas directas para os campos já existentes da preparação do
Provas delimitadas: 8 testes frontend de contrato/SSR e 3 testes isolados/12 verificações, zero falhas; tipos, compilação e sintaxe PHP passaram. Dados sintéticos revertidos em QA. Provas nacionais/selector/catálogo anteriores reutilizadas. Turismo: leitura anónima activa HTTP200 e QA visitante/conta sem Promotor HTTP200; a recusa da sessão real relatada não foi reproduzida, nem atribuída a uma permissão sem prova. A conferência autenticada e visual continua pendente. Este registo não instala o candidato nem encerra a implementação transversal. L8 permanece parcial.
Evidência: `investor-usability-qa-20261028.json`. Actualizados V2-01,V2-07,V2-10 na matriz existente; sem nova matriz ou backlog.
### 28/10 — botão de cadastro e compatibilidade do pipeline
A Tempo esclareceu que a recusa em Turismo ocorreu depois de clicar num botão; não foi possível recuperar o nome. A rota pública continuou HTTP200. Conferência de forma pública/origem não identificou anexos privados nos documentos públicos de Turismo; não se atribui a recusa à perda de publicação. Encontrada uma falha delimitada no consumidor: o botão de cadastro dependia da disponibilidade do módulo, sem a capacidade efectiva da conta. Corrigido para reutilizar o destino autorizado de criação de ContentManagement; sem essa capacidade, mostra expressamente o pedido de acesso já existente. Não cria papéis nem restringe a consulta pública ou a preparação de ideias. A confirmação visual da acção real permanece pendente.
O candidato 502e3ec4 falhou na Unit de agrupamento de corredores antes da troca de release; 6e8de43a ficou servido. Causa: a simulação do catálogo não representava a nova apresentação de localização e uma consulta era feita antes de conferir se a fonte era aplicável. Correcção: conferir primeiro a fonte e simular a nova dependência no teste, conservando as verificações anteriores de identidade, grupos e contagens. Regressão Unit1/13 passou; não foi alterado o pipeline, o contrato de schema ou a exigência dos testes. Frontend9, Feature3/14, tipos e compilação passaram. Rótulos conhecidos de temas, categorias e idiomas do acervo traduzidos para apresentação; valores dos filtros preservados.
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -19,6 +19,7 @@ export type SectorEntryData = {
}[];
start_href: string | null;
register_href: string | null;
register_label?: string;
start_notice: string | null;
register_notice: string | null;
specialization_href: string | null;
@@ -58,7 +59,7 @@ export default function SectorEntry({
className="rounded border px-4 py-3"
href={data.register_href}
>
Cadastrar oportunidade — escolher modalidade
{data.register_label ?? 'Cadastrar oportunidade — escolher modalidade'}
</Link>
) : (
<p role="status">{data.register_notice}</p>
+8
View File
@@ -0,0 +1,8 @@
/** Presentation only. Original taxonomy identifiers remain the form/query values. */
const names:Record<string,string>={
agronegocio:'Agronegócio','bens-de-consumo-e-franquias':'Bens de consumo e franquias',energia:'Energia e mineração (catálogo histórico)',
'energia-especifica':'Energia',fintech:'Fintech',fiscalidade:'Fiscalidade',imobiliario:'Imobiliário',infraestruturas:'Infraestruturas',
iva:'IVA',mineracao:'Mineração','território':'Território',territorio:'Território',turismo:'Turismo','mercado-financeiro':'Mercado financeiro',
education:'Formação e competências',media:'Imagens e conteúdos de apoio',pt:'Português',en:'Inglês',
};
export function documentFacetLabel(value:string):string{return names[value]??value;}
+6 -5
View File
@@ -1,4 +1,5 @@
import { PublicSectorCards } from '@/components/public-sector-cards';
import { documentFacetLabel } from '@/lib/document-facet-label';
import { PORTAL_MORE_LINK } from '@/lib/portal-presentation';
import {
PublicDocumentContent,
@@ -579,7 +580,7 @@ export default function Services(p: Props) {
>
<option value="">Todos</option>
{p.topics?.map((topic) => (
<option key={topic}>{topic}</option>
<option key={topic} value={topic}>{documentFacetLabel(topic)}</option>
))}
</select>
</label>
@@ -606,7 +607,7 @@ export default function Services(p: Props) {
value={category}
>
{documentTypeLabels[category] ??
category}
documentFacetLabel(category)}
</option>
))}
</select>
@@ -809,8 +810,8 @@ export default function Services(p: Props) {
>
<option value="">Todos</option>
{p.languages?.map((language) => (
<option key={language}>
{language}
<option key={language} value={language}>
{documentFacetLabel(language)}
</option>
))}
</select>
@@ -917,7 +918,7 @@ export default function Services(p: Props) {
className="rounded-full border bg-white px-3 py-1 underline"
href={`${p.section === 'documents' ? '/documentos' : '/legislacao'}?${new URLSearchParams({ topic }).toString()}`}
>
{topic}
{documentFacetLabel(topic)}
</Link>
))}
</nav>
@@ -20,6 +20,10 @@ final class InvestorUsabilityScopeTest extends TestCase
$actor=User::factory()->create();
$this->actingAs($actor)->get('/sectores/turismo')->assertOk();
$this->assertSame(0,DB::table('portal_role_assignments')->where('user_id',$actor->id)->count());
config(['portal.profile_access_enabled'=>true]);
$entry=app(\App\Services\SectorInvestment::class)->publicView(\App\Models\Sector::where('slug','turismo')->firstOrFail());
$this->assertSame('/perfil/promotor',$entry['register_href']);
$this->assertSame('Pedir acesso para cadastrar oportunidades',$entry['register_label']);
}
public function test_zones_all_equals_visible_set_and_province_and_district_never_fall_back_to_all():void
@@ -24,6 +24,10 @@ class OpportunityCorridorExplorerTest extends TestCase
$catalogue->shouldReceive('query')->with($filters)->once()->andReturn($query);
$catalogue->shouldReceive('context')->with(101)->once()->andReturn(['corridors' => collect([(object)['id'=>'a','name'=>'Corredor A'],(object)['id'=>'b','name'=>'Corredor B']])]);
$catalogue->shouldReceive('context')->with(102)->once()->andReturn(['corridors'=>collect()]);
$first->setAttribute('territorial_summary','Sofala / Dondo');
$other->setAttribute('territorial_summary','Localização não indicada');
$catalogue->shouldReceive('presentLocation')->with($first)->once()->andReturn($first);
$catalogue->shouldReceive('presentLocation')->with($other)->once()->andReturn($other);
$this->app->instance(InvestmentCatalogue::class, $catalogue);
$groups = app(OpportunityCorridorExplorer::class)->groups($filters);
$this->assertSame(['Corredor A','Corredor B','Outras localizações'], array_column($groups,'name'));
@@ -32,5 +36,6 @@ class OpportunityCorridorExplorerTest extends TestCase
$this->assertSame(['Sofala / Dondo'], $groups[0]['locations']);
$this->assertSame([], $groups[2]['locations']);
$this->assertSame([], $groups[0]['opportunities'][0]['stages']);
$this->assertSame('Sofala / Dondo',$groups[0]['opportunities'][0]['territorial_summary']);
}
}
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import {opportunityFillGroups,opportunityErrorGroup,opportunityMissingGroups} from '../../resources/js/lib/opportunity-fill-flow.ts';
import {territorialSearchParams} from '../../resources/js/lib/territorial-search-params.ts';
import {documentFacetLabel} from '../../resources/js/lib/document-facet-label.ts';
test('adaptive groups keep every applicable schema field once, including new specialized fields',()=>{
const keys=['official_title','locations','sector_id','documents','currency','real_estate','future_schema_field','currency'];
@@ -26,3 +27,9 @@ test('territorial search replaces old descendants while retaining all selected n
const all=territorialSearchParams(entries,[]);assert.equal(all.has('province_id'),false);assert.equal(all.get('category'),'zee');
const unchanged=territorialSearchParams(entries);assert.equal(unchanged.get('district_id'),'old-child');
});
test('document facets have readable labels while original query identifiers remain unchanged',()=>{
const original=['agronegocio','education','media','pt','turismo'];
assert.deepEqual(original.map(documentFacetLabel),['Agronegócio','Formação e competências','Imagens e conteúdos de apoio','Português','Turismo']);
assert.deepEqual(original,['agronegocio','education','media','pt','turismo']);
assert.equal(documentFacetLabel('Tema definido pela Tempo'),'Tema definido pela Tempo');
});