fix(opportunities): distinguish missing verification and improve metadata

This commit is contained in:
2026-11-10 10:41:48 +02:00
parent 4e61480272
commit 4e07557cb1
3 changed files with 90 additions and 1 deletions
@@ -0,0 +1,41 @@
export function opportunityDescription(record: {
title: string;
summary?: string | null;
}): string {
const summary = record.summary
?.replace(/<[^>]*>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
const text =
summary || `Consulte a ficha de ${record.title} no InvesteMoz.`;
return Array.from(
text
.replace(/<[^>]*>/g, ' ')
.replace(/\s+/g, ' ')
.trim(),
)
.slice(0, 160)
.join('');
}
export function recordedVerificationDate(value?: string | null): string {
if (!value?.trim()) return 'Não registada';
const parts = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
if (!parts) return 'Data não interpretável';
const [, year, month, day] = parts.map(Number);
const calendar = new Date(Date.UTC(year, month - 1, day));
if (
calendar.getUTCFullYear() !== year ||
calendar.getUTCMonth() !== month - 1 ||
calendar.getUTCDate() !== day
)
return 'Data não interpretável';
if (value.length > 10 && !/(Z|[+-]\d{2}:\d{2})$/.test(value))
return 'Fuso horário não registado';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return 'Data não interpretável';
return new Intl.DateTimeFormat('pt-MZ', {
dateStyle: 'medium',
timeZone: 'Africa/Maputo',
}).format(date);
}
+26 -1
View File
@@ -9,7 +9,11 @@ import {
} from '@/lib/search-territory';
import type { EducationalArticle } from '@/components/educational-article-card';
import { ClassificationSummary } from '@/components/opportunity-classification';
import { Link, router, usePage } from '@inertiajs/react';
import { Head, Link, router, usePage } from '@inertiajs/react';
import {
opportunityDescription,
recordedVerificationDate,
} from '@/lib/opportunity-information';
import { useState } from 'react';
import { downloadPublicOpportunityOffline } from '@/lib/public-opportunity-offline';
import InvestmentMapPanel from '@/components/investment-map-panel';
@@ -46,6 +50,23 @@ export default function Detail({
const [offlineMessage, setOfflineMessage] = useState('');
return (
<Frame title={r.title}>
<Head>
<meta
head-key="description"
name="description"
content={opportunityDescription(r)}
/>
<meta
head-key="og:title"
property="og:title"
content={r.title}
/>
<meta
head-key="og:description"
property="og:description"
content={opportunityDescription(r)}
/>
</Head>
{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
@@ -273,6 +294,10 @@ export default function Detail({
<dl className={card + ' space-y-4'}>
{[
['Promotor', r.promoter_name],
[
'Última verificação registada',
recordedVerificationDate(r.last_validated_at),
],
['Investimento', money(r)],
['Maturidade', r.maturity_label],
[
@@ -0,0 +1,23 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
opportunityDescription,
recordedVerificationDate,
} from '../../resources/js/lib/opportunity-information.ts';
test('metadata uses the actual summary, normalises markup and never invents investment terms', () => {
assert.equal(opportunityDescription({ title: 'Projecto', summary: '<b>Resumo</b>\n declarado' }), 'Resumo declarado');
const fallback = opportunityDescription({ title: 'Avicultura', summary: null });
assert.match(fallback, /Avicultura/);
assert.match(opportunityDescription({ title: 'Projecto', summary: '<b> </b>' }), /Projecto/);
assert.doesNotMatch(fallback, /A negociar|USD|aprovad|benefício/i);
assert.equal(Array.from(opportunityDescription({ title: 'Projecto', summary: 'a'.repeat(300) })).length, 160);
});
test('unknown and invalid verification dates remain distinct from a recorded verification', () => {
assert.equal(recordedVerificationDate(null), 'Não registada');
assert.equal(recordedVerificationDate('invalid'), 'Data não interpretável');
assert.equal(recordedVerificationDate('2026-02-30T12:00:00Z'), 'Data não interpretável');
assert.equal(recordedVerificationDate('2026-11-10 12:00:00'), 'Fuso horário não registado');
assert.match(recordedVerificationDate('2026-11-10T12:00:00Z'), /2026/);
});