fix: keep internal agribusiness delivery notes out of portal presentation
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-23 11:22:39 +02:00
parent 4ad1bf402b
commit 165c2f6171
9 changed files with 81 additions and 15 deletions
+7
View File
@@ -31,6 +31,13 @@ class Opportunity extends Model
protected $appends = ['maturity_catalogue_version'];
public function toArray(): array
{
$data = parent::toArray();
if (isset($data['description'])) $data['description'] = \App\Services\OpportunityPublicText::description($this, $data['description']);
return $data;
}
public function getMaturityCatalogueVersionAttribute(): ?string
{
// Only published metadata is public; never expose the current private draft.
+1
View File
@@ -79,6 +79,7 @@ final class OpportunityPromotion
'Limitações e lacunas: '.($text('limitations') ?: 'Informação ainda por completar.'),
'Fonte: '.($text('source_reference') ?: 'Não indicada.'), 'Edição: '.($text('edition') ?: 'Não indicada.'),
'Data-base: '.($data['source_date'] ?? 'Não indicada.')]));
$body = OpportunityPublicText::description($record, $body);
$projection = ['title' => $text('title'), 'summary' => $text('summary'), 'description' => $body,
'sector_id' => $data['sector_id'] ?? null, 'maturity_label' => 'Em preparação', 'notice' => self::NOTICE,
'territories' => array_map(fn ($p) => ['id' => end($p)->id, 'label' => implode(' / ', array_column($p, 'name'))], $paths)];
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Services;
use App\Models\Opportunity;
/** Presentation only: originals, editorial fields and audit records remain intact. */
final class OpportunityPublicText
{
public static function description(Opportunity $record, string $text): string
{
$source = data_get($record->workflow_context, 'corridor_agribusiness');
if (! is_array($source) || ($source['source_sha256'] ?? null) !== CorridorAgribusinessOpportunities::SOURCE_HASH) return $text;
$hash = CorridorAgribusinessOpportunities::SOURCE_HASH;
$sourceLine = 'Fonte: '.$source['source'].'; SHA-256 '.$hash;
$paragraphs = explode("\n\n", $text);
$result = [];
foreach ($paragraphs as $paragraph) {
$paragraph = trim($paragraph);
if (in_array($paragraph, [$sourceLine, $sourceLine.'.', 'Edição: Não indicada.', 'Data-base: Não indicada.',
'Limitações e lacunas: '.CorridorAgribusinessOpportunities::LIMITATIONS,
OpportunityPromotion::NOTICE], true)) continue;
if ($paragraph === 'Enquadramento documental: '.$source['corridor'].'.') $paragraph = $source['corridor'].'.';
if ($paragraph === CorridorAgribusinessOpportunities::LIMITATIONS) {
$paragraph = 'A localização exacta, a disponibilidade de terreno e as condições do investimento serão definidas durante a preparação do projecto.';
}
if ($paragraph !== '') $result[] = $paragraph;
}
return implode("\n\n", $result);
}
}
@@ -1,5 +1,13 @@
# Rastreabilidade, utilização e formação
## Apresentação sem notas internas — 23/10/2026
Correcção solicitada pela Tempo: a interface deve comunicar a tarefa e o conteúdo, sem narrar decisões de implementação. Simplificados o cabeçalho da gestão do lote, a entrada no roteiro e as escolhas de oportunidades. Na ficha pública, removido o alerta genérico de falta de data de validação; mantém-se o estado **Em preparação** e a indicação de que a execução depende das condições e autorizações aplicáveis.
A apresentação das 34 fichas elimina exclusivamente os parágrafos automáticos conhecidos de nome de ficheiro, hash e metadados vazios. Conserva a actividade, o corredor e a informação útil sobre definição de localização e condições. A mesma apresentação é usada na serialização HTML/API e na prévia. Não há alteração de dados, fontes, versões, permissões, auditoria ou publicação; textos posteriormente revistos pela equipa são preservados. As notas e originais continuam no cadastro interno. Esta correcção não declara revista a totalidade das páginas do portal.
Verificação: teste novo de apresentação/preservação (7 asserções), quatro testes do ciclo promocional e controlo de acesso passaram; tipos e build passaram; ficha conferida no navegador QA. A execução alargada encontrou três testes históricos incompatíveis com o estado actual da fixture (lote já divulgado e directório de contas excluído da candidata anterior); não são declarados aprovados nesta passagem nem foram alterados para ocultar essas diferenças.
## Orientação do lote de Agronegócio — catálogo e selecção múltipla, 23/10/2026
**Homologacao concluida, 23/10/2026:** 34/34 fichas cadastradas e divulgadas como **Em preparação**, pela interface da conta real, com previa, motivo, confirmacao e MFA recente. Revisao servida `e12ef6ad426ff6ebc4e7421a1ddbe7f184d77a55` (funcional `c641b267`), integrada pelo Git, sem migration. [Catalogo publico](https://teste-investidor.tempo.co.mz/oportunidades?sector=agronegocio), [gestao unica](https://teste-investidor.tempo.co.mz/gestao-conteudos) e [gestao do lote](https://teste-investidor.tempo.co.mz/minhas-oportunidades/lotes/agronegocio). A divulgacao nao autoriza execucao. [Recibo final](agribusiness-homologation-20261023.json).
@@ -10,10 +10,9 @@ export default function OpportunityChoices({choices, onChange, disabled = false}
<p>Cada ficha conserva a sua identidade e fonte. Os dados gerais e a simulação têm como referência principal a primeira ficha; não somam investimentos ou benefícios de fichas diferentes.</p>
{choices.map((c,i) => <article key={c.opportunity.key} className={card + ' space-y-3'}>
<h3 className="font-semibold"><Link className="underline" href={c.opportunity.href}>{i + 1}. {c.opportunity.title}</Link></h3>
<p>{c.opportunity.key} · versão {c.opportunity.version ?? 'não indicada'}</p>
{c.coverage && <p>Abrangência documental: {c.coverage}</p>}
{c.coverage && <p>Abrangência: {c.coverage}</p>}
<p>Localização: {c.territory?.title ?? 'Por escolher — não existe implantação documentada nesta ficha.'}</p>
{c.source && <p>Fonte: {c.source}</p>}
{c.source && !/SHA-256|\.docx/i.test(c.source) && <p>Fonte: {c.source}</p>}
{onChange && <>
<fieldset><legend>Actividades que pretende realizar nesta oportunidade</legend>{Object.entries({production:'Produção agrícola',processing:'Transformação agroindustrial',trade:'Comercialização e exportação',transport:'Transporte e logística',water:'Captação/utilização de água',forestry:'Actividade florestal',animal:'Produção/serviços pecuários'}).map(([key,label])=><label key={key} className="mr-4 inline-block"><input type="checkbox" checked={c.activities?.includes(key) ?? false} onChange={e=>update(i,{activities:e.target.checked?[...(c.activities??[]),key]:(c.activities??[]).filter(x=>x!==key)})}/>{label}</label>)}</fieldset>
<TerritoryFields initial={c.territory_path} disabled={disabled} onChange={path => update(i,{territory_path:path,territory:path.length ? {key:'territory:'+path.at(-1)!.id,kind:'territory',id:path.at(-1)!.id,title:path.map(p=>p.name).join(' / '),href:'/oportunidades',version:null,sha256:'',fields:{}} : null})}/>
+1 -9
View File
@@ -32,7 +32,7 @@ export default function Detail({
const [sharing, setSharing] = useState('');
return (
<Frame title={r.title}>
{r.maturity_label === 'Em preparação' && <p className="rounded border border-amber-700 bg-amber-50 p-4"><strong>Em preparação — divulgação promocional.</strong> A divulgação não autoriza a execução do investimento.</p>}
{r.maturity_label === 'Em preparação' && <p className="rounded border border-amber-700 bg-amber-50 p-4"><strong>Em preparação.</strong> Consulte a oportunidade e prepare o seu investimento. A execução depende das condições e autorizações aplicáveis ao projecto.</p>}
<Link
className={button}
href={'/investimentos/iniciar?source=opportunity:' + r.id}
@@ -56,14 +56,6 @@ export default function Detail({
<p>
{r.reference} · {r.sector?.name} · {r.province} {r.district}
</p>
{!r.last_validated_at && (
<p className="rounded border border-amber-700 bg-amber-50 p-4">
Esta ficha não regista data de validação documental.
Confirme a origem e as condições antes de agir. Preparar uma
manifestação expressa interesse; não confirma a oferta nem
representa aprovação.
</p>
)}
<p className="text-xl">{r.summary}</p>
<ClassificationSummary rows={c.classification ?? []} />
<TerritorialSources sources={c.source_evidence} />
@@ -103,7 +103,7 @@ export default function ContextStart({
>
<h2 className="text-xl font-bold">Iniciar uma nova ideia</h2>
<p>
Utiliza o cadastro e as cinco fases/19 etapas já existentes.
Organize a sua ideia e acompanhe a preparação do investimento.
</p>
<label className="grid gap-1">
Título do investimento
@@ -14,8 +14,7 @@ export default function AgribusinessBatch({entries,manifestHash,canRegister}:{en
if(action==='preview'){const result=await profileJson(base+'/previa',{}) as {previews:Preview[]};setPreviews(result.previews.map(p=>({...p,operation_key:createOperationKey()})));setConfirmed(false);}
if(action==='disclose'){await profileJson(base+'/divulgar',{previews:previews.map(({id,token,operation_key})=>({id,token,operation_key})),reason,confirm:confirmed});setPreviews([]);setMessage('As 34 fichas foram divulgadas como Em preparação; a execução não foi autorizada.');router.reload();}
}catch(e){setMessage(e instanceof Error?e.message:'Operação não concluída. A versão anterior foi preservada.');}finally{setBusy(false);}}
return <Management title="34 oportunidades de Agronegócio"><p>Fonte: investimentos nos corredores.docx. Mantêm-se as seis designações documentais, sem renomear mestres nem inferir implantação. Promotor, área, montantes, financiamento e licenças ausentes continuam por completar. A associação a corredores, clusters e projectos não é condição de divulgação.</p>
<p>O cadastro é privado. A divulgação seguinte exige prévia, confirmação e MFA recente, com os papéis existentes. Documentos e contactos privados permanecem separados.</p>
return <Management title="Oportunidades de Agronegócio"><p>Reveja as oportunidades, complete a informação disponível e confira a prévia antes de divulgar.</p>
<p>{entries.filter(e=>e.id).length} cadastradas · {entries.filter(e=>e.status==='published').length} divulgadas</p>
{canRegister && <button className={button} disabled={busy} onClick={()=>void run('register')}>Cadastrar ou recuperar as 34 fichas privadamente</button>}
{entries.every(e=>e.id) && <button className={button+' ml-3'} disabled={busy} onClick={()=>void run('preview')}>Pré-visualizar divulgação das 34 fichas</button>}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace Tests\Unit;
use App\Models\Opportunity;
use App\Services\CorridorAgribusinessOpportunities;
use App\Services\OpportunityPublicText;
use Tests\TestCase;
class OpportunityPublicTextTest extends TestCase
{
public function test_public_serialization_omits_generated_internal_notes_without_changing_original_or_other_content(): void
{
$source = ['source' => 'investimentos nos corredores.docx', 'source_sha256' => CorridorAgribusinessOpportunities::SOURCE_HASH, 'corridor' => 'Corredor de Maputo'];
$body = 'Produção de arroz.'."\n\nFonte: ".$source['source'].'; SHA-256 '.$source['source_sha256']."\n\n".CorridorAgribusinessOpportunities::LIMITATIONS;
$record = new Opportunity(['description' => $body]);
$record->workflow_context = ['corridor_agribusiness' => $source];
$public = $record->toArray();
$this->assertStringContainsString('Produção de arroz.', $public['description']);
$this->assertStringNotContainsString('SHA-256', $public['description']);
$this->assertStringNotContainsString('.docx', $public['description']);
$this->assertSame($body, $record->description);
$this->assertArrayNotHasKey('workflow_context', $public);
$other = new Opportunity(['description' => $body]);
$this->assertSame($body, OpportunityPublicText::description($other, $body));
$this->assertSame('Texto revisto pela equipa.', OpportunityPublicText::description($record, 'Texto revisto pela equipa.'));
}
}