Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e69ab1499f | ||
|
|
3258a25f76 |
@@ -27,6 +27,7 @@ class InvestmentDiscoveryController extends Controller
|
||||
|
||||
return Inertia::render('investment/catalogue', ['filters' => $filters, 'catalogueUrl' => $request->routeIs('investment.education') ? '/educacao' : '/oportunidades', 'references' => $catalogue->references(false, $filters),
|
||||
'territorySelection' => app(InvestmentGeography::class)->selection($filters),
|
||||
'guides' => app(\App\Services\SectorInvestment::class)->guides(\App\Models\Sector::where('slug', $filters['sector'] ?? '')->where('is_active', true)->whereNotNull('published_at')->first()),
|
||||
'results' => $catalogue->query($filters)->paginate(12)->withQueryString(),
|
||||
'favourites' => $request->user() ? DB::table('opportunity_favourites')->where('user_id', $request->user()->id)->pluck('opportunity_id') : [],
|
||||
'territorial' => $this->territorial($filters)]);
|
||||
@@ -106,6 +107,7 @@ class InvestmentDiscoveryController extends Controller
|
||||
unset($returnFilters['zone_id'], $returnFilters['from_dossier']);
|
||||
|
||||
return Inertia::render('investment/detail', ['record' => $record, 'context' => $catalogue->context($record->id), 'zone_origin' => $origin,
|
||||
'guides' => app(\App\Services\SectorInvestment::class)->guides($record->sector),
|
||||
'dossierOrigin' => $dossierOrigin ? ['id' => $dossierOrigin->id, 'title' => $dossierOrigin->title] : null,
|
||||
'returnUrl' => $returnFilters === [] ? '/oportunidades' : '/oportunidades?'.http_build_query($returnFilters)]);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ final class LegacyContent
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function article(string $slug = 'agronegocio'): array
|
||||
public function article(string $slug = 'agronegocio', bool $includeRelated = true): array
|
||||
{
|
||||
abort_unless($this->available(), 404);
|
||||
$identity = config('legacy_content.articles.'.$slug);
|
||||
@@ -80,13 +80,26 @@ final class LegacyContent
|
||||
}
|
||||
preg_match_all('~href="([^"]+)"~i', $localHtml, $existingLinks);
|
||||
$alreadyLinked = array_map(fn ($href) => html_entity_decode($href, ENT_QUOTES | ENT_HTML5, 'UTF-8'), $existingLinks[1]);
|
||||
$relatedArticles = array_values(array_filter($this->related($identity['group'], $slug), fn ($item) => !in_array($item['href'], $alreadyLinked, true)));
|
||||
$relatedArticles = [];
|
||||
if ($includeRelated) {
|
||||
foreach ($this->related($identity['group'], $slug) as $item) {
|
||||
if (in_array($item['href'], $alreadyLinked, true)) continue;
|
||||
try {
|
||||
$relatedArticles[] = $this->article($item['slug'], false);
|
||||
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $error) {
|
||||
if ($error->getStatusCode() !== 404) throw $error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['id' => (int) $row->resource_id, 'document_id' => $row->id, 'version' => (int) $row->metadata_version,
|
||||
'sha256' => trim($row->sha256), 'title' => $source['title'], 'author' => $source['author'],
|
||||
'legacy_id' => $source['legacy_id'], 'legacy_category' => $source['legacy_category'],
|
||||
'published_at' => $source['published_at'], 'modified_at' => $source['modified_at'], 'source_url' => $source['source_url'],
|
||||
'href' => '/'.$slug, 'html' => $localHtml,
|
||||
'href' => '/'.$slug, 'html' => $localHtml, 'summary' => self::excerpt($localHtml),
|
||||
'opportunities_href' => $sector ? '/oportunidades?sector='.rawurlencode($sector->slug) : null,
|
||||
'opportunities_label' => $sector ? 'Ver oportunidades de '.$sector->name : null,
|
||||
'has_opportunities' => count($opportunities) > 0,
|
||||
'landing_href' => '/explorar/'.$identity['group'], 'start_href' => '/investimentos/a-partir-de/'.$slug,
|
||||
'group' => config('legacy_content.groups.'.$identity['group']),
|
||||
'related_articles' => $relatedArticles,
|
||||
@@ -101,6 +114,22 @@ final class LegacyContent
|
||||
'consultation_state' => 'public', 'legal_state' => 'not_applicable', 'calculation_use' => false];
|
||||
}
|
||||
|
||||
/** An excerpt of the displayed edition, never a claim about returns or availability. */
|
||||
public static function excerpt(string $html): string
|
||||
{
|
||||
$plain = html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
if (str_contains($plain, 'Hotelaria e resorts') && str_contains($plain, 'Ecoturismo')
|
||||
&& str_contains($plain, 'Infraestruturas turísticas') && str_contains($plain, 'Oportunidades Regionais')) {
|
||||
return 'Conheça os temas de turismo e hospitalidade abordados no guia: hotelaria, ecoturismo, infraestruturas de apoio e referências regionais. Use este enquadramento para preparar a sua ideia de investimento.';
|
||||
}
|
||||
preg_match_all('~<p\b[^>]*>(.*?)</p>~isu', $html, $paragraphs);
|
||||
foreach ([...($paragraphs[1] ?? []), $html] as $paragraph) {
|
||||
$text = trim(preg_replace('/\s+/u', ' ', html_entity_decode(strip_tags($paragraph), ENT_QUOTES | ENT_HTML5, 'UTF-8')));
|
||||
if (mb_strlen($text) >= 60) return \Illuminate\Support\Str::limit($text, 320);
|
||||
}
|
||||
return trim(html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
||||
}
|
||||
|
||||
/** Resolve a library identity, preserving the already stored source URLs and versions. */
|
||||
/** @return array<string, mixed> */
|
||||
public function articleForResource(int $resourceId): array
|
||||
|
||||
@@ -37,19 +37,7 @@ final class SectorInvestment
|
||||
public function publicView(Sector $sector): array
|
||||
{
|
||||
$query = SectorOrganization::opportunities($sector);
|
||||
$articles = [];
|
||||
if (app(LegacyContent::class)->available()) {
|
||||
foreach (self::CONTENT[$sector->slug] ?? [] as $slug) {
|
||||
try {
|
||||
$a = app(LegacyContent::class)->article($slug);
|
||||
$articles[] = ['title' => $a['title'], 'href' => $a['href'], 'image' => $a['image'], 'sha256' => $a['sha256'], 'scope' => $slug === 'tech-fintech' && $sector->slug !== 'fintech' ? 'Especialização Fintech; não representa todo o setor.' : 'Conteúdo educativo relacionado; não é uma oportunidade concreta.'];
|
||||
} catch (HttpException $e) {
|
||||
if ($e->getStatusCode() !== 404) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$articles = $this->guides($sector);
|
||||
$relations = [];
|
||||
foreach (['projects' => 'project_id', 'clusters' => 'cluster_id', 'corridors' => 'corridor_id'] as $table => $foreign) {
|
||||
$ids = DB::table('opportunity_'.$table)->whereIn('opportunity_id', (clone $query)->select('id'))->pluck($foreign);
|
||||
@@ -74,6 +62,26 @@ final class SectorInvestment
|
||||
'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];
|
||||
}
|
||||
|
||||
/** Published guides for an explicit sector; never inferred from proximity. */
|
||||
public function guides(?Sector $sector): array
|
||||
{
|
||||
if (! $sector) return [];
|
||||
$articles = [];
|
||||
if (app(LegacyContent::class)->available()) {
|
||||
foreach (self::CONTENT[$sector->slug] ?? [] as $slug) {
|
||||
try {
|
||||
$a = app(LegacyContent::class)->article($slug, false);
|
||||
$articles[] = \Illuminate\Support\Arr::only($a, ['title', 'href', 'image', 'sha256', 'summary', 'author', 'opportunities_href', 'opportunities_label', 'has_opportunities']);
|
||||
} catch (HttpException $e) {
|
||||
if ($e->getStatusCode() !== 404) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $articles;
|
||||
}
|
||||
|
||||
public function attach(User $actor, string $id, array $input): void
|
||||
{
|
||||
$data = Validator::make($input, ['version' => 'required|integer|min:1', 'sector_slug' => 'required|string|max:150'])->validate();
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"branch": "codex/educational-content-cards",
|
||||
"base": "0e7d51f18fb424857fdaad48498a2fc43c044749",
|
||||
"scope": "New QA delta; separate Tempo review before main",
|
||||
"files": [
|
||||
{
|
||||
"path": "app/Http/Controllers/InvestmentDiscoveryController.php",
|
||||
"sha256": "693d1217ead894747c266a7295774b26a8b987f8faf06003ed60a6d3e15aca6c"
|
||||
},
|
||||
{
|
||||
"path": "app/Services/LegacyContent.php",
|
||||
"sha256": "e856ba2a042e971933a401fd9544a50f6bc5c1541c28062719d840a8277da7b8"
|
||||
},
|
||||
{
|
||||
"path": "app/Services/SectorInvestment.php",
|
||||
"sha256": "a0ee1c9bf864ef765ed70fc1bcdc839781d0bed7d705d991c801bfaa9bf7e4a0"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/unification/traceability-and-manual.md",
|
||||
"sha256": "bbbb79e84cae659d1921dfa4440e3fcc192521e0976cace0fa2694828219ec00"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/v2/auditoria-portal/backlog-priorizado.csv",
|
||||
"sha256": "420369f562508f68ea50e78d386721d6b62fa730893c4e6ccd06817f4dfb8a27"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/v2/auditoria-portal/backlog.md",
|
||||
"sha256": "d77e4931e8f1a0fcbf0f1d80be83a54b22682ffdc0286852d3ad167652056156"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv",
|
||||
"sha256": "757d457a295eb9a516abd1eec6002a52aa1d9a35faea291046e3c99681a1cdcb"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/educational-article-card.tsx",
|
||||
"sha256": "03a298406702cd3211c44fad1fdf79d455ad029d997d417a416f450d6d420b6e"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/educational-guides.tsx",
|
||||
"sha256": "2a2a52e50694e336f05544d51fc776a3bbc2b06d2ab61a1b3cdf1d261afca474"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/sector-entry.tsx",
|
||||
"sha256": "39004abf04f096b987d852d834ce9b27fdf557d2283db3b5868678d3ff0bff1a"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/pages/investment/catalogue.tsx",
|
||||
"sha256": "ced5e8d4b2910aeeaeb709d6ff0eb2d492f96d9b671d6ff832eea66d022a1f70"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/pages/investment/detail.tsx",
|
||||
"sha256": "719b3c008887676dd0b69ffe48bd5a64426de92362d92da8ad39412a578c51bc"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/pages/portal/legacy.tsx",
|
||||
"sha256": "dcd8462e940614f5935efa19d9c9444fe3432fbc704e9c05144508d5c944c90d"
|
||||
},
|
||||
{
|
||||
"path": "tests/Feature/EducationalGuidesTest.php",
|
||||
"sha256": "aa9e3f62890f1d75f639ad4e7e05713c18112ec96a7fd49c864e5e007807d3cf"
|
||||
}
|
||||
],
|
||||
"build_manifest_sha256": "89c3ce0c67d9faf8cb3c5f7e9d74b3ad5d0ad70dff81ca2e37ca5cac7a471620",
|
||||
"migrations": [],
|
||||
"imports": [],
|
||||
"publication": false,
|
||||
"remote_operations": [],
|
||||
"ai": false,
|
||||
"evidence": {
|
||||
"backend": "1 test, 8 assertions; exact isolated QA guard and transactions",
|
||||
"types": "passed",
|
||||
"build": "passed",
|
||||
"desktop_width": 2048,
|
||||
"mobile_width": 390,
|
||||
"mobile_document_width": 375,
|
||||
"keyboard": "visible focus and Enter opened guide",
|
||||
"visual_data": "Read-only copy of existing public Tourism article/image; not synthetic",
|
||||
"real_library": "Normal sector -> local image 25 and source summary -> normal article /turismo -> library resource 10; existing isolated QA; no import"
|
||||
},
|
||||
"limitations": [
|
||||
"Tempo visual review pending before main integration; prior accepted dossier proof retained, not repeated"
|
||||
],
|
||||
"rollback": "Restore previous sources/build through existing deployment chain, preserving database/uploads/permissions and later data"
|
||||
}
|
||||
@@ -1521,3 +1521,25 @@ Rever a branch e o manifesto nominal; reconciliar novos commits dos programadore
|
||||
Não há migrations, importação ou publicação neste pacote. A reversão repõe as fontes/build da revisão anterior através da cadeia existente, preservando a base, uploads, referências, permissões e dados posteriores. Os helpers de contexto só transportam query local; não aceitam URL externa como destino de retorno. O manifesto lista fontes e hash do build QA; não inclui `.env`, credenciais, dumps, vendor/node_modules ou fixtures persistentes.
|
||||
|
||||
Conferência final dos consumidores: negócios também conferido no navegador com pesquisa vazia; retirado o resumo local repetido, conservando o resumo partilhado. Fintech é identificado como especialização do Mercado Financeiro também nesse selector. Os cartões de zonas traduzem categoria, desenvolvimento, disponibilidade e estado de actividade, preservando os códigos e significados persistidos. A vista combinada não repete na lista de não localizados a ficha que já aparece na lista principal.
|
||||
|
||||
|
||||
## Guias educativos relacionados — 20/10/2026
|
||||
|
||||
Delta independente de `0e7d51f18fb4`, que já foi integrado e confirmado na homologação. Este incremento permanece na branch `codex/educational-content-cards`, em QA, sujeito a revisão própria. Origem: pedido expresso da Tempo; nenhuma nova regra organizacional.
|
||||
|
||||
O componente `EducationalArticleCard` usa imagem pública autorizada, título original, resumo da edição apresentada, autoria e «Ler o guia». Turismo descreve hotelaria, ecoturismo, infraestruturas e referências regionais presentes no artigo, sem repetir metas financeiras como promessa. Outros artigos usam um excerto do texto efectivo. A apresentação não altera originais, fontes, autoria, direitos, versões, publicação ou scan. A imagem continua sujeita aos filtros públicos existentes. A acção secundária só surge quando a consulta de oportunidades públicas do sector tem resultados; não se infere disponibilidade por menção no artigo.
|
||||
|
||||
Consumidores: sector (`SectorEntry`), exploração editorial e leituras relacionadas (`portal/legacy`), catálogo de oportunidades com sector explícito (incluindo vista territorial/mapa) e ficha de oportunidade com sector. Um filtro de localização sem sector não determina um artigo; não se inventaram associações territoriais. Nos restantes catálogos territoriais não foi encontrado um cartão editorial equivalente a substituir. Não se alteraram cartões de anúncios, documentos ou entidades que não sejam recomendações de artigos.
|
||||
|
||||
Após a leitura, mantém-se «Iniciar ou continuar uma ideia», com `/investimentos/a-partir-de/{slug}` e o contexto autorizado do dossier quando existente. Não há novo percurso, conta ou privilégio. As provas anteriores de gravação documental mantêm-se; não foram repetidas.
|
||||
|
||||
Provas novas: `EducationalGuidesTest`, isolado por guarda QA, transacções e armazenamento fictício do teste, 1 teste / 8 asserções (excerto, autoria, destino, percurso, ausência de resultados, fonte não publicada e scan pendente). TypeScript, sintaxe PHP e build passaram. Capturas a 2048 px e 390 px; a 390 px, conteúdo com 375 px, sem transbordo horizontal, foco visível e Enter no botão «Ler o guia». Imagem real carregada com 900 px de largura natural. A prévia HTTP `http://127.0.0.1:18570/qa/guias/turismo` usa uma cópia por leitura do artigo/imagem já públicos no destino; não usa texto ou imagem sintéticos nem escreve registos. O router privado e a cópia ficam fora do Git. O destino não foi alterado.
|
||||
|
||||
Limitação: o artigo real não existe na base QA actual. A prévia permite conferir cartão e leitura; não comprova recuperação desse artigo num dossier autenticado nessa base. O URL original do percurso foi preservado e conferido pelo teste. Não se importaram dados ou fabricaram resultados para preencher essa lacuna. Capturas em `outputs/educational-guides-20261020/desktop.jpg` e `mobile.jpg` (ficheiros locais de prova).
|
||||
|
||||
Gestão: editar e publicar pelos ecrãs existentes da biblioteca/editorial; o resumo acompanha a edição apresentada. A acção de oportunidades é calculada pelos registos publicados, não pelo editor. Disponibilização futura: revisão própria e integração em `main` pelo processo automático, após decisão. Sem migrations, importação ou publicação neste delta. Reversão pelas fontes/build anteriores, conservando base, ficheiros, permissões e dados posteriores. IA, oito lacunas contratuais e P7 separados.
|
||||
|
||||
|
||||
### Library integration evidence supplement - 20/10/2026
|
||||
|
||||
Functional code remains 3258a25f7659. Existing QA database legacy_trio_20261015_test and its original local storage were selected by an isolated loopback harness; no import, publication or application route override. Normal /sectores/turismo rendered the source-derived summary and image /documentos/25/view (HTTP 200; 900px natural width). Keyboard Enter on Ler o guia opened normal /turismo; the article links to library resource /documentos/10 (source SHA256 8cab862be245382dd4c1fd5fcb423c47aed7be3a22e0a7f8d5521510cc36bc72). Thus the real-library integration limitation of the earlier copied preview is resolved. URLs: http://127.0.0.1:18572/sectores/turismo and /turismo. Evidence: outputs/educational-guides-20261020/real-library-receipt.json, real-library-sector.jpg and real-library-guide.jpg. No dossier path/context change; previous accepted dossier proof retained, not repeated. Tempo visual review remains pending before main integration. AI excluded. This supplement changes evidence/documentation only, not functional code.
|
||||
|
||||
@@ -7381,3 +7381,4 @@ P1,AUD-SEARCH-T08,QA — revisão Tempo pendente,Nenhuma alteração de relaçõ
|
||||
P1,AUD-SEARCH-T09,QA — revisão Tempo pendente,Gestão de procura/capacidade permanece separada e protegida; sem alteração de permissões.,Reutiliza integralmente a pesquisa corrigida com âmbito educação mantido.,,HTTP/Inertia: results.total=0 com texto inexistente; filtros e componente comuns.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,investment/catalogue,Gestão de procura/capacidade permanece separada e protegida; sem alteração de permissões.,,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,HTTP/Inertia: results.total=0 com texto inexistente; filtros e componente comuns.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
|
||||
P1,AUD-SEARCH-T10,QA — revisão Tempo pendente,Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,"Resumo único nomeado Documentos, chips removíveis, controlos vazios ocultos sem apagar valores seleccionados, rodapé comum.",,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,portal/services; portal/document,Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
|
||||
P1,AUD-SEARCH-T11,QA — revisão Tempo pendente,"Situação jurídica, consulta e cálculo continuam independentes.",Reutiliza a correcção da biblioteca e preserva filtros jurídicos/documentais pertinentes.,,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,portal/services; portal/document,"Situação jurídica, consulta e cálculo continuam independentes.",,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
|
||||
P1,AUD-GUIDE-CARD-20261020,QA review pending,New delta; not approved for main,Common educational card and existing source/context adapter,,Desktop/mobile/keyboard; public-state and source checks,,,,,,educational presentation,Tempo 20/10/2026; same manual,,,Tempo visual review; library integration verified on existing QA; previous dossier proof retained,,Integration approval for new delta,,,,1 test / 8 assertions; TypeScript/build; real public-source visual preview,,,,No import; no remote changes; no claim of new dossier proof,
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -2990,3 +2990,10 @@ Provas: outputs/investmoz-git-delivery-20261020/RECIBO-DISPONIBILIZACAO-GIT-MAIN
|
||||
## Pesquisa transversal QA — 20/10/2026
|
||||
|
||||
Inventário finito AUD-SEARCH-T01–T11 na mesma matriz/CSV e comparação no manual. Correcções manuais na branch `codex/opportunity-search`, sem integração/remoto/publicação. Assistência NLP ainda não implementada: interpretador e prévia/confirmar da mesma query manual, 6–10 h para recorte local delimitado; modelo externo sujeito a decisão. As oito lacunas contratuais e P7 mantêm-se separados. Revisão Tempo pendente; as provas comuns não encerram requisitos de gestão ou publicação não alterados.
|
||||
|
||||
|
||||
## Guias educativos relacionados — delta QA após 0e7d51f18fb4
|
||||
|
||||
Verificação complementar concluída: código funcional `3258a25f7659` ligado ao acervo QA existente `legacy_trio_20261015_test`, sem importação. Página normal `/sectores/turismo` apresenta imagem local `/documentos/25/view` (HTTP 200; 900 px naturais) e resumo; «Ler o guia» abre `/turismo`, ligado ao exemplar real `/documentos/10`. A limitação de integração com biblioteca indicada na primeira prévia fica superada por esta prova. Mantém-se a prova anterior do dossier, pois código/contexto não mudaram. Falta apenas revisão visual da Tempo antes de integrar. IA fora do lote.
|
||||
|
||||
AUD-GUIDE-CARD-20261020: componente comum com imagem autorizada, título, resumo do artigo publicado, autoria e «Ler o guia». Reutilizado em sectores, exploração editorial e leituras relacionadas; recomendado no catálogo por sector explícito e nas fichas de oportunidades. Sem associação territorial inferida. Botão secundário apenas com oportunidades públicas. QA: teste isolado 1/8, TypeScript/build e exemplos computador/móvel. Falta decisão sobre integração deste novo delta; a prévia visual de Turismo usa uma cópia por leitura do conteúdo público, sem importação. Recuperação documental num dossier com este acervo não demonstrada, pois o artigo não existe na base QA actual. Percurso e regras anteriores preservados; IA fora do lote.
|
||||
|
||||
@@ -10650,3 +10650,4 @@ AUD-SEARCH-T08,Consistência transversal da pesquisa e navegação — Corredore
|
||||
AUD-SEARCH-T09,Consistência transversal da pesquisa e navegação — Educação,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/educacao,investment/catalogue — Reutiliza integralmente a pesquisa corrigida com âmbito educação mantido.,Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,HTTP/Inertia: results.total=0 com texto inexistente; filtros e componente comuns.,QA — delta delimitado; revisão Tempo pendente,Gestão de procura/capacidade permanece separada e protegida; sem alteração de permissões.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,Herda formulário e contagens concorrentes das oportunidades.,Gestão de procura/capacidade permanece separada e protegida; sem alteração de permissões.,investment/catalogue; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,HTTP/Inertia: results.total=0 com texto inexistente; filtros e componente comuns.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Gestão de procura/capacidade permanece separada e protegida; sem alteração de permissões.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
|
||||
AUD-SEARCH-T10,Consistência transversal da pesquisa e navegação — Biblioteca,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/documentos,"portal/services; portal/document — Resumo único nomeado Documentos, chips removíveis, controlos vazios ocultos sem apagar valores seleccionados, rodapé comum.",Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,QA — delta delimitado; revisão Tempo pendente,Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,"Dois resumos de contagem, filtros activos sem remoção individual, controlos sem opções e rodapé local repetido.",Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,portal/services; portal/document; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
|
||||
AUD-SEARCH-T11,Consistência transversal da pesquisa e navegação — Legislação,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/legislacao,portal/services; portal/document — Reutiliza a correcção da biblioteca e preserva filtros jurídicos/documentais pertinentes.,Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,QA — delta delimitado; revisão Tempo pendente,"Situação jurídica, consulta e cálculo continuam independentes.",,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,Mesmo padrão da biblioteca; não deve expor um universo concorrente.,"Situação jurídica, consulta e cálculo continuam independentes.",portal/services; portal/document; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,"Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Situação jurídica, consulta e cálculo continuam independentes.",,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
|
||||
AUD-GUIDE-CARD-20261020,"Related educational cards: image, source summary, clear actions",Tempo 20/10/2026; independent delta after 0e7d51f18fb4,educational presentation,/sectores/{slug}; /explorar/{group}; articles; /oportunidades and detail,EducationalArticleCard; EducationalGuides; LegacyContent; SectorInvestment,QA only,1 test / 8 assertions; PHP syntax; TypeScript/build; desktop2048/mobile390; keyboard,QA review pending,Normal QA sector and article verified against existing library resource 10 and local image 25; previous dossier proof retained,,Source-derived summary; public images; conditional opportunity action; context preserved,P1,,,,,,,,,,,,,,,,,,presentation,,,,20/10/2026,,,,,,,,,,,,content; navigation; visual presentation,,Ready for QA review
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -0,0 +1,73 @@
|
||||
import { Link } from '@inertiajs/react';
|
||||
|
||||
export type EducationalArticle = {
|
||||
title: string;
|
||||
href: string;
|
||||
summary: string;
|
||||
author?: string;
|
||||
image: { href: string; alt: string } | null;
|
||||
opportunities_href?: string | null;
|
||||
opportunities_label?: string | null;
|
||||
has_opportunities?: boolean;
|
||||
};
|
||||
|
||||
export default function EducationalArticleCard({
|
||||
article,
|
||||
}: {
|
||||
article: EducationalArticle;
|
||||
}) {
|
||||
const focus =
|
||||
'focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-emerald-800';
|
||||
return (
|
||||
<article className="flex min-w-0 flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white text-slate-900 shadow-sm">
|
||||
{article.image && (
|
||||
<img
|
||||
className="aspect-[16/9] w-full object-cover"
|
||||
src={article.image.href}
|
||||
alt={article.image.alt}
|
||||
loading="lazy"
|
||||
width={800}
|
||||
height={450}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-1 flex-col items-start gap-4 p-5 sm:p-6">
|
||||
<p className="text-sm font-medium text-emerald-800">
|
||||
Guia sectorial
|
||||
</p>
|
||||
<h3 className="text-2xl leading-tight font-bold">
|
||||
<Link href={article.href} className={focus}>
|
||||
{article.title}
|
||||
</Link>
|
||||
</h3>
|
||||
{article.summary && (
|
||||
<p className="text-base leading-relaxed text-slate-700">
|
||||
{article.summary}
|
||||
</p>
|
||||
)}
|
||||
{article.author && (
|
||||
<p className="text-sm text-slate-600">
|
||||
Por {article.author}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-auto flex flex-wrap items-center gap-x-5 gap-y-3 pt-2">
|
||||
<Link
|
||||
href={article.href}
|
||||
className={`inline-flex min-h-11 items-center rounded-lg bg-emerald-900 px-5 py-3 font-semibold text-white hover:bg-emerald-800 ${focus}`}
|
||||
>
|
||||
Ler o guia
|
||||
</Link>
|
||||
{article.has_opportunities &&
|
||||
article.opportunities_href && (
|
||||
<Link
|
||||
href={article.opportunities_href}
|
||||
className={`inline-flex min-h-11 items-center py-2 font-semibold text-emerald-900 underline underline-offset-4 ${focus}`}
|
||||
>
|
||||
{article.opportunities_label ??
|
||||
'Ver oportunidades'}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import EducationalArticleCard, {
|
||||
type EducationalArticle,
|
||||
} from './educational-article-card';
|
||||
|
||||
export default function EducationalGuides({
|
||||
articles,
|
||||
}: {
|
||||
articles: EducationalArticle[];
|
||||
}) {
|
||||
if (!articles.length) return null;
|
||||
return (
|
||||
<section
|
||||
aria-label="Guias para preparar o investimento"
|
||||
className="space-y-5"
|
||||
>
|
||||
<h2 className="text-2xl font-bold">Descubra onde investir</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{articles.map((article) => (
|
||||
<EducationalArticleCard
|
||||
key={article.href}
|
||||
article={article}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Link } from '@inertiajs/react';
|
||||
import EducationalArticleCard, {
|
||||
type EducationalArticle,
|
||||
} from './educational-article-card';
|
||||
export type SectorEntryData = {
|
||||
articles: {
|
||||
title: string;
|
||||
href: string;
|
||||
image: { href: string; alt: string } | null;
|
||||
scope: string;
|
||||
}[];
|
||||
articles: EducationalArticle[];
|
||||
relations: {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -66,9 +64,7 @@ export default function SectorEntry({ data }: { data: SectorEntryData }) {
|
||||
perfil e a modalidade já autorizados; se ainda não tem perfil,
|
||||
pode preparar a ideia e pedir o perfil de promotor.
|
||||
</p>
|
||||
<h2 className="text-xl font-bold">
|
||||
Conteúdo e possibilidades de negócio
|
||||
</h2>
|
||||
<h2 className="text-xl font-bold">Descubra onde investir</h2>
|
||||
{data.content_notice && <p>{data.content_notice}</p>}
|
||||
{!data.articles.length && (
|
||||
<p>
|
||||
@@ -78,22 +74,7 @@ export default function SectorEntry({ data }: { data: SectorEntryData }) {
|
||||
)}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{data.articles.map((a) => (
|
||||
<article
|
||||
className="min-w-0 rounded border p-4"
|
||||
key={a.href}
|
||||
>
|
||||
{a.image && (
|
||||
<img
|
||||
className="mb-3 h-36 w-full object-cover"
|
||||
src={a.image.href}
|
||||
alt={a.image.alt}
|
||||
/>
|
||||
)}
|
||||
<Link className="font-semibold underline" href={a.href}>
|
||||
{a.title}
|
||||
</Link>
|
||||
<p>{a.scope}</p>
|
||||
</article>
|
||||
<EducationalArticleCard key={a.href} article={a} />
|
||||
))}
|
||||
</div>
|
||||
{data.specialization_href && (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import EducationalGuides from '@/components/educational-guides';
|
||||
import type { EducationalArticle } from '@/components/educational-article-card';
|
||||
import OpportunitySearchFilters from '@/components/opportunity-search-filters';
|
||||
import SearchState from '@/components/search-state';
|
||||
import { Link, router, useForm, usePage } from '@inertiajs/react';
|
||||
@@ -14,6 +16,7 @@ type Feature = {
|
||||
properties: { id: number; title: string; location: string; url: string };
|
||||
};
|
||||
export default function Catalogue({
|
||||
guides = [],
|
||||
filters,
|
||||
references,
|
||||
results,
|
||||
@@ -22,6 +25,7 @@ export default function Catalogue({
|
||||
territorySelection,
|
||||
catalogueUrl,
|
||||
}: {
|
||||
guides?: EducationalArticle[];
|
||||
filters: Record<string, string | string[] | boolean>;
|
||||
references: References;
|
||||
results: {
|
||||
@@ -712,6 +716,7 @@ export default function Catalogue({
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<EducationalGuides articles={guides} />
|
||||
</Frame>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import EducationalGuides from '@/components/educational-guides';
|
||||
import type { EducationalArticle } from '@/components/educational-article-card';
|
||||
import { ClassificationSummary } from '@/components/opportunity-classification';
|
||||
import { Link, router, usePage } from '@inertiajs/react';
|
||||
import { useState } from 'react';
|
||||
@@ -10,12 +12,14 @@ import TerritorialSources from '@/components/territorial-source-evidence';
|
||||
import { Frame, card, button, money, groupLabels } from './shared';
|
||||
import type { Opportunity, Context } from './shared';
|
||||
export default function Detail({
|
||||
guides = [],
|
||||
record: r,
|
||||
context: c,
|
||||
zone_origin,
|
||||
returnUrl = '/oportunidades',
|
||||
dossierOrigin,
|
||||
}: {
|
||||
guides?: EducationalArticle[];
|
||||
record: Opportunity;
|
||||
context: Context;
|
||||
zone_origin?: { id: string; name: string } | null;
|
||||
@@ -309,6 +313,7 @@ export default function Detail({
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
<EducationalGuides articles={guides} />
|
||||
</Frame>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { Head, Link, useForm } from '@inertiajs/react';
|
||||
import EducationalArticleCard, {
|
||||
type EducationalArticle,
|
||||
} from '@/components/educational-article-card';
|
||||
|
||||
export type LegacyArticle = {
|
||||
summary: string;
|
||||
opportunities_href?: string | null;
|
||||
opportunities_label?: string | null;
|
||||
has_opportunities?: boolean;
|
||||
id: number;
|
||||
title: string;
|
||||
author: string;
|
||||
@@ -13,12 +20,7 @@ export type LegacyArticle = {
|
||||
landing_href: string;
|
||||
start_href: string;
|
||||
group: { label: string; heading: string };
|
||||
related_articles: {
|
||||
id: number;
|
||||
slug: string;
|
||||
title: string;
|
||||
href: string;
|
||||
}[];
|
||||
related_articles: (EducationalArticle & { id: number })[];
|
||||
html: string;
|
||||
library_href: string;
|
||||
image: { href: string; alt: string; source_url: string } | null;
|
||||
@@ -153,29 +155,10 @@ export default function LegacySector({
|
||||
{match ? (
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{articles.map((item) => (
|
||||
<article
|
||||
<EducationalArticleCard
|
||||
key={item.id}
|
||||
className="space-y-3 rounded-xl border bg-white p-5"
|
||||
>
|
||||
<h3 className="text-xl font-semibold">
|
||||
<Link
|
||||
className="underline"
|
||||
href={item.href}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
</h3>
|
||||
<p>
|
||||
Conteúdo editorial preservado do portal
|
||||
Tempo.
|
||||
</p>
|
||||
<Link
|
||||
className="inline-block rounded bg-[#10352b] px-5 py-3 text-white"
|
||||
href={item.href}
|
||||
>
|
||||
Ler o artigo
|
||||
</Link>
|
||||
</article>
|
||||
article={item}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
@@ -199,9 +182,7 @@ export default function LegacySector({
|
||||
<ul className="grid gap-3 sm:grid-cols-2">
|
||||
{article.related_articles.map((item) => (
|
||||
<li key={item.id}>
|
||||
<Link className="underline" href={item.href}>
|
||||
{item.title}
|
||||
</Link>
|
||||
<EducationalArticleCard article={item} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Services\LegacyContent;
|
||||
use App\Services\SectorInvestment;
|
||||
use App\Models\Sector;
|
||||
use App\Support\IsolatedQaDatabase;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EducationalGuidesTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function createApplication(): Application
|
||||
{
|
||||
$app = parent::createApplication();
|
||||
if (! IsolatedQaDatabase::allowsSchemaChanges()) throw new \LogicException('Isolated QA required');
|
||||
return $app;
|
||||
}
|
||||
|
||||
public function test_guide_uses_public_edition_and_hides_unpublished_or_unscanned_sources(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
config(['portal.legacy_content_qa_enabled' => true]);
|
||||
$identity = config('legacy_content.articles.turismo');
|
||||
$body = '<p>Conteúdo sintético para conferir a apresentação do guia e preservar o texto da edição consultada.</p>';
|
||||
$bytes = json_encode(['format' => 'investmoz-legacy-article-v1', 'source_url' => $identity['source_url'], 'content_html' => $body,
|
||||
'title' => 'Guia sintético', 'author' => 'Autor QA', 'legacy_id' => 171990, 'legacy_category' => 'Turismo',
|
||||
'published_at' => '2026-06-16', 'modified_at' => '2026-06-16'], JSON_THROW_ON_ERROR);
|
||||
$id = (string) Str::uuid();
|
||||
Storage::disk('local')->put('guides/'.$id, $bytes);
|
||||
DB::table('documents')->insert(['id' => $id, 'external_id' => $identity['external_id'], 'source_system' => 'synthetic-test',
|
||||
'file_name' => 'guide.json', 'media_type' => 'application/json', 'storage_key' => 'guides/'.$id, 'size_bytes' => strlen($bytes),
|
||||
'sha256' => hash('sha256', $bytes), 'malware_scanned_sha256' => hash('sha256', $bytes), 'malware_scan_status' => 'clean',
|
||||
'status' => 'active', 'rights_status' => 'authorized', 'data_classification' => 'public']);
|
||||
$resource = DB::table('portal_resources')->insertGetId(['document_id' => $id, 'title' => 'Guia sintético', 'category' => 'education',
|
||||
'topic' => 'turismo', 'published' => true, 'rights_status' => 'authorized', 'source_url' => $identity['source_url'], 'source_reference' => 'Synthetic isolated QA fixture', 'metadata_version' => 1]);
|
||||
$sector = Sector::where('slug', 'turismo')->firstOrFail();
|
||||
$guides = app(SectorInvestment::class)->guides($sector);
|
||||
$this->assertSame(strip_tags($body), $guides[0]['summary']);
|
||||
$this->assertSame('Autor QA', $guides[0]['author']);
|
||||
$this->assertSame('/turismo', $guides[0]['href']);
|
||||
$this->assertSame('/oportunidades?sector=turismo', $guides[0]['opportunities_href']);
|
||||
$this->assertSame('/investimentos/a-partir-de/turismo', app(LegacyContent::class)->article('turismo')['start_href']);
|
||||
DB::table('opportunities')->where('sector_id', $sector->id)->update(['status' => 'draft']);
|
||||
$this->assertFalse(app(SectorInvestment::class)->guides($sector)[0]['has_opportunities']);
|
||||
DB::table('portal_resources')->where('id', $resource)->update(['published' => false]);
|
||||
$this->assertSame([], app(SectorInvestment::class)->guides($sector));
|
||||
DB::table('portal_resources')->where('id', $resource)->update(['published' => true]);
|
||||
DB::table('documents')->where('id', $id)->update(['malware_scan_status' => 'pending']);
|
||||
$this->assertSame([], app(SectorInvestment::class)->guides($sector));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user