Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
b2d4871
fix: dichiara le metriche di dominio in un catalogo e misura la profo…
subnetMusk Aug 17, 2026
a0e02c7
fix: ripara QueueBacklogHigh e APIHighErrorRate, che non potevano sca…
subnetMusk Aug 17, 2026
8656e37
refactor: ristruttura le sei dashboard su triage, imbuto di pipeline …
subnetMusk Aug 17, 2026
633fce3
test: lega le metriche emesse a quelle interrogate da dashboard e alert
subnetMusk Aug 17, 2026
e880907
feat: espone la serie giornaliera delle metriche nel contratto di stato
subnetMusk Aug 17, 2026
fb6bb9d
feat: rifa i componenti metrica su valore, contesto e stato, con i re…
subnetMusk Aug 17, 2026
e91a680
fix: distribuisce le metriche fra le pagine senza ripetere gli stessi…
subnetMusk Aug 17, 2026
14aa0ca
docs: allinea runbook e ADR alla pipeline metriche corrente
subnetMusk Aug 17, 2026
075d80f
fix: esegue su Loki le query dei riquadri di Logs and Errors, che fal…
subnetMusk Aug 17, 2026
bb9ada5
refactor: toglie il pannello introduttivo dalle dashboard e riordina …
subnetMusk Aug 17, 2026
424f697
fix: sposta la riga di contesto dentro il dd della scheda metrica, ch…
subnetMusk Aug 17, 2026
dac1d6b
fix: esegue la suite con il limite di memoria gia' usato in CI, che a…
subnetMusk Aug 17, 2026
3ba616b
fix: semina le serie a zero delle state machine con i nomi letti dall…
subnetMusk Aug 17, 2026
07917fc
fix: espone a zero il probe di una DLQ non configurata, che prima spa…
subnetMusk Aug 17, 2026
06a003a
docs: annota nel runbook il seeding delle state machine e la DLQ non …
subnetMusk Aug 17, 2026
db733c0
feat: porta il totale della scheda metrica accanto al valore invece c…
subnetMusk Aug 18, 2026
ea8497b
refactor: rimuove la barra di avanzamento e la utility di stato, che …
subnetMusk Aug 18, 2026
5e6680a
fix: rende le voci di navigazione collegamenti con un indirizzo e agg…
subnetMusk Aug 18, 2026
b527ce5
fix: estende axe e Pa11y alle tre pagine e marca come heading i titol…
subnetMusk Aug 18, 2026
e291cc5
refactor: estrae dal template dell'Assistant i due elenchi dello stor…
subnetMusk Aug 18, 2026
e2ee27d
fix: allinea le etichette dello stato a "Scaricato" e "Non scaricato"…
subnetMusk Aug 18, 2026
4fe428c
feat: ospita i caratteri nel progetto, rimisura il colore del focus e…
subnetMusk Aug 18, 2026
dfe89af
refactor: toglie dalla Overview la sezione "Da dove partire", che non…
subnetMusk Aug 18, 2026
8c2c70e
fix: lega la preparazione del messaggio alla validazione dei dati, ch…
subnetMusk Aug 18, 2026
7e7e991
refactor: raccoglie in fogli condivisi campo, segnalazioni, occhiello…
subnetMusk Aug 18, 2026
1d99720
refactor: divide per forma i due stati della riga e riordina lo stori…
subnetMusk Aug 18, 2026
30ae088
feat: ridisegna la valutazione come gruppo di radio con stelle vettor…
subnetMusk Aug 18, 2026
2e835ff
refactor: uniforma i nomi delle classi CSS al camelCase, che convivev…
subnetMusk Aug 18, 2026
43d1af4
feat: affianca all'evidenziazione dei campi un contrassegno e una leg…
subnetMusk Aug 18, 2026
8a07dab
feat: affianca alle tappe il tempo trascorso, che distingue un'attesa…
subnetMusk Aug 18, 2026
004fd84
refactor: riunisce in fondo alla bozza i comandi che stavano in due b…
subnetMusk Aug 18, 2026
3291b82
refactor: porta a token i raggi scritti a mano, toglie i tre mai usat…
subnetMusk Aug 18, 2026
09d793d
fix: toglie il <form> dai pannelli che non si inviano e alza il contr…
subnetMusk Aug 18, 2026
47a679e
feat: tiene ferma l'etichetta dei comandi in corso e ne segnala il la…
subnetMusk Aug 18, 2026
4c74017
refactor: porta il copy a un registro neutro, che alternava domande d…
subnetMusk Aug 18, 2026
ffc254c
docs: registra nell'ADR 0012 cosa e' stato applicato, i due scostamen…
subnetMusk Aug 18, 2026
822df8b
fix: rende di nuovo bianco il marchio in tema scuro, che stava su una…
subnetMusk Aug 19, 2026
935a13a
fix: evidenzia nella barra laterale anche le ultime sezioni, che la b…
subnetMusk Aug 19, 2026
868d7a2
refactor: ridisegna le tappe con contrassegno e tratto, nelle forme d…
subnetMusk Aug 19, 2026
acff000
fix: restituisce spazio alle due colonne incomprimibili dello storico…
subnetMusk Aug 19, 2026
f9369eb
feat: porta nelle schede i tempi, le corse ferme e quelle fallite, ch…
subnetMusk Aug 19, 2026
d300599
fix: centra nome e stato sotto il contrassegno e chiude l'ultima tapp…
subnetMusk Aug 19, 2026
105755d
fix: da' alla validazione automatica un colore suo, che prendeva in p…
subnetMusk Aug 19, 2026
6d81de0
fix: toglie dagli input numerici le frecce di sistema e centra l'icon…
subnetMusk Aug 19, 2026
184f2d9
fix: toglie dalla tabulazione i campi in sola lettura e riunisce l'em…
subnetMusk Aug 19, 2026
e6b03cc
feat: chiede al modello anche codice fiscale, email e matricola, scar…
subnetMusk Aug 19, 2026
3b4d21b
refactor: divide le schede metrica per domanda a cui rispondono, che …
subnetMusk Aug 19, 2026
6d54f71
refactor: compatta la Overview e porta le priorita' a quote, con il r…
subnetMusk Aug 19, 2026
530dd98
fix: aggiorna l'audit visivo alle voci di navigazione, che sono colle…
subnetMusk Aug 19, 2026
36eccd1
fix: abbrevia la validazione nella colonna e lascia che la pastiglia …
subnetMusk Aug 19, 2026
508679f
fix: sillaba le intestazioni lunghe e ricalibra le colonne, che rompe…
subnetMusk Aug 19, 2026
b9b8023
feat: misura dove il tempo se ne va, quanta parte della scheda l'AI r…
subnetMusk Aug 20, 2026
6526b4c
refactor: porta fuori dai componenti la geometria dei grafici, che e'…
subnetMusk Aug 20, 2026
e0fa8e5
feat: aggiunge la densita' delle durate, la ripartizione in fasi e le…
subnetMusk Aug 20, 2026
1a189f4
refactor: ridisegna le schede esistenti in barre, anello e verdetto, …
subnetMusk Aug 20, 2026
1e46da6
feat: dispone le metriche a mosaico, con due colonne alle schede che …
subnetMusk Aug 20, 2026
594b1df
refactor: porta la provenienza del dato dentro la casella e lascia il…
subnetMusk Aug 20, 2026
36e6f67
refactor: da' ai due riquadri della qualita' la stessa forma e la ste…
subnetMusk Aug 20, 2026
ac28154
fix: fa riempire alla scheda il riquadro della qualita', che restava …
subnetMusk Aug 20, 2026
c5220de
feat: espone gli esiti della revisione e lo stato di scaricamento com…
subnetMusk Aug 20, 2026
7f7a0db
feat: aggiunge l'anello segmentato, che tiene distinti stati che una …
subnetMusk Aug 20, 2026
b1e5932
refactor: porta il tono sul bordo della scheda, che come striscia lat…
subnetMusk Aug 20, 2026
97d3128
refactor: disegna sempre la curva delle durate e sposta sotto l'asse …
subnetMusk Aug 20, 2026
949d319
refactor: sposta a destra l'indicatore di provenienza e gli da' le sc…
subnetMusk Aug 20, 2026
3be1864
refactor: monta il pannello del Co-Pilot su quattro righe e toglie la…
subnetMusk Aug 20, 2026
11fb4bb
fix: toglie le scintille dai campi vuoti e segna quelli corretti a ma…
subnetMusk Aug 20, 2026
82a4510
fix: limita i worker di jest, che a otto per volta finivano la memori…
subnetMusk Aug 20, 2026
a3e47ba
test: usa la ripartizione della revisione al posto della metrica rimossa
subnetMusk Aug 20, 2026
4192902
feat: attribuisce a ogni campo la confidenza della riga da cui provie…
subnetMusk Aug 20, 2026
e225e3a
docs: registra nell'ADR 0013 perche' la confidenza lascia la media di…
subnetMusk Aug 20, 2026
0cb7d15
feat: porta nel contratto la confidenza di ogni campo e l'elenco di q…
subnetMusk Aug 20, 2026
1cfed36
feat: distingue nel pannello i campi dubbi da quelli buoni, che porta…
subnetMusk Aug 20, 2026
942e6d5
feat: impagina lo storico a dieci righe, che oltre la quarantesima fa…
subnetMusk Aug 20, 2026
315958e
feat: porta la pagina al dettaglio quando si consulta un documento, r…
subnetMusk Aug 20, 2026
07a871d
fix: tara sul dato la soglia del codice fiscale, che a 99 non lasciav…
subnetMusk Aug 20, 2026
12daf62
fix: sposta l'ancora dello scorrimento su un elemento che genera un b…
subnetMusk Aug 20, 2026
0a80bd1
feat: aggiunge i documenti e i prompt di prova per la demo, con l'esi…
subnetMusk Aug 21, 2026
d958dda
feat: ripartisce i campi estratti fra buona confidenza e da revisionare
subnetMusk Aug 21, 2026
55e2f69
fix: nomina nella legenda tutti i segni dei campi, non lo stato del d…
subnetMusk Aug 21, 2026
119336d
fix: centra le colonne dello storico e l'etichetta dentro il pulsante
subnetMusk Aug 21, 2026
b1f843c
fix: da' alle segnalazioni il bordo intero al posto della striscia la…
subnetMusk Aug 21, 2026
53c37ff
docs: allinea gli ADR 0012 e 0013 ai glifi per campo
subnetMusk Aug 21, 2026
196deb1
refactor: chiama orchestrazione il tempo fra un passo e l'altro
subnetMusk Aug 21, 2026
f814af2
feat: reimpagina i PDF e centra la filigrana, che usciva dal foglio
subnetMusk Aug 22, 2026
72642ff
feat: nomina nell'oggetto il documento e il suo periodo, non l'invio …
subnetMusk Aug 22, 2026
39f2bcf
feat: accoda al messaggio il documento del destinatario, che prima er…
subnetMusk Aug 22, 2026
548adf2
fix: chiede al modello testo semplice e ripara i ritorni a capo lasci…
subnetMusk Aug 22, 2026
f36298b
fix: spegne il cursore nelle caselle in sola lettura
subnetMusk Aug 22, 2026
d04e52c
fix: lega il messaggio alla conferma umana, che la validazione automa…
subnetMusk Aug 22, 2026
13862f3
docs: allinea i documenti a PDF, oggetto e conferma del messaggio
subnetMusk Aug 22, 2026
92fd22f
feat: pretende la conferma umana prima di scaricare il messaggio, non…
subnetMusk Aug 22, 2026
0a369ff
fix: porta la filigrana dietro il testo, che sul canvas ne copriva le…
subnetMusk Aug 22, 2026
95f0a79
fix: tiene insieme le voci di un elenco che il modello separa con una…
subnetMusk Aug 22, 2026
2842e91
fix: azzera il cronometro a ogni nuova corsa, che contava dalla prece…
subnetMusk Aug 22, 2026
283317a
fix: tiene la curva delle durate dentro il riquadro, che il picco sfo…
subnetMusk Aug 22, 2026
eeb29ca
fix: lascia aperto il messaggio dopo il salvataggio e spegne la confe…
subnetMusk Aug 22, 2026
62a0651
fix: non declassa la scheda confermata quando se ne corregge un campo
subnetMusk Aug 22, 2026
ddeb5f9
chore: rinomina il logo della barra laterale in eggon-logo.png
subnetMusk Aug 22, 2026
2b78111
style: allinea a Pint le virgolette e l'ordine degli import
subnetMusk Aug 22, 2026
ae5feb8
docs: accetta gli ADR 0012 e 0013 e allinea il sistema visivo a com'e…
subnetMusk Aug 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -523,13 +523,16 @@ jobs:
run: docker compose --profile tools run --rm node npm ci --ignore-scripts

- name: Smoke SPA with enforced CSP
run: docker compose --profile tools run --rm frontend-audit node scripts/a11y/csp-smoke.mjs https://traefik:8443
run: docker compose --profile tools run --rm frontend-audit node scripts/a11y/csp-smoke.mjs
https://traefik:8443/overview https://traefik:8443/assistant https://traefik:8443/copilot

- name: Run axe
run: docker compose --profile tools run --rm frontend-audit node scripts/a11y/axe-playwright.mjs https://traefik:8443
run: docker compose --profile tools run --rm frontend-audit node scripts/a11y/axe-playwright.mjs
https://traefik:8443/overview https://traefik:8443/assistant https://traefik:8443/copilot

- name: Run Pa11y
run: docker compose --profile tools run --rm frontend-audit node scripts/a11y/pa11y-runner.mjs https://traefik:8443
run: docker compose --profile tools run --rm frontend-audit node scripts/a11y/pa11y-runner.mjs
https://traefik:8443/overview https://traefik:8443/assistant https://traefik:8443/copilot

- name: Collect stack diagnostics
if: failure()
Expand Down
23 changes: 17 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,18 @@ TERRAFORM := docker compose --profile tools run --rm -T terraform
NODE := docker compose --profile tools run --rm -T node
AWS_CLI := docker compose --profile tools run --rm -T --entrypoint aws aws-cli --endpoint-url=$(LOCALSTACK_ENDPOINT_INTERNAL)
FRONTEND_AUDIT := docker compose --profile tools run --rm -T frontend-audit
# RVC9-OB chiede axe e Pa11y sulle interfacce utente principali, al plurale: la
# sola root reindirizza su /overview e lascerebbe fuori le due pagine piu' dense.
A11Y_URLS := https://traefik:8443/overview https://traefik:8443/assistant https://traefik:8443/copilot
TLS_TOOL := docker compose --profile tools run --rm -T tls-tool
# Pest viene invocato direttamente e con un limite di memoria esplicito, lo
# stesso usato dal workflow CI. Con i 128M di default dell'immagine PHP la suite
# esaurisce la memoria durante i test Feature e, poiche' display_errors e' Off,
# il processo muore senza stampare nulla: si vede solo un exit 255, che diventa
# uno 0 ingannevole appena l'output finisce in pipe verso tail o grep.
# `artisan test` non basterebbe: avvia Pest in un sottoprocesso, a cui i flag -d
# di questa invocazione non arrivano.
PEST := php -d memory_limit=1G vendor/bin/pest
TEST_ENV := -e CONFIG_SOURCE=env \
-e APP_ENV=testing \
-e CACHE_STORE=array \
Expand Down Expand Up @@ -81,7 +92,7 @@ verify-backend:
docker compose build app
docker compose run --rm --no-deps app composer validate --strict
docker compose run --rm --no-deps $(TEST_ENV) app php artisan route:list
docker compose run --rm --no-deps $(TEST_ENV) app php artisan test
docker compose run --rm --no-deps $(TEST_ENV) app $(PEST)
docker compose run --rm --no-deps app php vendor/bin/pint --test
docker compose run --rm --no-deps $(TEST_ENV) app sh -lc 'if [ -x vendor/bin/phpstan ]; then vendor/bin/phpstan analyse --memory-limit=1G; else echo "phpstan non installato in vendor: skip locale"; fi'
bash scripts/ci/check-dependency-rule.sh
Expand All @@ -105,13 +116,13 @@ verify-ci-local: verify-fast openapi-validate

test:
docker compose build app
docker compose run --rm --no-deps $(TEST_ENV) app php artisan test
docker compose run --rm --no-deps $(TEST_ENV) app $(PEST)

backend-coverage:
docker compose build app
mkdir -p coverage
chmod 0777 coverage
docker compose run --rm --no-deps -v "$(CURDIR)/coverage:/var/www/html/coverage" -e XDEBUG_MODE=coverage $(TEST_ENV) app php -d memory_limit=1G vendor/bin/pest --coverage --path-coverage --coverage-cobertura coverage/cobertura.xml
docker compose run --rm --no-deps -v "$(CURDIR)/coverage:/var/www/html/coverage" -e XDEBUG_MODE=coverage $(TEST_ENV) app $(PEST) --coverage --path-coverage --coverage-cobertura coverage/cobertura.xml
$(NODE) node scripts/ci/normalize-cobertura-paths.mjs coverage/cobertura.xml /var/www/html
$(NODE) node scripts/ci/check-coverage-thresholds.mjs backend coverage/cobertura.xml

Expand Down Expand Up @@ -164,9 +175,9 @@ frontend-audit: node-install
frontend-a11y: frontend-s3-local-deploy
@if [ ! -f docker/traefik/certs/mvp-local.test.crt ] || [ ! -f docker/traefik/certs/mvp-local.test.key ]; then $(MAKE) local-tls; fi
docker compose up -d --wait --force-recreate app nginx edge-cdn traefik
$(FRONTEND_AUDIT) node scripts/a11y/csp-smoke.mjs https://traefik:8443
$(FRONTEND_AUDIT) node scripts/a11y/axe-playwright.mjs https://traefik:8443
$(FRONTEND_AUDIT) node scripts/a11y/pa11y-runner.mjs https://traefik:8443
$(FRONTEND_AUDIT) node scripts/a11y/csp-smoke.mjs $(A11Y_URLS)
$(FRONTEND_AUDIT) node scripts/a11y/axe-playwright.mjs $(A11Y_URLS)
$(FRONTEND_AUDIT) node scripts/a11y/pa11y-runner.mjs $(A11Y_URLS)

openapi-generate: node-install
$(NODE) npm run openapi:generate
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ La state machine pubblica task su SQS usando il callback pattern con task token.

Il risultato è una pipeline documentale composta da passaggi isolati, monitorabili e riavviabili, con persistenza dello stato e visibilità sui risultati prodotti.

Sui sotto-documenti prodotti l’operatore lavora in revisione human-in-the-loop: corregge i campi estratti (inclusi email destinatario, codice fiscale e matricola, con validazione dedicata) e li marca come validati. Il dettaglio riporta anche l’email del destinatario, copiabile negli appunti con un comando, e la data e ora di caricamento del documento di origine. Da ogni sotto-documento il sistema compone un messaggio di invio precompilato con destinatario, oggetto e testo, che si può correggere, visualizzare in anteprima ed esportare in PDF. Il recapito avviene fuori dalla piattaforma tramite canali terzi: per questo lo stato di invio coincide con l’avvenuto **scaricamento** del PDF, e non con un invio effettuato dal sistema. Lo storico dei documenti è filtrabile per nome, cognome o azienda, stato di invio, soglia di confidenza e periodo, sempre con i criteri applicati lato API e limitati al tenant chiamante.
Sui sotto-documenti prodotti l’operatore lavora in revisione human-in-the-loop: corregge i campi estratti (inclusi email destinatario, codice fiscale e matricola, con validazione dedicata) e li marca come validati. Il dettaglio riporta anche l’email del destinatario, copiabile negli appunti con un comando, e la data e ora di caricamento del documento di origine. Da ogni sotto-documento il sistema compone un messaggio di invio precompilato con destinatario, oggetto e testo, che si può correggere, visualizzare in anteprima ed esportare in PDF. Il recapito avviene fuori dalla piattaforma tramite canali terzi: per questo lo stato mostrato è quello di **scaricamento** («Scaricato» / «Non scaricato»), e non un invio effettuato dal sistema. Lo storico dei documenti è filtrabile per nome, cognome o azienda, stato di scaricamento, soglia di confidenza e periodo, sempre con i criteri applicati lato API e limitati al tenant chiamante.

## Monitoring e osservabilità

Expand Down
12 changes: 9 additions & 3 deletions app/Console/Commands/ListDlqMessages.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@

namespace App\Console\Commands;

use App\Mvp\Observability\MetricsRecorder;
use Aws\Sqs\SqsClient;
use Illuminate\Console\Command;

/**
* Strumento di diagnosi, non punto di strumentazione: la profondita' della DLQ
* e' misurata da DlqDepthProbe a ogni scrape. Questo comando incrementava un
* counter `dlq_messages_total` che cresceva solo quando qualcuno lo lanciava a
* mano, con un tetto di 10 messaggi, e non tornava mai indietro — gli alert che
* ci si basavano erano quindi ciechi prima della prima esecuzione e bloccati in
* firing dopo.
*/
class ListDlqMessages extends Command
{
protected $signature = 'mvp:dlq:list {--queue=documents : Pipeline DLQ to inspect: documents or communications} {--limit=10 : Maximum DLQ messages to inspect}';

protected $description = 'List workflow DLQ messages without deleting them.';

public function handle(SqsClient $sqs, MetricsRecorder $metrics): int
public function handle(SqsClient $sqs): int
{
$pipeline = (string) $this->option('queue');
$queueUrl = match ($pipeline) {
Expand Down Expand Up @@ -43,7 +50,6 @@ public function handle(SqsClient $sqs, MetricsRecorder $metrics): int
'VisibilityTimeout' => 0,
]);
$messages = $result->get('Messages') ?? [];
$metrics->recordDomainCounter('dlq_messages_total', ['queue' => $pipeline], count($messages));

if ($messages === []) {
$this->info('DLQ vuota.');
Expand Down
42 changes: 38 additions & 4 deletions app/Http/Controllers/Api/V1/SendMessageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
use App\Http\Controllers\Api\V1\Concerns\ResolvesActor;
use App\Http\Requests\UpdateSendMessageRequest;
use App\Models\SubDocument;
use App\Mvp\Documents\Domain\Exceptions\SendMessageAttachmentUnavailableException;
use App\Mvp\Documents\Domain\Exceptions\SendMessageNotConfirmedException;
use App\Mvp\Documents\Domain\Ports\Inbound\SendMessageUseCase;
use App\Mvp\Support\MvpStateService;
use Illuminate\Http\JsonResponse;
Expand All @@ -20,25 +22,47 @@ class SendMessageController
{
use AuthorizesDocuments, ResolvesActor;

public function sendPreview(Request $request, SubDocument $subDocument, SendMessageUseCase $sendMessage): Response
public function sendPreview(Request $request, SubDocument $subDocument, SendMessageUseCase $sendMessage): Response|JsonResponse
{
$actor = $this->actor($request);
$this->authorizeSubDocument($subDocument, $actor);

$rendered = $sendMessage->preview($subDocument->id, $actor);
try {
$rendered = $sendMessage->preview($subDocument->id, $actor);
} catch (SendMessageNotConfirmedException $exception) {
return response()->json([
'error' => [
'code' => 'review_not_confirmed',
'message' => $exception->getMessage(),
],
], 422);
} catch (SendMessageAttachmentUnavailableException $exception) {
return $this->attachmentUnavailable($exception);
}

return response($rendered->pdf, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline',
]);
}

public function sendExport(Request $request, SubDocument $subDocument, SendMessageUseCase $sendMessage): Response
public function sendExport(Request $request, SubDocument $subDocument, SendMessageUseCase $sendMessage): Response|JsonResponse
{
$actor = $this->actor($request);
$this->authorizeSubDocument($subDocument, $actor);

$rendered = $sendMessage->export($subDocument->id, $actor);
try {
$rendered = $sendMessage->export($subDocument->id, $actor);
} catch (SendMessageNotConfirmedException $exception) {
return response()->json([
'error' => [
'code' => 'review_not_confirmed',
'message' => $exception->getMessage(),
],
], 422);
} catch (SendMessageAttachmentUnavailableException $exception) {
return $this->attachmentUnavailable($exception);
}

return response($rendered->pdf, 200, [
'Content-Type' => 'application/pdf',
Expand All @@ -64,4 +88,14 @@ public function updateSendMessage(
'state' => $state->forActor($actor),
]);
}

private function attachmentUnavailable(SendMessageAttachmentUnavailableException $exception): JsonResponse
{
return response()->json([
'error' => [
'code' => 'attachment_unavailable',
'message' => $exception->getMessage(),
],
], 404);
}
}
3 changes: 3 additions & 0 deletions app/Models/ExtractedData.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* @property string|null $description
* @property string|null $recipient_email
* @property int|null $confidence_score
* @property array<string, float|null>|null $field_confidences
* @property array<string, mixed>|null $ai_payload
* @property SubDocument|null $subDocument
*/
Expand All @@ -38,6 +39,7 @@ class ExtractedData extends Model
'description',
'recipient_email',
'confidence_score',
'field_confidences',
'ai_payload',
];

Expand All @@ -49,6 +51,7 @@ protected function casts(): array
return [
'document_date' => 'date',
'confidence_score' => 'integer',
'field_confidences' => 'array',
'ai_payload' => 'array',
];
}
Expand Down
9 changes: 8 additions & 1 deletion app/Mvp/Ai/AiOutputValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public function validateSplitDocument(mixed $decoded): array
}

/**
* @return array{employee_first_name: ?string, employee_last_name: ?string, company_name: ?string, document_date: ?string, document_type: ?string, description: ?string, confidence_score: ?int}
* @return array{employee_first_name: ?string, employee_last_name: ?string, company_name: ?string, document_date: ?string, document_type: ?string, description: ?string, recipient_email: ?string, fiscal_code: ?string, employee_id: ?string, confidence_score: ?int}
*
* @throws InvalidAiOutputException
*/
Expand Down Expand Up @@ -83,6 +83,13 @@ public function validateExtractFields(mixed $decoded): array
'document_date' => $string('document_date'),
'document_type' => $string('document_type'),
'description' => $string('description'),
// Identificativi del destinatario. Qui si normalizza soltanto la
// forma: se il valore sia plausibile — checksum del codice
// fiscale, sintassi dell'indirizzo — lo decide il caso d'uso, che
// e' dove vive la conoscenza del dominio documentale.
'recipient_email' => $string('recipient_email'),
'fiscal_code' => $string('fiscal_code'),
'employee_id' => $string('employee_id'),
'confidence_score' => isset($decoded['confidence_score']) && is_int($decoded['confidence_score'])
? $decoded['confidence_score']
: null,
Expand Down
74 changes: 71 additions & 3 deletions app/Mvp/Ai/BedrockService.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,69 @@ private function extractJsonFromAiResponse(array $rawResponse, string $operation

$decoded = json_decode($cleanJson, true);

// Seconda occasione: il modello scrive spesso i ritorni a capo dei
// paragrafi cosi' come sono, e dentro una stringa JSON quelli sono
// caratteri di controllo che invalidano l'intero documento. Il testo
// che portano e' buono: si ripara la forma, non il contenuto.
if (! is_array($decoded)) {
$decoded = json_decode($this->escapeControlCharactersInStrings($cleanJson), true);
}

if (! is_array($decoded)) {
throw new InvalidAiOutputException($operation, ['la risposta del modello non è JSON decodificabile']);
}

return $decoded;
}

/**
* Scrive come sequenze di escape i ritorni a capo e le tabulazioni rimasti
* dentro le stringhe di un JSON.
*
* La scansione e' byte per byte: in UTF-8 nessun byte di un carattere
* multibyte coincide con la virgoletta, la barra rovesciata o un carattere
* di controllo ASCII, quindi non c'e' modo di spezzare una lettera accentata
* a meta'.
*/
private function escapeControlCharactersInStrings(string $json): string
{
$repaired = '';
$inString = false;
$escaped = false;

foreach (str_split($json) as $char) {
if ($escaped) {
$repaired .= $char;
$escaped = false;

continue;
}

if ($char === '\\') {
$repaired .= $char;
$escaped = true;

continue;
}

if ($char === '"') {
$inString = ! $inString;
$repaired .= $char;

continue;
}

$repaired .= $inString ? match ($char) {
"\n" => '\\n',
"\r" => '',
"\t" => '\\t',
default => $char,
} : $char;
}

return $repaired;
}

/**
* Il modello testuale descrive anche la copertina: avendo davanti il testo
* che ha appena scritto, produce una direzione visiva coerente con la
Expand All @@ -241,6 +297,14 @@ private function buildCommunicationPrompt(string $userPrompt, string $tone, stri
{
return "Agisci come un assistente HR. Genera una comunicazione con tono '{$tone}' e stile '{$style}'.\n"
."Argomento: {$userPrompt}\n"
// Ne' il PDF ne' l'anteprima interpretano Markdown: un asterisco
// arrivato fin li' si legge come un asterisco.
.'Scrivi il corpo in testo semplice: niente Markdown, niente cancelletti, asterischi, '
.'trattini di elenco, trattini bassi o backtick. Separa i paragrafi con una riga vuota '
."e apri con il segno • ogni voce di un eventuale elenco. Non ripetere il titolo nel corpo.\n"
// Chiedere paragrafi e' chiedere ritorni a capo, e dentro una
// stringa JSON un ritorno a capo vero rompe il documento.
."Nel JSON i ritorni a capo vanno scritti come \\n dentro le stringhe, mai andando a capo davvero.\n"
.'Produci anche "imagePrompt": la descrizione in inglese, massimo 60 parole, dell\'immagine di copertina '
.'coerente con il contenuto che hai generato. Descrivi soggetto, composizione, atmosfera e palette. '
."Non includere testo leggibile, loghi, filigrane, volti riconoscibili o dati personali.\n"
Expand Down Expand Up @@ -629,7 +693,7 @@ public function splitDocument(string $ocrText, int $pageCount, string $pageBound
* Extract structured fields for a single recipient from its OCR text.
* Works on any document type, not just payslips.
*
* @return array{employee_first_name: ?string, employee_last_name: ?string, company_name: ?string, document_date: ?string, document_type: ?string, description: ?string, confidence_score: ?int}
* @return array{employee_first_name: ?string, employee_last_name: ?string, company_name: ?string, document_date: ?string, document_type: ?string, description: ?string, recipient_email: ?string, fiscal_code: ?string, employee_id: ?string, confidence_score: ?int}
*
* @throws \RuntimeException
*/
Expand All @@ -638,8 +702,12 @@ public function extractFields(string $ocrText): array
$this->ensureConfigured();

$prompt = "Estrai i seguenti campi dal testo OCR di questo documento (qualsiasi tipologia).\n"
."Rispondi SOLO con JSON valido con le chiavi: employee_first_name (nome del destinatario), employee_last_name (cognome del destinatario), company_name (azienda o ente, se presente), document_date (formato YYYY-MM-DD), document_type (tipologia del documento rilevata dal contenuto), description (max 200 caratteri), confidence_score (intero 0-100).\n"
."Usa null per i campi non trovati.\n\n"
."Rispondi SOLO con JSON valido con le chiavi: employee_first_name (nome del destinatario), employee_last_name (cognome del destinatario), company_name (azienda o ente, se presente), document_date (formato YYYY-MM-DD), document_type (tipologia del documento rilevata dal contenuto), description (max 200 caratteri), recipient_email (indirizzo email del destinatario), fiscal_code (codice fiscale del destinatario, 16 caratteri), employee_id (matricola o codice dipendente), confidence_score (intero 0-100).\n"
."Usa null per i campi non trovati.\n"
// I tre identificativi valgono solo se stanno scritti nel
// documento: un codice fiscale plausibile ma inventato passerebbe
// per dato estratto, e l'operatore non ha modo di distinguerlo.
."Per recipient_email, fiscal_code e employee_id riporta esclusivamente valori presenti alla lettera nel testo: se non compaiono, usa null senza dedurli.\n\n"
."Per confidence_score usa questa scala:\n"
."- 90-100: tutti i campi principali (nome, cognome, azienda, data) sono chiaramente leggibili\n"
."- 70-89: la maggior parte dei campi è leggibile ma uno o due sono ambigui o parziali\n"
Expand Down
Loading
Loading