Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Questo documento è rivolto a sviluppatori, tecnici e Partner Tecnologici ed è volto a fornire informazioni tecniche sull'architettura, le integrazioni, le interfacce disponibili nella piattaforma.
La piattaforma OpenCity Italia semplifica la realizzazione e la gestione di servizi digitali per cittadini e imprese, anche da smartphone (art. 7 CAD).
OpenCity Italia è open source, scaricabile da Developers Italia o disponibile come servizio pronto all'uso (Software as a Service).
La piattaforma è sviluppata con l'obiettivo di erogare servizi per centinaia di Enti contemporaneamente da una singola installazione, per questo motivo il codice della piattaforma è organizzato in microservizi indipendenti tra loro e il dialogo tra gli stessi è realizzato mediante stream di eventi piuttosto che mediante chiamate dirette alle API.
Per queste scelte l'installazione e la gestione della piattaforma in un ambiente di produzione richiede un ampio ventaglio di conoscenze:
Sistema Operativo GNU/Linux e uso della command line (CLI)
database PostgresQL, MongoDB, Redis
Docker e sistemi di orchestrazione di container
Kafka e KsqlDB
Per contribuire alla piattaforma, a seconda dell'area su cui si desidera sviluppare sono necessarie cometenze nei seguenti linguaggi di programmazione:
PHP
NodeJS
Python
Golang
Infine la piattaforma è in costante evoluzione e nuovi componenti vengono aggiunti o prendono il posto di componenti esistenti divenuti obsoleti.
In questa area della documentazione si fornisce una descrizione mediante diagrammi dell'architettura logica della piattaforma.
In questa sezione viene descritto in dettaglio il json del pagamento e la definizione dettagliata dei campi
Un'architettura di microservizi è costituita da un insieme di servizi ridotti autonomi.
Ogni servizio è autonomo e deve implementa una singola funzionalità all'interno di un contesto delimitato. Un contesto delimitato è una divisione naturale all'interno di un Ente e fornisce un limite esplicito all'interno del quale esiste un modello di dominio.
Ogni microservizio della piattaforma è distribuito come docker container, ha un proprio versionamento ed espone una o più porte per comunicare con l'esterno.
I microservizi sono esposti all'esterno da un web router che assolve solitamente anche il ruolo di terminatore di SSL.
Ad ogni release della piattaforma vengono aggiornati uno o più microservizi.
Per facilitare il lavoro di chi lavora su integrazioni e sulle API della piattaforma, forniamo una visione generale della roadmap.
Per ulteriori dettagli si veda https://gitlab.com/groups/opencity-labs/-/milestones
La piattaforma può offrire sia una esperienza completa al cittadino o all'impresa, che consente l'invio di pratiche e la loro gestione nel backend, sia farsi carico di uno solo di questi aspetti, lasciando ad altri sistemi il ruolo di interfaccia con il cittadino o a sistemi verticali il compito di gestire pratiche (pratiche edilizie o in ambito sociale)
In presenza di questa esigenza è importante che la piattaforma e il sistema terzo siano in grado di instaurare e mantenere nel tempo la relazione tra le pratiche, mantenere aggiornati lo stato della pratica o di un pagamento pendente per la stessa.
Vari strumenti nella piattaforma consentono di mantenere aggiornate le informazioni e ricevere aggiornamenti:
API ReST
Webhook
Prima di entrare nei dettagli di questi strumenti merita avere uno sguardo di insieme del flusso della pratica e di tutti gli stati che può attraversare. Nel grafico che segue il tratto continuo più forte rappresenta il flusso più frequente di una pratica, il tratto punteggiato rappresenta i flussi alternativi. I cerchi costituiscono gli stati finali di una pratica
Per una visione generale di tutti gli stati che una pratica può assumere e quali sono i cambi di stato ammessi segue un grafico più generale. Nel grafico sono evidenziati in azzurro gli stati fondamentali della pratica, mentre in grigio gli stati deprecati.
Il sistema è costituito da un core, che assolve ai compiti principali e da alcuni sottosistemi dedicati a compiti specifici: gestione dei pagamenti, protocollazione, sistema di analytics.
Il componente principale, il core, è ovviamente il frutto del lavoro dei primi anni di sviluppo della piattaforma, ed assolve ai compiti principali della stessa:
Definizione e organizzazione dei Servizi Digitali
Definizione dei Moduli on-line
Gestione delle Pratiche inviate dai cittadini e dei messaggi
Gestione degli utenti
Gestione Uffici e appuntamenti on-line su slot di tempo e orari di apertura.
Gestione Sale pubbliche e altre risorse a prenotazione flessibile
Il core assolve anche al compito di interfaccia utente, sia per i cittadini che per gli operatori e gli amministratori degli Enti.
Il core è costituito dai seguenti microservizi:
Caddy / Symfony App (dal 2023 i due servizi sono distribuiti in una immagine unica: registry.gitlab.com/opencontent/stanza-del-cittadino/core/app
)
Nota
Form Server, implementazione del server di Form.IO: registry.gitlab.com/opencontent/stanza-del-cittadino/form-server
Form Builder, un editor multi-tenant con accesso riservato agli amministratori della piattaforma per l'editing dei componenti condivisi dei form degli Enti: registry.gitlab.com/opencontent/stanza-del-cittadino/formbuilderjs
Varnish, un cache proxy server che incrementa l'affidabilità e la scalabilità del formserver e del catalogo dei servizi: registry.gitlab.com/opencontent/varnish:latest
Gotenberg, a Docker-powered stateless API for PDF files: gotenberg/gotenberg:7
Payment Updates, ascolta gli eventi relativi ai pagamenti e aggiorna le pratiche di conseguenza: registry.gitlab.com/opencontent/stanza-del-cittadino/payment-updater
Signature Check, api usata dal componente sdc_upload di form.io per validate le firme dei documenti firmati .p7m: geopartner/signcheckwebapi:latest
Per un esempio completo di deploy dei microservizi si veda il file nel repositoy del core, che fornisce una configurazione funzionante della piattaforma.
Utilizza inoltre i seguenti servizi di persistenza:
PostgreSQL, per la gestione di tutti i dati e delle configurazioni della piattaforma, ad eccezione delle form. La versione usata in sviluppo è la 11 e comprende anche l'estensione PostGIS: postgis/postgis:11-3.3-alpine
MongoDB, per la persistenza dei moduli realizzati con l'editor di Form.IO: mongo:4.2
Redis, per la gestione delle sessioni degli utenti e della cache applicativa: redis:5-alpine.
Nuova Area Personale del cittadino, per le pratiche generate dalla piattaforma e per le pratiche generate in altre piattaforme.
Erogazione dei servizi digitali mediante Widget React
Gestione di Documenti in Area Personale
Estensione del sistema di notifica al di fuori del flusso delle pratiche.
Pagamenti in Area Personale e integrazione con Checkout
L’area personale di OpenCity Italia è progettata per integrarsi con vari intermediari di pagamento utilizzati da Comuni ed enti pubblici. Questa sezione descrive i requisiti tecnici necessari per sviluppare e implementare l'integrazione tra la piattaforma e l'intermediario di pagamento PagoPA.
Le interazioni avvengono in seguito ad alcune azioni da parte dei cittadini e degli operatori:
Invio di una pratica da parte del cittadino
Approvazione di una pratica da parte di un operatore nel caso di un pagamento posticipato
Importazione di dovuti da parte dell'operatore mediante API file csv
Pagamento di un dovuto da parte del cittadino a seguito dell'importazione di quest'ultimo
Per sviluppare l'integrazione con l'intermediario di pagamento PagoPA, sono richiesti i seguenti elementi:
Documentazione Tecnica: Manuali, guide e riferimenti API.
Endpoint/Ambiente di Test: URL degli endpoint e accesso a un ambiente di test per effettuare chiamate API.
Modalità di Verifica dello Stato del Pagamento: Specifiche delle modalità di verifica dello stato del pagamento, tramite polling o notifica.
Per integrare OpenCity Italia Area Personale con l’intermediario di PagoPA, sono necessari i seguenti parametri minimi:
Parametri per la creazione di un pagamento
Parametri per la creazione di una Marca da Bollo Digitale
Restituzione del numero IUV e/o del numero avviso in fase di creazione del pagamento
Parametri per scaricare l’avviso di pagamento cartaceo
Parametri per scaricare la ricevuta telematica una volta effettuato il pagamento
API per il download degli importi dovuti (se disponibile)
L’intermediario di pagamento deve fornire documentazione dettagliata delle API, comprensiva di:
Endpoint
Metodi supportati (GET, POST, etc.)
Parametri richiesti e facoltativi
Formato dei dati (JSON, XML)
Esempi di richieste e risposte
Per garantire le comunicazioni fra la piattaforma e l’intermediario di pagamento, è necessario sbloccare gli IP delle chiamate entranti verso gli endpoint dell’intermediario di pagamento. Gli IP di OpenCity da sbloccare sono:
93.41.234.251/32
78.47.94.152/32
54.220.150.231/32
63.33.155.248/32
37.186.144.203/32
Per risolvere eventuali complicazioni, errori o bug durante l'integrazione, è fondamentale avere un contatto tecnico dedicato. Si prega di fornire i seguenti dettagli del contatto tecnico:
Nome
Telefono
Disponibilità (orari di lavoro)
La piattaforma è responsabile della gestione della protocollazione dei documenti derivanti dai diversi processi burocratici che essa offre. Il documento generato è conforme alle linee guida stabilite dall'AGID(si veda la sezione Documento digitale). Esso è soggetto a protocollazione in determinati casi. Tale processo viene eseguito attraverso l’integrazione con un sistema di protocollazione esterno. Nella seguente sezione verrà esaminato in dettaglio come tale integrazione deve essere fatta, descrivendo gli attori, i componenti coinvolti e le loro interazioni.
La generazione di documenti varia a seconda del tipo di pratica che deve essere registrata o gestita. Ci sono diverse tipologie di pratica, ognuna delle quali corrisponde a uno stato specifico della procedura:
Creazione di una pratica da parte o per conto di un cittadino (application-request): questo si riferisce alla creazione iniziale di una pratica da parte di un cittadino o da parte di qualcuno agendo per conto del cittadino.
Invio di una richiesta di integrazioni da parte di un operatore (integration-request): questo si verifica quando un operatore richiede ulteriori informazioni o documenti relativi a una pratica.
Invio di un'integrazione da parte del cittadino (integration-response): In questo caso, il cittadino risponde alle richieste di integrazione inviando ulteriori documenti o informazioni.
Approvazione/rifiuto di una pratica da parte di un operatore (application-outcome, application-revocation): questo rappresenta l'atto di approvare o rifiutare una pratica da parte di un operatore.
Ritiro di una pratica da parte di un cittadino (application-withdraw): un cittadino può ritirare una pratica in qualsiasi momento.
Queste tipologie rappresentano diversi stati o fasi attraverso cui una pratica può passare durante il suo ciclo di vita. Poiché una pratica può attraversare più di uno di questi stati, ciò significa che saranno generati diversi documenti associati alla stessa pratica, ognuno dei quali corrisponderà a una fase specifica del processo.
Un'architettura guidata dagli eventi è costituita da producer eventi che generano un flusso di eventi e consumer eventi che sono in ascolto degli eventi.
Streaming eventi: gli eventi vengono scritti in un log. Gli eventi sono rigorosamente ordinati (in una partizione) e durevoli. I client non sottoscrivono il flusso, ma un client può invece leggere da qualsiasi parte del flusso. Il client è responsabile di far avanzare la propria posizione nel flusso. Questo significa che un client può aggiungersi in qualsiasi momento e può riprodurre gli eventi.
Elaborazione del flusso di eventi. Usare una piattaforma di flussi di dati come Apache Kafka, come pipeline per inserire gli eventi e fornirli agli elaboratori di flussi. Gli elaboratori di flussi intervengono per elaborare o trasformare il flusso. Possono essere presenti più elaboratori di flussi per sottosistemi diversi nell'applicazione.
Le API consentono di leggere e modificare informazioni sulla piattaforma in modo programmatico.
Una API ReST, nota anche come API RESTful, è un'interfaccia di programmazione delle applicazioni (API o API web) conforme ai vincoli dello stile architetturale ReST, che consente l'interazione con servizi web RESTful. Il termine REST, coniato dall'informatico Roy Fielding, è l'acronimo di REpresentational State Transfer.
La documentazione delle API ReST della piattaforma è pubblicamente disponibile in ogni Ente all'indirizzo:
Ad esempio per il comune demo della piattaforma è possibile consultarle alla pagina:
Alcune API richiedono un'autenticazione prima di essere chiamate, in questo caso un cittadino o un operatore possono usare le proprie credenziali per ottenere un token di autenticazione come segue:
POST
https://servizi.comune.bugliano.pi.it/lang/api/auth
Con questa chiamata si riceve un token di autenticazione in formato JWT, che dovrà essere inviato per tutte le successive chiamate che richiedono autenticazione come bearer token nell'header Autorization
username*
String
nome utente
password*
String
password
Alcuni esempi di chiamate
Esempio di chiamata da linea di comando con il client Httpie:
echo '{ "username": "my-api-user", "password": "xxxx" }' | \
http post https://servizi.comune.bugliano.pi.it/lang/api/auth
Esempio di chiamata da linea di comando con il client curl:
curl \
--header 'Content-Type: application/json' \
--data $'{\n "username": "my-api-user",\n "password": "xxxx"\n}' \
https://servizi.comune.bugliano.pi.it/lang/api/auth
Le API sono disponibili in due versioni, la "1" e la "2". Le capacità delle API sono le stesse per le due versioni, nella versione 2 c'e' una migliore gestione del formato JSON, quindi si consiglia l'uso della versione più recente.
È possibile specificare la versione delle Api che si vanno ad interrogare, questo è possibile in 2 modi differenti:
tramite parametro version
da passare come parametro delle GET
tramite parametro X-Accept-Version
da specificare nell' header della richiesta
La versione di default è per retrocompatibilità la 1
Dalla versione 2 le chiavi dei valori del campo data
delle applications
non sono più restituite come un insieme di strighe piatto separato dal punto (.) ma come campo json con le chiavi esplose.
Nota bene: si raccomanda di specificare sempre la versione di API utilizzata, anche se è il default (1), perche' in futuro il default potrebbe cambiare.
Sono un meccanismo di comunicazione tra sistemi che consentono una comunicazione in tempo reale tra due API
Le API non sono sufficienti per realizzare una buona integrazione tra due sistemi. Se due o più sistemi si devono aggiornare reciprocamente in base agli eventi che avvengono al proprio interno, è necessario che periodicamente ogni sistema interroghi gli altri per sapere se il loro stato è cambiato: questo approccio si chiama anche polling. Questo è molto oneroso e introduce un ritardo nell'aggiornamento che è tanto maggiore quanto maggiore è l'intervallo con cui un sistema interroga gli altri. D'altra parte se di intensifica la frequenza del polling si avranno maggiori costi a fronte di aggiornamenti magari poco frequenti.
Un modo più efficiente di far dialogare due sistemi è avviare una comunicazione in occasione di cambiamenti rilevanti per il sistema in ascolto e per ottenere questo, eliminando del tutto la necessità di fare polling, è possibile usare i Webhook.
Ogni webhook consente di inviare un aggiornamento a una API di un altro sistema che sta ascolto, in base agli eventi che si desidera comunicare.
Per fare test sui webhook si consiglia di utilizzare un servizio come Request Inspector.
Widget Formio – Servizio permette di integrare i form registrati all’interno del portale servizi del Comune. Di seguito la guida all’installazione e configurazione.
Aggiungi dentro il tag <head>
della tua pagina questi riferimenti:
<widget-formio>
Posiziona <widget-formio>
nel punto della pagina in cui vuoi mostrare il form:
Attributi
Tutte gli esempi dei servizi sono documentati in Storybook:
Per aggiungere o personalizzare un servizio built-in esistente (Richiedi assistenza, Prenota appuntamento, ecc.), è necessario configurare il campo identifier come amministratore nella sezione dettaglio del servizio.
Il valore di identifier
deve seguire il formato: <nome-servizio>-custom
dove:
<nome-servizio>
è obbligatorio e corrisponde al servizio di base (helpdesk
, bookings
, ecc.)
-custom
è suffisso libero a scelta per distinguere la versione personalizzata
Esempi:
helpdesk-custom
per una versione custom di Richiedi assistenza
bookings-custom
per una versione custom di Prenota appuntamento
Questa configurazione permette al widget di riconoscere e caricare il servizio personalizzato attraverso il tag <widget-formio>
.
Le API fornite nella documentazione dell'intermediario vengono generalmente testate in prima battuta via Postman/Insomnia nel caso di chiamate REST o via SoapUI nel caso di chiamate SOAP
A seguito dei test delle chiamate, si procede con l'implementazione del microservizio. Per l'ambiente di sviluppo viene utilizzato Docker e Docker compose come da esempio nel seguente repository :
<!-- JavaScript (deferred) -->
<script
defer
src="https://static.opencityitalia.it/widgets/formio/latest/js/web-formio.js"
></script>
<!-- CSS -->
<link
rel="stylesheet"
href="https://static.opencityitalia.it/widgets/formio/latest/css/web-formio.css"
/>
<widget-formio
service-id="0121edc9-57d8-45f2-ae92-260f45aec6d5"
base-url="https://servizi.comune-qa.bugliano.pi.it/lang"
formserver-url="https://form-qa.stanzadelcittadino.it"
pdnd-url="https://api.qa.stanzadelcittadino.it/pdnd/v1"
enable-draft="true"
></widget-formio>
service-id
Sì
UUID del servizio/Form registrato sulla “Stanza del Cittadino”
base-url
Sì
URL della “Stanza del Cittadino” (autenticazione, privacy e routing del widget).
formserver-url
Sì
URL del Form Server Form.io (endpoint per recupero e invio dei form).
pdnd-url
Sì
URL del servizio PDND (Piattaforma Digitale Nazionale Dati) per il caricamento delle anagrafiche.
enable-draft
No
true
/ false
– abilita il salvataggio in bozza delle pratiche. Default: false
.
La configurazione dei pagamenti avviene interrogando delle API apposite fornite dal proxy di riferimento. Quest'ultimo fornisce inoltre un form come json schema sviluppato con form.io mediante il quale l'utente inserisce tali configurazioni.
Per creare un form utilizzare il seguente builder: https://formio.github.io/formio.js/app/builder
Per SSO si intende la possibilità per il cittadino di fare login sull'area personale o su un servizio digitale e non dovrelo ripetere se si sposta su un altro servizio digitale dello stesso Ente.
La piattaforma supporta il multilinguismo dal punto di vista tecnico. Le due lingue supportate sono Italiano e Tedesco
Panoramica dei ciclo di funzionamento dei pagamenti di Opencity Italia - Area Personale.
Un pagamento nasce da una interazione con il cittadino: il cittadino presenta una pratica per la quale l'Ente richiede un pagamento.
Il cuore dell'integrazione con un intermediario di pagamento è un microservizio apposito che dialoga con l'intermediario da un lato, implementando le chiamate specifiche in protocollo ReST o SOAP, e con il core della piattaforma dall'altro lato, mediante la lettura e la scrittura su un topic di Kafka.
In questo momento i pagamenti hanno una relazione 1 a 1 con le pratiche, di conseguenza ci sono due servizi che fanno da intermediari tra le pratiche e i pagamenti, ovvero il payment dispatcher e il payment updater.
È l’interfaccia dalla quale l’utente usufruisce di un servizio digitale, inserendo i dati di una pratica attraverso un modulo: se il servizio prevede un pagamento, i dati di questo sono presenti nella descrizione JSON della pratica e saranno utilizzati dal dispatcher per creare il pagamento.
È il microservizio che, a partire dall'evento di una pratica nel topic applications
, crea l'evento del pagamento nel topic payments
.
È il microservizio che sostituirà il Payment Dispatcher, esso, anziche leggere l'evento di una pratica dal topic applications, esporrà un API che, quando chiamata, crea l'evento del pagamento nel topic payments. Questo permette di avere una soluzione unica per gestire tutti i flussi di pagamento della piattaforma, quindi sia pagamenti creati a partire dalle pratiche, che i pagamenti importati tramite csv o API esterne.
Il microservizio legge gli eventi dal topic payments
e contestualmente aggiorna lo stato della pratica cambiando lo stato da Payment pending a Inviata.
E' il microservizio che gestisce la comunicazione e l’aggiornamento della posizione debitoria con l'Intermediario di pagamento di riferimento. Esso gestisce inoltre il salvataggio delle configurazioni tramite le quali può interagire con l'intermediario di pagamento di riferimento. Queste configurazioni sono inserite dall'operatore mediante l'interfaccia del Core, il quale comunica con il payment proxy mediante delle apposite API che quest'ultimo serve.
E' un microservizio che ascolta dal topic payments e crea un database interrogabile con SQL contenente tutti i pagamenti in stati non definitivi.
E' il microservizio che monitora lo stato dei pagamenti: periodicamente legge su KSQL l'elenco dei pagamenti aperti e invia per ognuno di questi una chiamata alla "update.url" specificata nel pagamento stesso: questa url è una url del proxy stesso che consente al proxy di interrogare l'Intermediario per avere lo stato aggiornato del pagamento.
Questo microservizio si occupa di centralizzare il pagamento online dei dovuti in un'unica API, ovvero quella del checkout di pagoPA, in questo modo si astrae dall'intermediario con cui ci si deve integrare facilitando quindi lo sviluppo dell'integrazione e omogeneizzando l'esperienza di pagamento del cittadino, che diventa quindi indipendente dall'intermediario tramite qui andrà a pagare.
Dettaglio degli stati che attraversa un pagamento nel suo ciclo di vita
Questa pagina descrive l'architettura impiegata per attuare il processo di protocollazione di una pratica
Seguendo l'approccio del viene descritta l'architettura utilizzata per attuare il processo di protocollazione. Partendo da una panoramica ad alto livello si va sempre più in dettaglio nell'architettura fino a raggiungere il secondo livello del C4 model.
Per poter attuare il processo di protocollazione, l'area personale del cittadino si avvale dei sistemi di protocollazione esterni utilizzati dai diversi tenant. È fondamentale notare che la piattaforma è un servizio SaaS di tipo multi-tenant.
L’area personale del cittadino effettua diverse interazioni con il sistema di protocollo, ma tutte seguono una logica comune. Le interazioni avvengono in seguito ad alcune azioni da parte cittadini e degli operatori:
Invio di una pratica da parte del cittadino
Invio di una richiesta di integrazioni da parte di un operatore
Invio di una integrazione da parte del cittadino
Approvazione/rifiuto di una pratica
Altre azioni possibili, mediante configurazioni avanzate del servizio:
Creazione di una pratica da parte di un operatore per conto di un cittadino
Ritiro di una pratica da parte del cittadino
Annullamento di una pratica da parte di un operatore
Per ognuna di queste operazioni viene prodotto un documento. A partire dall'entità documento si costruiscono le richieste per i sistemi di protocollazioni esterni.
Il processo di protocollazione è stato realizzato seguendo i principi del Domain Driven Design utilizzando il pattern Event Sourcing.
Di conseguenza si è scelto di separare il dominio dei servizi dal dominio dei documenti. In questo modo, i documenti sono indipendenti dalla loro fonte di origine e possono essere generati sia per esempio dalle pratiche, sia da altre strutture dati diverse senza che il sistema di protocollazione debba subire modifiche o integrazioni.
La piattaforma è multi tenant e multi servizio: ogni tenant può selezionare un diverso sistema di protocollazione per ogni diverso servizio che offre. Questo significa che da qualche parte la piattaforma deve tenere traccia delle configurazioni di protocollo scelte per un determinato tenant e i servizi abilitati per il sistema scelto. Inoltre va tenuta traccia anche delle configurazioni specifiche di ogni sistema di protocollazione.
Per tenere traccia di tali configurazioni è necessario un meccanismo di storage persistente(il cui tipo è da definire a seconda dei casi), mentre la scelta su chi detiene la responsabilità di gestire tali configurazioni è stata attribuita al Protocol Proxy.
Questa decisione è stata dettata dal fatto che ogni sistema di protocollazione funziona in maniera diversa, e quindi ognuno di essi necessita di una configurazione specifica. Affidando la responsabilità di gestire le configurazioni al Protocol Proxy, si evita di dover modificare, per ogni nuova integrazione, il Core Application, aggirando inoltre il problema di uno sviluppo coordinato con un terzo sistema come il Protocol Proxy.
Si può dedurre quindi che esiste un mapping 1:1 tra i Protocol proxy e i sistemi di protocollazione esterni.
Riguardo a coding, contribuzioni, eventi, API
Nella piattaforma si fa ampio uso del pattern Event Sourcing, perciò è fondamentale che il formato degli eventi sia stabile, versionato e documentato.
Tutti i producer DEVONO includere obbligatoriamente i seguenti campi in ogni evento:
id
identificativo unico dell'entità oggetto dell'evento
UUID
Es: in un pagamento è l'ID del pagamento, in una pratica è l'application_id etc...
event_id
identificativo unico dell'evento
UUID
Non deve esserci per nessun motivo due volte lo stesso ID
event_version
versione dell'evento
Integer or Float
E' un intero (Es: 1, 2, etc..) cambia ad ogni cambiamento NON retrocompatibile.
event_created_at
data dell'evento
ISO8601
Ricordarsi di usare il formato della timezone Europe/Rome (Es: 2022-06-22T15:11:20+02:00
)
app_id
Nome e versione dell'applicativo che ha generato l'evento
$application:$version
Utile per motivi di debug (Es: payment-dispatcher:1.0.15).
Nota importante: Se si genera un nuovo evento a partire da uno esistente, NON vanno ricopiati i campi
event_id
,event_created_at
,event_version
,app_id
. Si tratta di una nuova entità, quindi deve avere nuovi valori.
Chi consuma eventi deve:
Gestire esplicitamente le versioni supportate.
Scartare eventi con versioni non supportate.
Non generare errori per versioni non gestite:
È corretto loggare in DEBUG la ricezione di una versione non supportata.
In condizioni normali, un consumer emette un log a livello INFO solo se:
ha processato con successo l’evento.
ha eseguito un'azione concreta.
Questo approccio evita rumore nei log e rende evidenti solo le operazioni significative.
La piattaforma è dotata di un componente, detto RetryOrchestrator, responsabile della gestione automatica dei retry per eventi temporaneamente non processabili. In caso di errore temporaneo, consente di riprogrammare la consegna dell’evento al consumer in base a una politica configurabile, evitando la perdita di messaggi e riducendo l'intervento manuale.
Quando un servizio X fallisce nel processare un messaggio, questo viene inviato nel Failed Message Topic (FMT).
Il RetryOrchestrator legge i messaggi dal FMT e, sulla base della politica di retry, li ripropone al servizio X.
Se il messaggio esaurisce i tentativi previsti, viene inviato in una Dead Letter Queue (DLQ).
La politica di retry viene definita tramite due variabili d’ambiente principali:
KAFKA_DISPATCHER_POLICY
Specifica la sequenza dei retry come array di coppie NxTm
, dove N
è il numero di tentativi e Tm
è il tempo di attesa (es. 2x1m,1x2m,3x3m
).
KAFKA_RETRY_QUEUE_PREFIX
Prefisso dei topic di attesa su Kafka (es. retryQueue_
).
Con la configurazione:
KAFKA_DISPATCHER_POLICY=2x1m,1x2m,3x3m
KAFKA_RETRY_QUEUE_PREFIX=retryQueue_
Il servizio si aspetta i seguenti topic di retry:
retryQueue_1m
retryQueue_2m
retryQueue_3m
Un messaggio attraverserà quindi una sequenza di 6 tentativi: 2 tentativi a distanza di 1 minuto, 1 tentativo dopo 2 minuti, 3 tentativi dopo 3 minuti ciascuno.
Tutte le date all’interno della piattaforma devono essere rappresentate nel formato ISO 8601, con la seguente struttura:
YYYY-MM-DDTHH:MM:SS+HH:MM
Esempio:
2025-03-12T00:00:00+01:00
Questo formato garantisce coerenza, leggibilità e interoperabilità tra i diversi sistemi e microservizi, soprattutto in contesti distribuiti.
Tutte le date devono essere espresse nel fuso orario Europe/Rome
, che gestisce automaticamente l’ora solare (+01:00
) e l’ora legale (+02:00
).
Non utilizzare
UTC
o altri fusi orari, salvo esplicita eccezione documentata.
Le date devono includere l’offset del fuso orario (+01:00
o +02:00
) come parte della stringa.
Le librerie utilizzate per serializzazione/deserializzazione devono essere configurate per usare Europe/Rome
come timezone di default.
Il formato deve essere rispettato sia in input che in output per tutte le API e gli eventi.
API REST
Tutte le date nei payload (richieste e risposte)
Messaggi su Kafka
Timestamp negli eventi pubblicati
Ogni data deve essere validata secondo il pattern ISO8601 con timezone Europe/Rome
.
I test automatici devono includere:
Verifica del formato in fase di creazione e lettura degli eventi.
Verifica dell’offset corretto (+01:00
o +02:00
) in base alla data.
Gestione corretta dell’ora legale.
Le API della piattaforma seguono i principi di:
I servizi DEVONO esporre solo i verbi HTTP previsti e restituire errore per metodi non supportati.
I metodi seguenti devono essere idempotenti, come da RFC 7231:
DELETE
, GET
, HEAD
, OPTIONS
, PUT
, TRACE
I percorsi (path) delle risorse devono:
Essere al plurale per rappresentare collezioni
Usare kebab-case (es. user-preferences
)
POST /tenants
GET /tenants/{tenant_id}
PUT /tenants/{tenant_id}
PATCH /tenants/{tenant_id}
DELETE /tenants/{tenant_id}
Le URL nelle risposte devono essere sempre espresse in forma assoluta.
Per le risorse collezione, le risposte devono includere una struttura di meta-informazioni e link di navigazione:
{
"meta": {
"page": {
"offset": 5000,
"limit": 20,
"sort": "creationTime"
},
"total": 46561
},
"links": {
"self": "https://api.example.it/items?offset=5000&limit=20&sort=creationTime",
"prev": "https://api.example.it/items?offset=4980&limit=20&sort=creationTime",
"next": "https://api.example.it/items?offset=5020&limit=20&sort=creationTime"
},
"data": [
{ "id": "..." },
{ "id": "..." },
{ "id": "..." }
]
}
I parametri di paginazione da usare nei GET
, ove applicabile, sono:
offset
Offset dell’elemento iniziale
limit
Numero massimo di elementi da restituire
sort
Ordinamento (campo o criterio)
cursor
Puntatore per navigazione cursor-based
embed
Specifica risorse correlate da includere
q
Query full-text o filtro libero
fields
Specifica dei campi da includere nella risposta
Per endpoint con elevata numerosità di dati, si può usare la cursor-based pagination, ad esempio:
GET /payments?created_since=2024-12-01T00:00:00+01:00
Tutti gli errori devono seguire lo standard RFC 7807 - Problem Details for HTTP APIs:
{
"type": "/errors/incorrect-user-pass",
"title": "Incorrect username or password.",
"status": 401,
"detail": "Authentication failed due to incorrect username or password.",
"instance": "/login/log/abc123"
}
Ogni errore può includere:
type
: URL che identifica il tipo di errore
title
: breve descrizione dell’errore
status
: HTTP status code (opzionale)
detail
: descrizione dettagliata (opzionale)
instance
: identificatore specifico dell’occorrenza (opzionale)
{
"title": "Risposta integrazione: Protocollazione esterna [FAKE REGISTRY PROXY] Michelangelo Buonarroti BNRMHL75C06G702B - 1a30529e-9c41-4352-9198-1fac44c5e911",
"id": "a83ead00-2a62-404c-a416-b965a400bcd6",
"app_id": "document-dispatcher:1.3.2",
"event_created_at": "2023-12-21T16:58:49+00:00",
"event_id": "367fd4d2-4c8a-4124-a853-70c67bea0b03",
"event_version": 1,
"registration_data": {
"transmission_type": "Inbound",
"date": "",
"document_number": ""
},
"folder": {
"title": "Protocollazione esterna [FAKE REGISTRY PROXY]-Michelangelo Buonarroti BNRMHL75C06G702B",
"id": ""
},
"status": "DOCUMENT_CREATED",
"type": "integration-response",
"remote_id": "1a30529e-9c41-4352-9198-1fac44c5e911",
"remote_collection": {
"id": "bdefd713-feb9-4bbe-9308-1afe4dcbd858",
"type": "service"
},
"topics": null,
"short_description": "Protocollazione esterna [FAKE REGISTRY PROXY] Michelangelo Buonarroti BNRMHL75C06G702B - 1a30529e-9c41-4352-9198-1fac44c5e911",
"description": "Protocollazione esterna [FAKE REGISTRY PROXY] Michelangelo Buonarroti BNRMHL75C06G702B - 1a30529e-9c41-4352-9198-1fac44c5e911",
"main_document": {
"name": "risposta-a-richiesta-integrazione-cccda494-9d02-4a92-9e05-cbad01096818-202311230314.pdf",
"description": "Risposta a richiesta integrazione: cccda494-9d02-4a92-9e05-cbad01096818 2023-11-23 03:14",
"mime_type": "application/pdf",
"url": "https://servizi.comune-qa.bugliano.pi.it/lang/api/applications/1a30529e-9c41-4352-9198-1fac44c5e911/attachments/241879ee-a133-4456-8473-811781daeb67?version=1",
"md5": "",
"filename": "655f5e343daa3.pdf"
},
"image_gallery": null,
"attachments": [
{
"name": "dummy.pdf",
"description": "Allegato - dummy.pdf",
"mime_type": "application/pdf",
"url": "https://servizi.comune-qa.bugliano.pi.it/lang/api/applications/1a30529e-9c41-4352-9198-1fac44c5e911/attachments/a4c81464-ee91-449a-b2a3-7010c59fdee7?version=1",
"md5": "",
"filename": "dummy.pdf"
},
{
"name": "dummy2.pdf",
"description": "Allegato - dummy2.pdf",
"mime_type": "application/pdf",
"url": "https://servizi.comune-qa.bugliano.pi.it/lang/api/applications/1a30529e-9c41-4352-9198-1fac44c5e911/attachments/d284be8a-2700-48a9-aa4b-fc0136674548?version=1",
"md5": "",
"filename": "dummy2.pdf"
}
],
"related_public_services": null,
"normative_requirements": null,
"related_documents": null,
"life_events": null,
"business_events": null,
"allowed_readers": null,
"tenant_id": "60e35f02-1509-408c-b101-3b1a28109329",
"owner_id": "5b00796e-ef10-4c98-b9f5-b0f7ffd7a17d",
"created_at": "2023-11-23T14:14:23+00:00",
"updated_at": "",
"author": [
{
"type": "human",
"tax_identification_number": "BNRMHL75C06G702B",
"name": "Michelangelo",
"family_name": "Buonarroti",
"street_name": "Via Antica Aia",
"postal_code": "67049",
"email": "[email protected]",
"role": "sender"
}
],
"source_type": "user",
"recipient_type": "tenant"
}
{
"title": "Risposta integrazione: Protocollazione esterna [FAKE REGISTRY PROXY] Michelangelo Buonarroti BNRMHL75C06G702B - 1a30529e-9c41-4352-9198-1fac44c5e911",
"id": "a83ead00-2a62-404c-a416-b965a400bcd6",
"event_created_at": "2023-12-21T16:58:49+00:00",
"event_id": "367fd4d2-4c8a-4124-a853-70c67bea0b03",
"event_version": 1,
"registration_data": {
"transmission_type": "Inbound",
"date": "2023-11-23T14:14:24+00:00",
"document_number": "123456789"
},
"folder": {
"title": "Protocollazione esterna [FAKE REGISTRY PROXY]-Michelangelo Buonarroti BNRMHL75C06G702B",
"id": "n/a"
},
"status": "REGISTRATION_COMPLETE",
"type": "integration-response",
"remote_id": "1a30529e-9c41-4352-9198-1fac44c5e911",
"remote_collection": {
"id": "bdefd713-feb9-4bbe-9308-1afe4dcbd858",
"type": "service"
},
"topics": null,
"short_description": "Protocollazione esterna [FAKE REGISTRY PROXY] Michelangelo Buonarroti BNRMHL75C06G702B - 1a30529e-9c41-4352-9198-1fac44c5e911",
"description": "Protocollazione esterna [FAKE REGISTRY PROXY] Michelangelo Buonarroti BNRMHL75C06G702B - 1a30529e-9c41-4352-9198-1fac44c5e911",
"main_document": {
"name": "risposta-a-richiesta-integrazione-cccda494-9d02-4a92-9e05-cbad01096818-202311230314.pdf",
"description": "Risposta a richiesta integrazione: cccda494-9d02-4a92-9e05-cbad01096818 2023-11-23 03:14",
"mime_type": "application/pdf",
"url": "https://servizi.comune-qa.bugliano.pi.it/lang/api/applications/1a30529e-9c41-4352-9198-1fac44c5e911/attachments/241879ee-a133-4456-8473-811781daeb67?version=1",
"md5": "",
"filename": "655f5e343daa3.pdf"
},
"image_gallery": null,
"attachments": [
{
"name": "dummy.pdf",
"description": "Allegato - dummy.pdf",
"mime_type": "application/pdf",
"url": "https://servizi.comune-qa.bugliano.pi.it/lang/api/applications/1a30529e-9c41-4352-9198-1fac44c5e911/attachments/a4c81464-ee91-449a-b2a3-7010c59fdee7?version=1",
"md5": "",
"filename": "dummy.pdf"
},
{
"name": "dummy2.pdf",
"description": "Allegato - dummy2.pdf",
"mime_type": "application/pdf",
"url": "https://servizi.comune-qa.bugliano.pi.it/lang/api/applications/1a30529e-9c41-4352-9198-1fac44c5e911/attachments/d284be8a-2700-48a9-aa4b-fc0136674548?version=1",
"md5": "",
"filename": "dummy2.pdf"
}
],
"related_public_services": null,
"normative_requirements": null,
"related_documents": null,
"life_events": null,
"business_events": null,
"allowed_readers": null,
"tenant_id": "60e35f02-1509-408c-b101-3b1a28109329",
"owner_id": "5b00796e-ef10-4c98-b9f5-b0f7ffd7a17d",
"created_at": "2023-11-23T14:14:23+00:00",
"updated_at": "",
"author": [
{
"type": "human",
"tax_identification_number": "BNRMHL75C06G702B",
"name": "Michelangelo",
"family_name": "Buonarroti",
"street_name": "Via Antica Aia",
"postal_code": "67049",
"email": "[email protected]",
"role": "sender"
}
],
"source_type": "user",
"recipient_type": "tenant"
}
Il campo "retry_meta" è stato creato dal protocol proxy e contiene il topic di origine ed eventuali campi utili al protocol proxy per tenere lo stato della protocollazione
{
"title": "Risposta integrazione: Protocollazione esterna [FAKE REGISTRY PROXY] Michelangelo Buonarroti BNRMHL75C06G702B - 1a30529e-9c41-4352-9198-1fac44c5e911",
"id": "a83ead00-2a62-404c-a416-b965a400bcd6",
"event_created_at": "2023-12-21T16:58:49+00:00",
"event_id": "367fd4d2-4c8a-4124-a853-70c67bea0b03",
"event_version": 1,
"registration_data": {
"transmission_type": "Inbound",
"date": "",
"document_number": ""
},
"folder": {
"title": "Protocollazione esterna [FAKE REGISTRY PROXY]-Michelangelo Buonarroti BNRMHL75C06G702B",
"id": ""
},
"status": "REGISTRATION_FAILED",
"type": "integration-response",
"remote_id": "1a30529e-9c41-4352-9198-1fac44c5e911",
"remote_collection": {
"id": "bdefd713-feb9-4bbe-9308-1afe4dcbd858",
"type": "service"
},
"topics": null,
"short_description": "Protocollazione esterna [FAKE REGISTRY PROXY] Michelangelo Buonarroti BNRMHL75C06G702B - 1a30529e-9c41-4352-9198-1fac44c5e911",
"description": "Protocollazione esterna [FAKE REGISTRY PROXY] Michelangelo Buonarroti BNRMHL75C06G702B - 1a30529e-9c41-4352-9198-1fac44c5e911",
"main_document": {
"name": "risposta-a-richiesta-integrazione-cccda494-9d02-4a92-9e05-cbad01096818-202311230314.pdf",
"description": "Risposta a richiesta integrazione: cccda494-9d02-4a92-9e05-cbad01096818 2023-11-23 03:14",
"mime_type": "application/pdf",
"url": "https://servizi.comune-qa.bugliano.pi.it/lang/api/applications/1a30529e-9c41-4352-9198-1fac44c5e911/attachments/241879ee-a133-4456-8473-811781daeb67?version=1",
"md5": "",
"filename": "655f5e343daa3.pdf"
},
"image_gallery": null,
"attachments": [
{
"name": "dummy.pdf",
"description": "Allegato - dummy.pdf",
"mime_type": "application/pdf",
"url": "https://servizi.comune-qa.bugliano.pi.it/lang/api/applications/1a30529e-9c41-4352-9198-1fac44c5e911/attachments/a4c81464-ee91-449a-b2a3-7010c59fdee7?version=1",
"md5": "",
"filename": "dummy.pdf"
},
{
"name": "dummy2.pdf",
"description": "Allegato - dummy2.pdf",
"mime_type": "application/pdf",
"url": "https://servizi.comune-qa.bugliano.pi.it/lang/api/applications/1a30529e-9c41-4352-9198-1fac44c5e911/attachments/d284be8a-2700-48a9-aa4b-fc0136674548?version=1",
"md5": "",
"filename": "dummy2.pdf"
}
],
"related_public_services": null,
"normative_requirements": null,
"related_documents": null,
"life_events": null,
"business_events": null,
"allowed_readers": null,
"tenant_id": "60e35f02-1509-408c-b101-3b1a28109329",
"owner_id": "5b00796e-ef10-4c98-b9f5-b0f7ffd7a17d",
"created_at": "2023-11-23T14:14:23+00:00",
"updated_at": "",
"author": [
{
"type": "human",
"tax_identification_number": "BNRMHL75C06G702B",
"name": "Michelangelo",
"family_name": "Buonarroti",
"street_name": "Via Antica Aia",
"postal_code": "67049",
"email": "[email protected]",
"role": "sender"
}
],
"source_type": "user",
"recipient_type": "tenant",
"retry_meta": {
"original_topic": "documents"
}
}
L'ente (es. Comune) deve prima abilitarsi sulla piattaforma PDND (procedura di PagoPA tramite SPID).
Sulla piattaforma PDND (spesso guidati o fatti da noi per l'ente):
Si crea un Client.
Si richiede la Fruizione degli e-service desiderati (es. ANPR stato famiglia). Questa richiesta deve essere approvata dall'ente erogatore.
Si crea una Finalità per ogni e-service richiesto, specificando le motivazioni e l'analisi del rischio. Anche questa finalità deve essere approvata.
Si associa la Finalità approvata e la Chiave Pubblica al Client sulla PDND.
Sulla Piattaforma OpenCity:
Si crea un Client corrispondente a quello creato sulla PDND.
Si genera una Chiave Pubblica (tramite un'API del PDND Connector) e la si associa al Client sulla PDND.
Si inseriscono gli identificativi ottenuti dalla PDND: Client ID, KID e Purpose ID (ParID).
Si abilitano gli E-service (configurati con il Client e la Finalità) a livello di "tenant" (per tutti i servizi compatibili) o di "singolo servizio".
Si configura il Nested Form sul frontend per ricevere i dati da quello specifico e-service.
Le slide OpenCity_Richiesta di fruizione dati via PDND_public link.pdf sono ottime per gli step lato Ente/PDND.
Le registrazioni video sono essenziali per vedere il flusso pratico:
La "Videolezione 1: Formazione Pdnd" per il contesto generale, gli attori, e la demo di configurazione/fruizione;
La "Videolezione 2: Revisione codice PDND" per il funzionamento interno del Connector (flusso GET/POST, struttura codice).
Serve l'accesso alla piattaforma PDND (test/collaudo) con permessi adeguati (amministratore è meglio). Richiedere i permessi per comuni di test come Asolo o Vicopisano. Configurare un ambiente locale per testare il PDND Connector, magari copiando le configurazioni esistenti da S3 (richiedere credenziali).
Seguire la Guida Passo Passo per l'Integrazione della PDND: identificare l'e-service (con PMs), studiarlo (dati vs form), e creare una issue dettagliata che includa lo studio, la mappatura, e la definizione della risposta del Connector.
Ci sono aree in miglioramento: lo Swagger del Connector potrebbe non funzionare nell'ambiente QA, la gestione degli ambienti collaudo/produzione sulla UI della Stanza ha delle criticità (è stata aperta una issue per questo), la strategia di caching nel Connector ha delle criticità.
L’area personale del cittadino effettua diverse interazioni con il sistema di protocollo, ma tutte seguono una logica comune. Le interazioni avvengono in seguito ad alcune azioni da parte cittadini e degli operatori:
invio di una pratica da parte del cittadino
invio di una richiesta di integrazioni da parte di un operatore
invio di una integrazione da parte del cittadino
approvazione/rifiuto di una pratica
Altre azioni possibili, mediante configurazioni avanzate del servizio:
creazione di una pratica da parte di un operatore per conto di un cittadino
ritiro di una pratica da parte del cittadino
annullamento di una pratica da parte di un operatore
Per ognuna di queste operazioni viene prodotto un documento principale dotato di eventuali allegati.
Per ogni documento ci aspettiamo di poter sempre specificare:
Un oggetto
Un mittente (il soggetto che ha formato il documento)
Un destinatario (solitamente l’ufficio che deve prendere in carico la richiesta)
Data di registrazione della pratica
Tipo di trasmissione (in entrata, in uscita, interno)
Classificazione (solitamente sotto forma di indice di classificazione nell’albero del titolario, es 6.3.4)
Per ogni documento protocollato ci aspettiamo di ricevere i seguenti dati:
Il Numero di protocollo (un identificativo unico e persistente)
Data di registrazione nel protocollo
(opzionale) Una impronta digitale del documento protocollato
Tutti i documenti di una pratica sono raccolti in un fascicolo per il quale ci aspettiamo di poter specificare
L’oggetto
(opzionale) La classificazione
Per ogni integrazione sono necessarie solitamente le seguenti chiamate API o webservice:
Ricerca di un documento per Numero di protocollo
Invio di un Documento principale
Invio di allegati al documento principale
Protocollazione di un documento e dei suoi allegati: se disponibile, preferiamo avere una chiamata atomica, che protocolla il documento principale e i suoi allegati.
Ricerca di un fascicolo per Numero di fascicolo
Creazione di un fascicolo
Aggiunta di un documento ad un fascicolo esistente
Condivisione documentazione:
Documentazione delle API o dei WebService
ambiente di test (endpoint, credenziali di accesso)
Esempio di chiamate necessarie per fare una protocollazione di un documento e dei suoi allegati
Test delle chiamate al sistema di protocollo effettuate in modo manuale
Validazione della protocollazione manuale da parte dell’Ente
Implementazione e rilascio dell’integrazione in ambiente di Quality Assurance
Validazione su un servizio di test
Rilascio in ambiente di produzione
(opzionale) Validazione su un servizio di produzione e annullamento della protocollazione effettuata
L’ultimo test effettuato in ambiente di produzione non è strettamente necessario, ma nella nostra esperienza può rivelare differenze nella configurazione del sistema di protocollo che possono diventare bloccanti.
CREATION_PENDING
pagamento in attesa di essere creato sull'IdP
CREATION_FAILED
pagamento di cui è fallita la creazione sull'IdP
PAYMENT_PENDING
pagamento creato sull'IdP e in attesa di essere eseguito dall'utente
PAYMENT_STARTED
procedura di pagamento iniziata dall'utente
COMPLETE
pagamento completato a seguito di conferma dalI'dP
PAYMENT_FAILED
pagamento fallito a causa di scadenza del termine ultimo entro cui doveva essere eseguito
nei proxy sviluppati questo stato non è quasi mai stato
utilizzato
CANCELED
pagamento annullato
EXPIRED
pagamento scaduto
E’ l'infrastruttura digitale italiana che facilita l'interoperabilità tra sistemi informativi delle Pubbliche Amministrazioni.
Il suo scopo primario è condividere dati in modo sicuro, standard e trasparente.
E' uno dei due approcci adottati da Opencity Italia per implementare il principio del (o one-time entry), evitando di chiedere al cittadino dati già in possesso di altre PA (es. residenza, stato famiglia da ANPR).
PDND: La piattaforma centrale che gestisce le interazioni e la sicurezza.
Erogatori: Gli enti che mettono a disposizione i dati tramite e-service (es. ANPR, INPS).
Fruitori: Gli enti (come i Comuni, che usano OpenCity) che hanno bisogno di accedere a questi dati.
La Piattaforma (OpenCity): L'interfaccia lato ente e cittadino dove i dati vengono visualizzati e usati.
PDND Connector: Il microservizio chiave che funge da ponte tra OpenCity e l’ecosistema PDND (compresi gli enti erogatori). Ha il compito di:
Gestire le configurazioni e il materiale crittografico degli enti fruitori;
Interagire con la PDND, per conto dei fruitori, al fine di ottenere i voucher di sicurezza;
Utilizzare i voucher per accedere in modo sicuro agli e-service esposti via API dagli enti erogatori, garantendo che ogni cittadino acceda a tutti e soli i dati di sua pertinenza.
Garantire la conservazione dei voucher secondo le linee guida.
In sintesi, il PDND Connector centralizza la logica di integrazione e sicurezza, permettendo a OpenCity di comunicare con la PDND in modo trasparente, conforme e scalabile.
Non sono generici servizi API, ma specifici servizi erogati dagli enti che forniscono dati ben definiti (es. "C021 - Accertamento Stato Famiglia" da ANPR). Possono avere diverse versioni, per poter essere utilizzati bisogna farne richiesta e in seguito associarli ad una finalità.
Un oggetto di configurazione fondamentale sulla PDND e su OpenCity. Raggruppa le informazioni necessarie per gestire e amministrare gli e-service che un ente (fruitore) vuole usare. L'ente, configurando il client di Opencity Italia, può accedere a tutti gli e-service che si vogliono integrare nei servizi digitale. Per poter utilizzare gli e-service, il client deve essere associato alle relative finalità.
Il motivo (anche legale) per cui un ente richiede l'accesso a un determinato e-service. È strettamente legata a uno o più e-service e richiede una specifica "Analisi del rischio". Per ampliare l'adozione della PDND, la piattaforma consente di creare tanti servizi digitali utilizzando una singola finalità.
Un token di sicurezza temporaneo che il PDND Connector ottiene dalla PDND. Questo voucher è essenziale perché funge da credenziale per autenticarsi e autorizzarsi direttamente presso l'ente erogatore quando si chiede il dato tramite l'e-service. Il voucher è relativo ad uno specifico client, una specifica finalità e una specifica coppia chiavi di crittografia.
Il componente sulla nostra piattaforma che riceve e visualizza i dati ottenuti tramite la PDND. Alcune proprietà nel suo codice definiscono quale e-service utilizzare e quali campi specifici riceveranno i dati PDND certificati.
Sono gli identificativi cruciali ottenuti dalla piattaforma PDND dopo aver configurato Client, Chiave Pubblica e Finalità. Vanno copiati dalla PDND e inseriti nella configurazione del Client corrispondente sulla nostra piattaforma.
Opencity Italia integra l'autenticazione Spid/CIE mediante il componente dedicato OpenLogin, dialogando con esso con protocollo oAuth2. Lo stesso componente può essere usato da altri applicativi.
Una autenticazione con SSO tra Opencity Italia e un applicativo in uso allo stesso Ente può essere agevolmente realizzata se può essere garantito il supporto per il protocollo oAuth2.
Vediamo uno schema generale degli attori in gioco:
Vediamo un flusso di autenticazione di un cittadino che fa login prima sull'area personale e successivamente sull'applicativo di terze parti:
L'applicativo di terze parti dovrà essere dotato di un opportuno client oAuth2. Ipotiziamo che le url coinvolte siano le seguenti e mostriamo una tipica configurazione del client.
Per attivare un nuovo client è necessario fornire:
La URL da autorizzare come redirct uri del client
La URL di logout
L'attivazione comprende i seguenti dati
redirect_url: http://servizi.comune.bugliano.pi.it/lang/auth/login-oauth
url_authorize: http://login.comune.bugliano.pi.it/authorize
url_access_token: http://login.comune.bugliano.pi.it/token
url_resource_owner_details: http://login.comune.bugliano.pi.it/api/profile
url_logout: http://login.comune.bugliano.pi.it/logout?redirect=http://servizi.comune.bugliano.pi.it/lang
client_id: {client_id} fornito da Opencity Labs
client_secret: {client_secret} fornito da Opencity Labs
L'aggiunta di un tema grafico richiede una modifica al Core della piattaforma, in queste pagine si da indicazione dei temi presenti e di come predisporre una Merge-Request per aggiungerne uno nuovo.
Step preparativi alla modifica:
Clonare il repository https://gitlab.com/opencity-labs/area-personale/core
Posizionarsi sul branch master
Creare un nuovo branch di nome add-theme-$themename e spostarsi sul nuovo branch con il comando git checkout -b "add-theme-$themename"
sostituendo con il nome del nuovo tema che si vuole creare (per le indicazioni sui nomi dei temi vedere sotto)
Creare una MR sul branch che effettui squash di tutti i commit del branch ed elimini il branch una volta mergiato come di seguito:
Successivamente creare un nuovo file in ./assets/styles
copiando il contenuto di seguito che dovrebbe coincidere con il file default.scss.
Il nome del file deve coincidere con il nome che avrà il tema e avere l'estensione .scss (l nome del tema può contenere solo lettere e il carattere `-`, deve rispondere cioè alla espressione regolare [a-z][a-z0-9-]+. Es: se si vuole creare il tema "amaranto" aggiungiamo un file amaranto.scss
):
@charset "UTF-8";
$primary: #3478bd;
$primary-a5: $primary;
$primary-a6: $primary;
$primary-a7: $primary;
// Header
$header-slim-bg-color: #2b649e; // Colore di background del top header
$header-slim-button-color: #2b649e; // Colore di background del bottone di accesso all'area personale
$header-slim-button-hover-color: $primary; // Colore di background del bottone di accesso all'area personale su hover
$header-center-bg-color: #3478bd; // Colore di background del header centrale
//$header-center-text-color: #fff; // Colore del testo del header centrale
// Footer
$bg-footer: #16334f; // Colore di background del footer
// Content
$card-link-color: $primary; // Colore link all'interno delle card
@import "core";
Nel file cambiare le opzioni ed i colori a proprio piacimento
Si compila il nuovo tema su webpack aggiungendo nel file webpack.config.js StyleEntry come di seguito: .addStyleEntry('amaranto', './assets/styles/amaranto.scss')
(Il nome deve coincidere con il nome del file creato in precedenza). I colori devono tenere conto della linea guida Accessibilità.
Si aggiunge alla piattaforma il nuovo tema aggiornando nel file src/Model/Tenant/ThemeOptions.php la costante THEMES
e inserendo il nuovo tema come di seguito: 'Amaranto' => 'amaranto'
Esempio dettagliato di una integrazione di un modulo di Opencity Italia che attinge a una API protetta mediante l'API gateway opensource GovWay di Link.it
API privata, che si intende esporre mediante GovWay:
https://192.168.1.111:8000/conti-correnti
L'API risponde un dato privato se viene correttamente inserito un parametro in get tax_code che identifica il codice fiscale del cittadino che esegue la richiesta:
https://192.168.1.111:8000/items/conti-correnti?tax_code=AAABBBXX...
L'API non ha un controllo interno che permette di rispondere ad ogni cittadino solo con i suoi dati, l'API è aperta e risponde a qualunque chiamata.
In questo esempio si desidera esporre l'API privata su un indirizzo pubblico e filtrare le chiamate in modo che ogni cittadino possa consultare l'API solo per i propri dati.
L'API esposta è la seguente:
https://govway-qa-out.boat.opencontent.io/govway/Ente/directus/v1
Quando viene chiamata quest'ultima url si desidera imporre un controllo sulla la presenza del token JWT e sul parametro della chiamta. La chiamata deve raggiungere l'API privata SOLO se:
il token è presente
il token è validato mediante la chiave pubblica esposta in JWKS
viene passato il parametro tax_code
in GET
il parametro tax_code corrisponde al valore username
del token
Quest'ultimo punto è il più importante perché ci da la certezza che il cittadino che sta facendo la richiesta è lo stesso che si è autenticato.
E' necessario creare una token policy, che poi verrà assegnata all'erogazione delle API
Il file jwks è stato scaricato da https://servizi.comune-qa.bugliano.pi.it/.well-known/jwks.json e inserito in un path raggiungibile da GovWay: nel nostro caso essendo GovWay installato con Docker abbiamo fatto uso della direttiva volumes
per condividere il file con il container di GovWay.
Fare attenzione inoltre che il kid
della chiave pubblica è specifico per ogni tenant e deve essere quello corretto.
Predisposta la policy deve essere applicata all'Erogazione API
Il controllo del contenuto viene fatto imponendo che l'informazione riportata nel token (${tokenInfo:username}
) e quella inserita come parametro GET della chiamata (${query:filter[tax_code]}
) abbiano lo stesso valore. Resta inteso che il valore nel token è vincolato dalla piattaforma, mentre il parametro tax_code
è scelto da chi ha progettato l'API che viene protetta.
A questo punto la configurazione è completa.
In questa pagina viene fornito un esempio di chiamata API per l'importazione dovuti
Come descritto nella sezione precedente tramite le API dei jobs è possibile creare un processo di importazione dovuti.
Per poter procedere correttamente all'importazione bisogna prima creare un servizio a cui i dovuti devono essere collegati.
Questa operazione può essere fatta tramite interfaccia nella sezione admin, il servizio creato dovrà essere di tipo "Servizio di pagamento dovuti"
Per il servizio appena creato dovrà essere configurato anche il gateway di pagamento
Per poter creare un Job di importazione dovuti bisogna eseguire una chiamata POST all' endpoint api/jobs
Il payload per la creazione di un Job è così composto
{
"attachment": {
"name": "dovuti.csv",
"mime_type": "text/csv",
"file": "bm90aWNlX251bWJlsYW1vdW50LHJlYXNvbixjcmVhdGVkX2F0LGV4cGlyZV9hdCxjcmV..."
},
"args": {
"service_id": "a5a9bf9f-7b5c-4aed-b020-5bf2d1ca75ab"
},
"type": "import_dovuti"
}
Dove:
attachments -> è il file csv contenente i dovuti da importare
Il file csv deve acere un template specifico di questo tipo:
service_id -> è un parametro specifico del tipo di job import_dovuti ed indica su quale servizio devono essere agganciati i dovuti importati
La piattaforma Opencity Italia supporta questa possibilità mediante una tecnica che permette di convertire una sessione basata su cookie in un token JWT.
Questo consente di usare l'autenticazione senza necessariamente usare lo stesso cookie:
integrarsi con altri applicativi di terze parti che possono usare un client JWT;
fare chiamate cross-origin (CORS) autenticate dove non è semplice o possibile sfruttare lo stesso cookie di autenticazione;
Come molti applicativi web anche Opencity Italia usa i cookie per implementare la sessione utente, il cookie si ottiene semplicemente l'indirizzo di accesso degli utenti: ad esempio per il nostro Ente di demo è
In base alla , si viene rediretti su un provider di autenticazione (tipicamente un sistema che permette il login con Spid/CIE), si eseguono tutti gli step necessari e infine si torna sull'area personale con una sessione valida.
A questo punto nel proprio brower è possibile rintracciare facilmente il cookie della piattaforma.
Il cookie di sessione viene inviato in modo trasparente all'utente dal browser al sito che lo ha generato, e questo garantisce che l'utente abbia sempre una sessione valida quando naviga nella sua area personale.
Per fare chiamate alle API però è necessario convertire il Cookie in Token JWT, per farlo esiste una apposita API, nel nostro esempio:
Per simulare la chiamata con Postman ad esempio:
Il token JWT può essere usato adesso per fare chiamate alle API che richiedono una autenticazione, ad esempio posso usare l'interfaccia Swagger delle API come segue.
Faccio click in alto a destra sul comando Authorize
A questo punto posso chiamare le API che richiedono autenticazione come la /applications
All'interno del token sono presenti le seguenti informazioni:
Il campo "retry_meta" contiente le informazioni inserite dal sistema di retry. Queste dovrebbero essere ignorate da protocol proxy
{
"title": "Risposta integrazione: Protocollazione esterna [FAKE REGISTRY PROXY] Michelangelo Buonarroti BNRMHL75C06G702B - 1a30529e-9c41-4352-9198-1fac44c5e911",
"id": "a83ead00-2a62-404c-a416-b965a400bcd6",
"event_created_at": "2023-12-21T16:58:49+00:00",
"event_id": "367fd4d2-4c8a-4124-a853-70c67bea0b03",
"event_version": 1,
"registration_data": {
"transmission_type": "Inbound",
"date": "",
"document_number": ""
},
"folder": {
"title": "Protocollazione esterna [FAKE REGISTRY PROXY]-Michelangelo Buonarroti BNRMHL75C06G702B",
"id": ""
},
"status": "REGISTRATION_FAILED",
"type": "integration-response",
"remote_id": "1a30529e-9c41-4352-9198-1fac44c5e911",
"remote_collection": {
"id": "bdefd713-feb9-4bbe-9308-1afe4dcbd858",
"type": "service"
},
"topics": null,
"short_description": "Protocollazione esterna [FAKE REGISTRY PROXY] Michelangelo Buonarroti BNRMHL75C06G702B - 1a30529e-9c41-4352-9198-1fac44c5e911",
"description": "Protocollazione esterna [FAKE REGISTRY PROXY] Michelangelo Buonarroti BNRMHL75C06G702B - 1a30529e-9c41-4352-9198-1fac44c5e911",
"main_document": {
"name": "risposta-a-richiesta-integrazione-cccda494-9d02-4a92-9e05-cbad01096818-202311230314.pdf",
"description": "Risposta a richiesta integrazione: cccda494-9d02-4a92-9e05-cbad01096818 2023-11-23 03:14",
"mime_type": "application/pdf",
"url": "https://servizi.comune-qa.bugliano.pi.it/lang/api/applications/1a30529e-9c41-4352-9198-1fac44c5e911/attachments/241879ee-a133-4456-8473-811781daeb67?version=1",
"md5": "",
"filename": "655f5e343daa3.pdf"
},
"image_gallery": null,
"attachments": [
{
"name": "dummy.pdf",
"description": "Allegato - dummy.pdf",
"mime_type": "application/pdf",
"url": "https://servizi.comune-qa.bugliano.pi.it/lang/api/applications/1a30529e-9c41-4352-9198-1fac44c5e911/attachments/a4c81464-ee91-449a-b2a3-7010c59fdee7?version=1",
"md5": "",
"filename": "dummy.pdf"
},
{
"name": "dummy2.pdf",
"description": "Allegato - dummy2.pdf",
"mime_type": "application/pdf",
"url": "https://servizi.comune-qa.bugliano.pi.it/lang/api/applications/1a30529e-9c41-4352-9198-1fac44c5e911/attachments/d284be8a-2700-48a9-aa4b-fc0136674548?version=1",
"md5": "",
"filename": "dummy2.pdf"
}
],
"related_public_services": null,
"normative_requirements": null,
"related_documents": null,
"life_events": null,
"business_events": null,
"allowed_readers": null,
"tenant_id": "60e35f02-1509-408c-b101-3b1a28109329",
"owner_id": "5b00796e-ef10-4c98-b9f5-b0f7ffd7a17d",
"created_at": "2023-11-23T14:14:23+00:00",
"updated_at": "",
"author": [
{
"type": "human",
"tax_identification_number": "BNRMHL75C06G702B",
"name": "Michelangelo",
"family_name": "Buonarroti",
"street_name": "Via Antica Aia",
"postal_code": "67049",
"email": "[email protected]",
"role": "sender"
}
],
"source_type": "user",
"recipient_type": "tenant",
"retry_meta": {
"next_attempt_at": "2023-11-23 14:04:25 +0000 UTC",
"original_topic": "documents",
"retry_counter": 1
}
}
{
"typ": "JWT",
"alg": "RS256"
}
{
"iat": 1711639327,
"exp": 1712503327,
"roles": [
"ROLE_CPS_USER",
"ROLE_USER"
],
"username": "AAAAAANNANNAMNNNA",
"id": "ba23337b-7b9e-4e5a-9566-e3f0583822f7",
"tenant_id": "5962qqfb-15b7-4407-b203-47ee3456f6c3"
}
In questa pagina viene fatta una breve descrizione delle API per la gestione dei Jobs (processi asincroni)
La piattaforma consente la possibilità da parte di sistemi esterni di creare/modificare/visualizzare ed eliminare oggetti Job
Ad esempio nel nostro ambiente di demo è possibile consultare le API dedicate a questo scopo:
https://servizi.comune-qa.bugliano.pi.it/lang/api/doc#jobs
Questi speciali oggetti sono dei processi onerosi che vengono creati e poi eseguiti in modo asincrono dalla piattaforma.
Esempio di interazione tra un sistema esterno e le API dei jobs:
In fase di creazione del Job viene specificato tramite il parametro type il tipo di processo (type) che verrà eseguito dal sistema.
In base al tipo di processo creato i parametri attachment e args possono cambiare. Attualmente l'unico processo disponibile è import_dovuti
Per poter creare un Job bisogna eseguire una chiamata POST all' endpoint api/jobs
Il payload per la creazione di un Job è così composto
{
"attachment": {
"name": "string",
"mime_type": "text/csv",
"file": "ZXNlbXBpbw== OR www.example.com"
},
"type": "string",
"args": {
"additionalProp1": "string",
"additionalProp2": "string",
"additionalProp3": "string"
}
}
Dove:
attachment -> è il file in base64 che verrà processato
type -> è il tipo di processo che deve essere eseguito
args -> sono dei parametri necessari al tipo di processo
L' API rispondere con un codice HTTP 201
e con l'id del Job creato
Per poter recuperare informazioni su Job bisogna eseguire una chiamata GET all' endpoint api/jobs/{id}
Il payload di risposta delle API è così composto:
{
"attachment": {
"name": "string",
"mime_type": "text/csv"
},
"id": "string",
"type": "string",
"status": "string",
"args": {
"additionalProp1": "string",
"additionalProp2": "string",
"additionalProp3": "string"
},
"output": "string",
"running_output": "string",
"started_at": "2024-05-31T09:19:18.779Z",
"terminated_at": "2024-05-31T09:19:18.779Z",
"created_at": "2024-05-31T09:19:18.779Z",
"updated_at": "2024-05-31T09:19:18.779Z"
}
Dove:
status -> è lo stato attuale del job e può assumere i seguenti valori pending, cancelled, running, finished, failed
output -> l'output finale del processo, può contenere un errore in caso di processo failed
running_output -> l'output corrente (Es. la riga del file che si sta processando)
Uno degli utilizzi più significativi che si può fare con i Job è l'importazione di contenuti come ad esempio l'importazione dei dovuti
In questa sezione viene descritta la modalità di integrazione con un servizio di terze parti protetto da autenticazione tramite il token JWT dell'utente.
Dataset e micro servizi esterni alla piattaforma possono essere facilmente integrati tramite il flusso di autenticazione basato su JWT (JSON Web Token) e JWKS (JSON Web Key Set)
Questo flusso di autenticazione prevede alcuni passaggi chiave e interazioni tra:
client (browser dell'utente)
area personale
micro servizio esterno
Di seguito il processo punto per punto:
Il client invia una richiesta all'area personale per ottenere un JWT, spesso includendo le credenziali dell'utente o altre forme di autorizzazione.
Come descritto nella sezione è possibile per un utente autenticato richiedere un token di autenticazione per la propria sessione.
Questa operazione può essere effettuata facendo una chiamata alle API:
oppure come utente non autenticato effettuando una chiamata POST alle API
L'area personale autentica il client, genera un JWT e lo firma con una chiave privata.
Il token viene quindi restituito al client.
Il client invia una richiesta al micro servizio esterno per accedere a una risorsa protetta, includendo il JWT nell'intestazione Authorization.
Per verificare la validità del token il micro servizio esterno deve:
Estrarre il JWT dalla richiesta
Recuperare il JWKS dall'area personale, il JWKS contiene le chiavi pubbliche necessarie per verificare la firma del JWT.
Verificare la firma del JWT e convalidare le sue richieste
In base alla validità del JWT il micro servizio esterno concede o nega l'accesso alla risorsa richiesta.
Oltre alla validità del token si possono verificare le affermazioni in esso contenute.
Il payload del token prodotto dall'area personale presenta delle informazioni utili a capire se quello specifico utente oltre ad avere un token valido ha i permessi per ottenere la risorsa richiesta.
Di seguito un esempio di payload del token
curl --request POST \
--url https://servizi.comune.bugliano.pi.it/lang/api/auth \
--header 'Content-Type: application/json' \
--data '{
"username": "username",
"password": "password"
}'
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InNkYy1xYSJ9.eyJpYXQiO.c2VybmFtZSI6ImFkbWluIiwiaWQiOiI2ZmZkZWRkZi04NmZhLTRjYmEtODUzNC05YTBlNjZmZTNlYmYiLCJ0ZW[...]5hbnRfaWQiOiI2MGUzNWYwMi0xNTA5LTQwOGMtYjEwMS0zYjFhMjgxMDkzMjkifQ.lFe0MR-LAj[...]RjoVvqwk3OX23T642ETu8PUy8sFaVRf1oM1qAnPhLEpnOcrIQY65mpKw6mrJ1rzZM5OVvEeOc4xxmtgcBOfMEBJo_Dw1pMfZHOv2S1S50Zr9XNxk0LcfWjXGdC7wy81eF7UuF-3cX9W"
}
curl --request GET \
--url '{Url API micro servizio esterno}' \
--header 'Authorization: Bearer {JWT Token}'
{
"iat": 1718278670,
"exp": 1719142670,
"roles": [
"ROLE_CPS_USER",
"ROLE_USER"
],
"username": "BNRMHL75C06G702B",
"id": "5b00796e-ef10-4c98-b9f5-b0f7ffd7a17d",
"tenant_id": "60e35f02-1509-408c-b101-3b1a28109329"
}
La piattaforma consente la creazione di servizi digitali che prevedono l'associazione di uno o più pagamenti, gestiti in modo integrato con il sistema pagoPA o tramite registrazione manuale di marche da bollo.
I pagamenti possono avvenire in due modalità:
In questo caso non è prevista alcuna integrazione con sistemi esterni. Il cittadino registra i dati delle marche da bollo acquistate autonomamente (numero identificativo e data), a corredo della pratica che sta presentando.
La piattaforma gestisce in modo integrato i pagamenti elettronici tramite l’infrastruttura pagoPA, attraverso l’interazione con uno degli intermediari accreditati.
La creazione del pagamento avviene automaticamente durante l’invio della pratica o a seguito di un'azione dell’operatore, che può:
generare posizioni debitorie direttamente tramite configurazione della pratica;
importare manualmente pagamenti pendenti da sistemi esterni tramite file CSV, rendendoli disponibili al cittadino nell’Area Personale.
L’integrazione consente quindi:
la creazione, la gestione e l’aggiornamento dei pagamenti;
la ricezione automatica delle notifiche di esito da parte del nodo pagoPA;
l’allineamento in tempo reale dello stato della pratica all'interno dell’Area Personale.
Nota: È attualmente in fase di sperimentazione l’introduzione della Marca da Bollo Digitale, che permetterà il pagamento del bollo direttamente tramite pagoPA, superando la modalità manuale tradizionale.
L’integrazione dei pagamenti nella piattaforma porta numerosi vantaggi operativi e strategici per gli enti:
Automazione dei processi di incasso: La generazione automatica delle posizioni debitorie e la ricezione dell’esito del pagamento riducono il carico di lavoro manuale per gli operatori.
Migliore esperienza per il cittadino: Il cittadino può completare i pagamenti online in autonomia, in un contesto centralizzato (Area Personale), senza dover interagire con sistemi esterni o recarsi fisicamente presso l’ente.
Tracciabilità e trasparenza amministrativa: Tutti i pagamenti vengono registrati con ID univoci, ricevute elettroniche e cronologia completa, garantendo sicurezza, conformità normativa e facilità di riconciliazione.
Di seguito alcuni casi d’uso reali in cui l’integrazione dei pagamenti viene utilizzata all’interno della piattaforma:
In questi scenari, la piattaforma crea autonomamente una posizione debitoria pagoPA in seguito a un’azione del cittadino o alla configurazione di un servizio digitale:
Al termine della compilazione dell’istanza, un pagamento di €20 per i diritti di segreteria. Il cittadino può pagare direttamente dall’Area Personale.
In alcuni casi, l’ente può caricare manualmente un file contenente posizioni debitorie provenienti da altri sistemi (es. tributi, multe, TARI):
Il comune importa le posizioni già determinate nel gestionale tributi. Il cittadino visualizza e paga queste richieste direttamente dalla propria Area Personale.
Alcuni servizi non richiedono l’integrazione con pagoPA, ma prevedono il pagamento tramite marche da bollo acquistate dal cittadino:
Il modulo richiede l’inserimento del numero identificativo e della data di una marca da bollo da €16 acquistata autonomamente. Il cittadino la registra all’interno della pratica.
L’integrazione con il sistema di pagamento si sviluppa in diverse fasi operative. Di seguito una descrizione dettagliata del processo.
In questa fase iniziale viene contattato il fornitore dell’intermediario di pagamento per ottenere:
La documentazione tecnica delle API, contenente le specifiche delle operazioni fondamentali:
creazione del pagamento;
generazione dell’avviso cartaceo;
ottenimento del link per il pagamento online (checkout);
recupero dello stato del pagamento;
download della ricevuta telematica (RT).
Le informazioni sull’ambiente di test, in particolare:
l’URL del web service (WSDL per chiamate SOAP o documentazione Postman/Swagger per REST);
le credenziali di accesso e i dati fittizi da usare per i test (codici tipo dovuto, codice esercente, identificativi utente, ecc.).
Questa fase prevede lo sviluppo vero e proprio dell’integrazione, seguendo:
la documentazione fornita dall’intermediario;
la documentazione tecnica della piattaforma Opencity;
gli standard architetturali e funzionali adottati internamente.
L’integrazione viene generalmente realizzata tramite un proxy che standardizza la comunicazione tra la piattaforma e l’intermediario.
Completato lo sviluppo, si procede alla validazione funzionale nell’ambiente di collaudo.
I test previsti includono:
Test automatici delle API di configurazione del pagamento;
Test manuale della configurazione del pagamento tramite form (interfaccia admin);
Test di creazione del pagamento a partire da una pratica compilata da un cittadino;
Test del completamento del pagamento attraverso il checkout pagoPA;
Test di scaricamento della ricevuta telematica a pagamento avvenuto.
Test di importazione mediante file csv di un pagamento pendente creato esternamente alla piattaforma
Una volta superata la fase di collaudo, si procede con il test in ambiente di produzione, in collaborazione con un cliente pilota.
Questa fase è fondamentale perché:
emergono spesso comportamenti specifici o difformità rispetto all’ambiente di test;
si rilevano dettagli contestuali noti solo al cliente, che può accedere al backoffice dell’intermediario e fornire informazioni utili;
si verificano disallineamenti tra test e produzione dovuti a configurazioni, dati o logiche specifiche dell’ente.
Prima di procedere, è necessario concordare la tempistica e la modalità di test con il cliente coinvolto.
Durante lo sviluppo e la messa in produzione dell’integrazione con i sistemi di pagamento, possono emergere alcune criticità ricorrenti. Di seguito le principali:
In alcuni casi, le API degli intermediari sono accessibili solo tramite VPN o da indirizzi IP autorizzati, sia in ambiente di test che in ambiente di produzione.
Per usufruire delle API, è necessario comunicare al fornitore gli indirizzi IP fissi da autorizzare. Questo vincolo può essere particolarmente rilevante quando lo sviluppo dell’integrazione è affidato a un’azienda esterna.
In questi casi si possono adottare due strategie:
Configurare l’accesso alla VPN di Opencity Labs per consentire lo sviluppo in modo controllato e sicuro.
Richiedere lo sblocco dell’IP dell’azienda esterna (se disponibile) da parte del fornitore dell’integrazione, insieme a quelli di Opencity Labs.
La documentazione fornita dai fornitori del sistema di pagamento può presentare, in alcuni casi, ambiguità, imprecisioni o mancanze. Questo può ostacolare:
la corretta progettazione dell’integrazione;
lo sviluppo in ambiente di test;
la validazione finale in produzione.
In presenza di dubbi o problemi tecnici, è fondamentale contattare tempestivamente il supporto tecnico del fornitore e fornire esempi concreti del comportamento anomalo riscontrato.
Gli ambienti di test – sia del fornitore intermediario, sia di pagoPA – possono presentare frequenti disservizi. Le criticità più comuni si verificano nei seguenti punti del flusso:
1. Ambiente di test dell’intermediario di pagamento I problemi più frequenti includono:
errori nella creazione del pagamento;
mancato download dell’avviso o della ricevuta telematica;
errori di aggiornamento dello stato del pagamento;
malfunzionamenti nel redirect verso il portale di pagamento.
In questi casi è necessario contattare il supporto tecnico dell’intermediario, fornendo tutti i dettagli tecnici (endpoint, parametri inviati, timestamp, codice esercente, ecc.).
2. Ambiente di test di pagoPA I problemi si manifestano generalmente dopo il redirect al portale pagoPA, in particolare:
nella schermata di inserimento dei dati della carta;
nel flusso di conferma o ricezione dell’esito.
Per queste problematiche è necessario contattare il supporto tecnico di pagoPA via email all’indirizzo: [email protected]
Molti aspetti implementativi dipendono più dalla normativa che da nostre scelte progettuali, in questa pagina si evidenziano gli uni e le altre.
Sono requisiti che dipendono da nostre scelte implementative, possono cambiare man mano che evolviamo la piattaforma, per supportare nuovi casi d'uso (pagamento dei dovuti) oppure perché ci accorgiamo che abbiamo commesso qualche errore nella progettazione.
Gestione di pagamenti spontanei
Gestione di pagamenti con bilancio
Gestione della notifica dello stato del pagamento da parte dell'IdP - se disponibile - o del polling se non è disponibile la notifica
Se è implementato il polling, questo deve avvenire con un esponenziale che assicuri alcuni check nel giro di pochi minuti e poi si diradi fino a un massimo di un check al giorno fino alla scadenza del pagamento o 1 anno se la scadenza è inferiore.
Gestione delle configurazioni a livello di Tenant e di Servizio
Avere una specifica in formato OpenAPI v3
Rispettare la : in particolare per quanto riguarda
(4.2)
(4.3) optando per la scelta di snake_case
per i nomi degli attributi
logging (4.4)
risultare valido nel
Sappiamo che in molti topic ci sono diversi eventi mal formati, per vari errori e assenza di uno schema registry, quindi è necessario validare gli eventi in input e opzionalmente in output (lo fanno le nostre prime implementazioni in python, ma è un requisito?)
Ci aspettiamo che se di dovesse aggiornare il formato dell'evento nel topic payments
gestiremo eventi anche in parallelo con versioni distinte, quindi ogni proxy deve Interpretare solo gli eventi che riportano il formato più recente dell'evento nel topic payments: event_version: 2.0
Integrarsi con per la gestione degli errori
Esporre metriche di monitoraggio in , in particolare:
counter sul numero di pagamenti creati
counter sul numero di errori durante il processamento di pagamenti, differenziando gli errori interni (errori di formato dati in ingresso o validazione) da errori esterni (errori di dialogo con l'IdP)
latenza delle chiamate fatte all'IdP
Di seguito la tabella contenente le specifiche di ogni metrica
Inoltre il servizio deve rispettare gli in particolare per quanto riguarda la gestione dello storage: non possiamo avere vendor-lockin sulle tecnologie, quindi dobbiamo essere compatibili con file-system posix tradizionale, NFS share e almeno i due cloud-storage più diffusi S3 e Azure Blob.
EFIL:
IRIS:
MYPAY:
PMPAY:
Questi proxy condividono alcune scelte implementative grazie a un apposito python-sdk sviluppato in parallelo ai proxy stessi:
le configurazioni sono salvate su disco locale o s3 secondo un albero ben definito tenant->servizio
i singoli pagamenti vengono salvati su storage fino a che il pagamento è pendente, così da poter fare il polling in autonomia.
oc_payment_validation_errors_total
cluster, env, app_name
la metrica deve misurare gli errori di validazione sull'evento letto (es. l'importo è una stringa invece che un float)
oc_api_requests_total
cluster, env, method, app_name, status_code
la metrica deve monitorare le chiamate http indicandone lo status code
oc_payment_success_events_total
cluster, env, app_name
la metrica deve misurare solo gli eventi per cui il proxy ha una configurazione e che stati processati con successo
oc_payment_failed_events_total
cluster, env, app_name
la metrica deve misurare gli eventi di pagamento validi di cui però è fallito il processing per qualsiasi motivo (escluso il caso in cui non esiste una configurazione per esso)
oc_payment_provider_errors_total
cluster, env, app_name
la metrica deve misurare gli eventi di pagamento validi di cui però è fallito il processing a causa di un errore sul provider
oc_payment_internal_errors_total
cluster, env, app_name
la metrica deve misurare gli eventi di pagamento validi di cui però è fallito il processing per errori interni al codice
oc_payment_provider_latency_bucket
cluster, env, app_name
istogramma che mostra la distribuzione di latenza delle risposte del provider
La configurazione di tenant e servizi avviene interrogando delle API apposite fornite dal Protocol Proxy di riferimento. Quest'ultimo fornisce inoltre un form come json schema sviluppato con form.io mediante il quale l'utente inserisce tali configurazioni.
Per creare un form utilizzare il seguente builder: https://formio.github.io/formio.js/app/builder
L'admin, dall'interfaccia di configurazione dei protocolli della piattaforma compila la configurazione mediante una form, il cui json schema è servito dall'API /v1/schema
del Protocol Proxy
Lo schema della form sopra riportata è il seguente:
{
"display": "form",
"settings": null,
"components": [
{
"label": "UUID dell'ente",
"placeholder": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"applyMaskOn": "change",
"hidden": true,
"tableView": true,
"validate": {
"required": true
},
"key": "tenant_id",
"type": "textfield",
"input": true
},
{
"label": "UUID del Servizio",
"placeholder": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"applyMaskOn": "change",
"hidden": true,
"tableView": true,
"validate": {
"required": true
},
"key": "id",
"type": "textfield",
"input": true
},
{
"label": "Descrizione del servizio",
"placeholder": "Comune di Bugliano",
"applyMaskOn": "change",
"hidden": true,
"tableView": true,
"validate": {
"required": true
},
"key": "description",
"type": "textfield",
"input": true,
"defaultValue": "test"
},
{
"label": "Impostazioni specifiche del protocollo",
"tableView": false,
"validate": {
"required": false
},
"key": "registry",
"type": "container",
"input": true,
"components": [
{
"label": "URL Protocollazione",
"placeholder": "http://www.example.com/",
"tableView": true,
"validate": {
"required": true
},
"key": "url",
"type": "textfield",
"input": true
},
{
"label": "Gerarchia di Classificazione",
"placeholder": "1.2.3",
"tableView": true,
"validate": {
"required": false
},
"key": "classification_hierarchy",
"type": "textfield",
"input": true
},
{
"label": "Numero Fascicolo",
"placeholder": "1",
"tableView": true,
"validate": {
"required": false
},
"key": "folder_number",
"type": "textfield",
"input": true
},
{
"label": "Anno Fascicolo",
"placeholder": "2022",
"tableView": true,
"validate": {
"required": false
},
"key": "folder_year",
"type": "textfield",
"input": true
}
]
},
{
"label": "Attivo",
"tableView": false,
"validate": {
"required": false
},
"key": "is_active",
"type": "checkbox",
"input": true,
"defaultValue": false
},
{
"label": "Salva",
"tableView": false,
"validate": {
"required": false
},
"key": "submit",
"type": "button",
"input": true,
"disableOnInvalid": true
}
]
}
Premendo poi il bottone Salva, viene eseguita una POST /services
servita dal Protocol Proxy, con payload
{
"data": {
"tenant_id": "",
"id": "",
"description": " ",
"registry": {
"url": "url.protocollo.it",
"classification_hierarchy": "1.2.3",
"folder_number": "1",
"folder_year": "2023"
},
"is_active": true,
"submit": false
},
"metadata": {}
}
Per modificare una configurazione esistente, il proxy serve l'API PUT /services/{service_id}
e PATCH /services/{service_id}
Per eliminare una configurazione esistente, il proxy serve l'API DELETE /services/{service_id}
. In questo caso l'eliminazione è una soft-delete, ovvero la configurazione viene semplicemente disattivata settando il parametro active
a false
.
la configurazione del tenant avviene in maniera nascosta durante la configurazione del servizio. Sarà la piattaforma a chiamare le API del Protocol Proxy. La loro implementazione è descritta nella sezione seguente
Le configurazioni di tenant e servizi vengono salvate con la seguente alberatura
root
|____tenants
| |____tenantid1.json
| |____tenantid2.json
| |____tenantid3.json
| |____.....
| |____tenantidn.json
|____services
| |____serviced1.json
| |____serviced2.json
| |____serviced3.json
| |____.....
| |____servicedn.json
pdnd-connector
Il pdnd-connector
espone endpoint specifici per ciascun e-service integrato, con diverse operazioni supportate tramite verbi HTTP.
Gli endpoint seguono una struttura che identifica l'erogatore e il servizio specifico. Ad esempio, per gli e-service ANPR (ANPR
è l'erogatore), si utilizzano path come /e-services/anpr/<nome-e-service>
. Esempi di <nome-e-service>
includono accertamento-cittadinanza
, stato-famiglia
, accertamento-residenza
.
GET
: Utilizzato per la fruizione degli e-service, ovvero per richiedere e ottenere i dati dal pdnd-connector
, che a sua volta li recupera dagli enti erogatori (es. ANPR, INPS).
POST
: Utilizzato per la validazione dei dati precedentemente ottenuti tramite GET
. Questa operazione verifica che i dati non siano stati alterati prima di essere inviati dal Core.
Parametri Richiesti (Payload per POST
, URL per GET
):
Per GET
(Fruizione): I parametri sono inclusi nella URL (query parameters). Esempi includono:
fiscalCode
: Il codice fiscale dell'utente di cui si richiedono i dati.
format
: Un parametro che specifica il formato in cui i dati devono essere restituiti dal pdnd-connector
. Questo formato deve essere compatibile con il Nested Form di OpenCity che riceverà i dati.
Esempio URL: GET /e-services/anpr/stato-famiglia?fiscalCode=ABCDEF01G23H456I&format=statoFamigliaArchetipo
Per POST
(Validazione): Il payload della richiesta POST
deve contenere due campi principali:
data
: L'oggetto JSON contenente i dati esatti (incluso il loro formato) così come ricevuti dalla risposta GET
precedente, senza i metadati di isFromPdnd
, isReadonly
, isUpdatedToday
.
meta
: Un oggetto contenente la firma digitale (signature
) dei dati, così come restituita dalla risposta GET
.
Header Necessari: Per tutte le chiamate agli endpoint del pdnd-connector
, è necessario includere un token JWT nell'header Authorization
.
Authorization: Bearer <JWT_TOKEN>
. Il pdnd-connector
effettua una validazione del token. Per le API di configurazione (non direttamente usate da OpenCity per fruizione), è richiesto un admin token
. Per le API di fruizione e validazione, è richiesto un user token
, e il codice fiscale contenuto nel token viene verificato con quello eventualmente presente nella URL.
Di seguito, alcuni esempi di richieste e risposte per chiarire il flusso di integrazione.
Richiesta:
Risposta:
È disponibile anche il formato "residenza_archetipo" che include campi aggiuntivi:
Richiesta:
Risposta (esempio con coniuge e figli):
Per validare l'autenticità di una risposta, si utilizza il relativo l'endpoint di validazione:
Richiesta:
Risposta:
Le API fornite nella documentazione dell'intermediario vengono generalmente testate in prima battuta via Postman/Insomnia nel caso di chiamate REST o via SoapUI nel caso di chiamate SOAP
A seguito dei test delle chiamate, si procede con l'implementazione del microservizio. Per testarlo localmente fare riferimento al seguente docker-compose.yml
version: '2'
services:
zookeeper:
image: confluentinc/cp-zookeeper:7.0.0
hostname: zookeeper
ports:
- "2181:2181"
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
kafka:
image: confluentinc/cp-kafka:7.0.0
hostname: kafka
depends_on:
- zookeeper
ports:
- "29092:29092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_CREATE_TOPICS: "applications,payments_cache"
kafka-ui:
image: provectuslabs/kafka-ui:latest
ports:
- "8080:8080"
depends_on:
- kafka
environment:
KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092
# KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181
KAFKA_CLUSTERS_0_KSQLDBSERVER: ksqldb-server:8088
topic-init:
image: lorello/alpine-bash:1.4.0
depends_on:
- kafka
volumes:
- ./init.d:/data
command:
- /bin/bash
- -c
- |
wait-for-it kafka:9092
kaf config add-cluster local -b kafka:9092 && \
kaf config use-cluster local
set -ex
for I in /data/payment*.json; do
echo "Importing $$I"
cat $$I | jq -c | kaf produce payments
done
# la configurazione varia in base al proxy da implementare
iris-proxy:
build:
context: .
ports:
- "8000:8000"
environment:
CANCEL_PAYMENT: "false"
KAFKA_TOPIC_NAME: "payments"
KAFKA_BOOTSTRAP_SERVERS: "kafka:9092"
KAFKA_GROUP_ID: "iris_payment_proxy"
IRIS_OTF_PAYMENT_URL: "https://apistage.regione.toscana.it/C01/ComunicazionePosizioniDebitorieOTF/v3/IdpAllineamentoPendenzeEnteOTF"
IRIS_IUV_URL: "https://iristest.rete.toscana.it/IdpBillerNdpServices/GenerazioneIUVService"
IRIS_OUTCOME_URL: "https://apistage.regione.toscana.it/C01/InvioNotificaPagamentoEsito/v3/IdpInformativaPagamento.Esito"
IRIS_VERIFY_URL: "https://apistage.regione.toscana.it/C01/VerificaStatoPagamento/v3/IdpVerificaStatoPagamento"
IRIS_NOTICE_URL: "https://iristest.rete.toscana.it/IdpBillerNdpServices/GenerazioneAvvisiService"
BASE_PATH_CONFIG: "sdc-payments/iris/tenants/"
BASE_PATH_EVENT: "sdc-payments/iris/payments/"
STORAGE_TYPE: "MINIO"
STORAGE_KEY: "AKIAIOSFODNN7EXAMPLE"
STORAGE_SECRET: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
STORAGE_BUCKET_NAME: "payments"
STORAGE_ENDPOINT: "minio:9000"
depends_on:
- createbuckets
- kafka
# storage dove vengono salvate le configurazioni del tenant, del servizio e lo stato dei vari pagamenti
minio:
image: minio/minio
command: server /data --console-address ":9001"
restart: unless-stopped
volumes:
- ./bucket:/data
ports:
- 9000:9000
- 9001:9001
environment:
MINIO_ACCESS_KEY: AKIAIOSFODNN7EXAMPLE
MINIO_SECRET_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
createbuckets:
image: minio/mc
depends_on:
- minio
entrypoint: >
/bin/sh -c "
while ! /usr/bin/mc config host add minio http://minio:9000 AKIAIOSFODNN7EXAMPLE wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY;
do echo 'Wait minio to startup...' && sleep 0.1; done;
/usr/bin/mc mb minio/payments;
/usr/bin/mc policy set public minio/payments;
exit 0;
"
Se si sta facendo il deploy di un nuovo microservizio per la prima volta o si sta aggiungendo una nuova API o una nuova pagina a una interfaccia esistente, è necessario aggiungere alcuni controlli di qualità minima prima del rilascio.
Test flusso standard
Inserire la configurazione del tenant
Inserire la configurazione del servizio
Inserire un esempio di pagamento in stato CREATION_PENDING
nel topic payments
e verificare che venga correttamente creato il debito
Chiamare l'API /offline-payment/{id}
e verificare che venga scaricato correttamente l'avviso di pagamento cartaceo
Chiamare l'API /online-payment/{id}
e verificare che si venga rediretti al portale di pagamento
Una volta arrivati in fondo al pagamento online, verificare che premendo il bottone "Torna alla Home" venga chiamata correttamente l'API /landing/{id}
Chiamare l'API /update/{id}
e verificare che venga correttamente controllato lo stato del pagamento ed eventualmente aggiornato e prodotto un evento sul topic payments
Chiamare l'API /receipt/{id}
e verificare che, a pagamento avvenuto, venga scaricata correttamente la ricevuta telematica
Inserire un esempio di pagamento in stato PAYMENT_PENDING
che è già stato processato precedentemente dal proxy e verificare che venga ignorato
Inserire un esempio di pagamento in stato PAYMENT_PENDING
che non è già stato processato precedentemente dal proxy e per cui esiste una configurazione sullo storage, e verificare che venga salvato correttamente sullo storage (caso importazione dovuti)
Test flusso di errore
Modificare la configurazione del servizio in modo che sia errata (mettendo ad esempio credenziali errate)
Inserire un esempio di pagamento in stato CREATION_PENDING
nel topic payments e verificare che, a seguito del fallimento, venga prodotto un evento in stato CREATION_FAILED
nel topic payments
I pagamenti sono testabili sul nostro ambiente di qa: https://servizi.comune-qa.bugliano.pi.it/.
Si potrà accedere come utente SPID utilizzando l'utente AGID TEST DEMO.
Si potrà accedere come admin per eventualmente modificare il servizio di test e la relativa configurazione di pagamento
Si potrà accedere come operatore per testare pagamenti posticipati, i quali richiedono la presa in carico della pratica e l'approvazione di quest'ultima affinchè l'utente possa procedere con il pagamento.
GET /anpr/accertamento-residenza
Headers:
Authorization: Bearer <token_jwt>
Query Parameters:
fiscal_code: RSSMRA80A01H501U
config_id: a6fcd036-f1a2-4df5-b986-7410e9e0e97a
format: residenza
{
"data": {
"address": "Via Roma",
"house_number": "123",
"municipality": "Roma",
"county": "RM",
"postal_code": "00100"
},
"meta": {
"signature": "base64_encoded_signature",
"format": "residenza",
"created_at": "2024-03-26T10:00:00Z",
"source": "ANPR",
"call_url": "https://api.example.com/anpr/accertamento-residenza?config_id=a6fcd036-f1a2-4df5-b986-7410e9e0e97a&fiscal_code=RSSMRA80A01H501U&format=residenza"
}
}
{
"data": {
"address": "Via Roma",
"house_number": "123",
"municipality": "Roma",
"county": "RM",
"postal_code": "00100",
"municipality_istat_code": "058091",
"locator_within": "int1"
},
"meta": {
// ... stessi campi meta ...
}
}
GET /anpr/stato-famiglia
Headers:
Authorization: Bearer <token_jwt>
Query Parameters:
fiscal_code: RSSMRA80A01H501U
config_id: c88d7efe-84a1-43c5-9edc-07f18a577289
format: famiglia
{
"data": {
"birth_date": "1980-01-01",
"birth_place": "Roma",
"family_name": "Rossi",
"given_name": "Maria",
"gender": "femmina",
"tax_id": "RSSMRA80A41H501U",
"children": [
{
"birth_date": "2010-02-25",
"birth_place": "Roma",
"family_name": "Rossi",
"given_name": "Marco",
"gender": "maschio",
"tax_id": "RSSMRC10B25H501U"
},
{
"birth_date": "2012-03-15",
"birth_place": "Roma",
"family_name": "Rossi",
"given_name": "Laura",
"gender": "femmina",
"tax_id": "RSSLRA12C55H501U"
}
]
},
"meta": {
"signature": "base64_encoded_signature",
"format": "famiglia",
"created_at": "2024-03-26T10:00:00Z",
"source": "ANPR",
"call_url": "https://api.example.com/anpr/stato-famiglia?config_id=c88d7efe-84a1-43c5-9edc-07f18a577289&fiscal_code=RSSMRA80A01H501U&format=famiglia"
}
}
POST /validate/anpr/accertamento-residenza
Headers:
Authorization: Bearer <token_jwt>
Query Parameters:
fiscal_code: RSSMRA80A01H501U
config_id: a6fcd036-f1a2-4df5-b986-7410e9e0e97a
format: residenza
Body:
{
"data": {
// ... dati ricevuti ...
},
"meta": {
// ... meta ricevuti con la firma ...
}
}
{
"result": true // o false se la validazione fallisce
}
Il Protocol Proxy interagisce con la piattaforma in due fasi distinte ma complementari:
Abilitazione e configurazione del sistema di protocollazione
Protocollazione dei documenti
In questa fase l'amministratore della piattaforma configura un servizio, decidendo quale sistema di protocollazione abilitare per la protocollazione dei relativi servizi.
Cliccando sul bottone "abilita" la piattaforma contatta il Protocol proxy relativo tramite chiamate REST API. Con la prima chiamata, la piattaforma richiede la configurazione del form schema che verrà presentato all'Amministratore. Attraverso tale form è possibile inserire le configurazioni relative al tenant e al servizio corrente. Tali configurazioni verranno inviate via REST HTTPS al Protocol Proxy che si occuperà della persistenza di tali configurazioni.
Una volta che tale processo si è concluso, il Protocol Proxy è in grado, sfruttando tali configurazioni, di interagire con il sistema di protocollazione esterno.
Le configurazioni di protocollazione possono essere create, modificate e disabilitate. Per questo motivo il Protocol Proxy deve esporre chiamate API CRUD (dettagliate nella pagina seguente).
Prima di eseguire qualsiasi operazione, il protocollo proxy deve verificare la versione dell'evento (event_version
) ed elaborare esclusivamente la versione 1, per la quale è abilitato. La versione dell'evento da considerare deve essere un valore fissato nel codice. Tutti gli eventi con event_version
diversa da 1 devono essere scartati.
In questo diagramma, viene descritto il workflow di una pratica (ma il processo è uguale per qualsiasi altro tipo di richiesta) presentata da un utente e presa in carico da un operatore(le altre casistiche descritte nella pagina "Architettura del sistema di protocollazione" seguono lo stesso processo).
Una volta che la pratica è stata presa in carico dell'operatore essa deve essere protocollata. La piattaforma produce un evento di tipo documento sul topic "documents". Il Protocol Proxy, consuma tale evento:
Per poter protocollare correttamente il documento, è fondamentale verificare l'esistenza delle configurazioni relative al tenant e al servizio pertinenti. Questa operazione può essere eseguita attraverso la verifica del campo document.RemoteCollection.ID
, che identifica il servizio, e document.tenantId
, che identifical'ID del tenant. Questi campi dovrebbero essere confrontati con gli ID delle configurazioni corrispondenti precedentemente salvate nello storage. Nel caso in cui tali configurazioni non siano disponibili, il documento dovrà essere semplicemente ignorato, procedendo all'evento successivo.
Una volta assicuratosi di dover protocollare il documento in esame, il Protocol Proxy deve verificare che il documento non sia già stato protocollato. Un evento è protocollato quando l'oggetto "document_number" è valorizzato con almeno il numero di protocollo. Nel caso il documento sia già stato protocollato, l'evento va semplicemente ignorato.
Una volta verificato che il documento sia da protocollare, è necessario modificare il campo status da DOCUMENT_CREATED
o REGISTRATION_FAILED
o PARTIAL_REGISTRATION
a REGISTRATION_PENDING
, poi si produce l'evento sul topic documents
Una volta che si rilegge lo stesso evento prodotto allo step precedente, e una volta superate le precedenti verifiche, Protocol Proxy può interagire con il sistema di protocollazione esterno per la protocollazione del documento.
Se la protocollazione va a buon fine, il documento verrà modificato con i dati di protocollazione (vedi sezione successive), verrà inoltre sovrascritto il campo stato settandolo a REGISTRATION_COMPLETE
, infine, sarà necessario aggiornare anche il campo updated_at
con il timestamp corrente (time.now()
) in formato ISO 8601
(es: 2023-11-10T10:54:44+00:00
).
Il Protocol Proxy produce un nuovo evento di tipo document sul topic documents con in aggiunta le informazioni di protocollazione (valorizzando l'oggetto registration_data
).
A questo punto la piattaforma, in ascolto sullo stesso topic, provvederà ad aggiornare il suo stato interno.
Quando viene generato un evento, il protocollo proxy deve assegnare i seguenti valori:
app_id
: <nome del servizio>:<versione corrente del servizio>
. Es: protocol-proxy-fake:1.1.1
event_created_at
: es: 2023-11-23T14:14:23+00:00
event_id
: stringa UUID autogenerata che identifica univocamente l'evento
In un sistema a eventi che utilizza il pattern event sourcing, è possibile che alcuni eventi non vengano processati correttamente a causa di errori temporanei come per esempio una comunicazione interrotta tra il Protocol Proxy e il sistema di protocollazione esterno. In questo caso basterebbe tentare nuovamente il consumo dello stesso evento più volte finché il problema non viene risolto.
La piattaforma offre questo tipo di possibilità attraverso l'implementazione di un meccanismo di retry che legge da un topic (retry_queue) specifico tutti gli eventi falliti e li reinserisce nei vari topic di origine in base a una politica di retry scelta a monte.
Il servizio che si occupa di questo meccanismo è in grado di gestire eventi eterogenei provenienti da diversi topic. Questo è possibile solo se il servizio è a conoscenza del topic di origine in cui reinserire gli eventi da processare nuovamente.
Per questo motivo, qualsiasi servizio si voglia avvalere del meccanismo di retry, deve modificare l'evento fallito inserendo il topic in cui si vuole che sia reinserito e poi produrre l'evento nel topic di retry impostato tramite variabile d'ambiente.
I metadati da inserire nell'evento sono nella seguente forma:
{
"retry_meta": {
"original_topic": "documents"
}
}
Quando l'oggetto sarà reinserito nel topic "document" dal Retry Orchestrator, l'oggetto "retry_meta" avrà dei campi aggiuntivi utili per attuare la politica di retry. Tali campi, sono gestiti e utili unicamente al Retry Orchestrator. Il Protocol Proxy non si deve quindi preoccupare di tale oggetto una volta inserito l'oggetto "retry_meta" con il campo "original_topic" valorizzato una tantum.
Nel caso in cui non si riesca a protocollare la pratica per un errore da parte del provider per esempio, o nel caso in cui nessuno delle chiamate al protocollo vadano a buon fine, è necessario sovrascrivere il campo status
a REGISTRATION_FAILED
.
Nel caso in cui la protocollazione richieda diversi step (come per esempio protocollazione del documento principale e a seguito la protocollazione dei vari allegati) e questa venga interrotta a metà, l'evento dovrà essere inserito nella coda di retry assime alle informazioni relative allo stato corrente in modo da riprendere la protocollazione da dove è stata interrotta. Specificamente il documento va messo nello stato PARTIAL_REGISTRATION
.
In tale circostanza, è possibile inserire liberamente tutti i campi rilevanti per il monitoraggio dello stato all'interno dell'oggetto retry_meta
. Questa flessibilità consente di adattare i dati alle specifiche esigenze, garantendo un controllo accurato del processo di protocollazione e prevenendo duplicazioni delle operazioni.
Solitamente la protocollazione di un documento digitale è accompagnata anche dalla protocollazione dei relativi allegati(menzionati nel paragrafo ) e dalla fascicolazione degli stessi. Dal punto di vista del Protocol Proxy, la protocollazione del documento, di tutti i suoi allegati e la fascicolazione è considerata come un unica operazione atomica. Questo implica che il processo di protocollazione è considerato completo solo quando tutte le chiamate relative al documento principale, agli eventuali allegati e alla eventuale fascicolazione si concludono con successo. Se anche solo una chiamata dovesse fallire, questo costituirà una condizione sufficiente per interrompere il processo di protocollazione e avviare il meccanismo di retry descritto nel paragrafo precedente.
Il documento può avere i seguenti stati:
DOCUMENT_CREATED
: stato iniziale in cui si trova il documento quando viene prodotto dal document dispatcher
REGISTRATION_PENDING
: stato in cui viene prodotto il documento dal protocol proxy non appena viene trovato in stato DOCUMENT_CREATED
, PARTIAL_REGISTRATION
o REGISTRATION_FAILED
e presenta una configurazione attiva per esso
REGISTRATION_COMPLETE
: stato in cui viene prodotto il documento dal protocol proxy dopo essere stato protocollato correttamente
PARTIAL_REGISTRATION
: stato in cui viene prodotto il documento dal protocol proxy nel caso in cui si ha molteplici chiamate da fare e una di questa fallisce (per esempio non vengono caricati tutti gli allegati)
REGISTRATION_FAILED
: stato in cui viene prodotto il documento dal protocol proxy nel caso in cui la protocollazione fallisce totalmente
Di seguito la rappresentazione grafica
La Fascicolazione di un documento rappresenta l'attribuzione dello stesso ad una unità archivistica – “il fascicolo” - che raggruppa un insieme di documenti appartenenti al medesimo procedimento. Non tutti i sistemi di protocollazione offrono questa funzionalità. In questo caso la valorizzazione del campo "folder_number" deve essere discussa e definita a seconda dei casi.
Durante la fase di protocollazione, è possibile che il Protocol Proxy debba utilizzare le API della piattaforma per ottenere le informazioni necessarie per eseguire il processo di protocollazione. Un esempio pratico di ciò riguarda la protocollazione degli allegati. Il sistema di protocollazione richiede che il base64 del file allegato sia incluso nel payload della chiamata. Tuttavia, nel documento digitale sono presenti solo i link per scaricare i file. Pertanto, sarà compito del Protocol Proxy autenticarsi sulla piattaforma per recuperare il Token JWS e successivamente chiamare l'API specifica per ottenere il file necessario da cui calcolare il Base64.
Il token può essere recuperato con la seguente chiamata:
Username : admin
Password: admin
Basepath: estratto dal campo "Url" di ogni allegato
Endpoint : /lang/api/auth
method: POST
header Content-Type: application/json
payload : { "username": "admin", "password": "admin" }
Response: 200 -> {"token": "eyDIJeiojf...."}
Response: 401 -> { "code": 401, "message": "Credenziali non valide." }
Response 400 -> HTML body
Le API della piattaforma sono documentate al seguente link:
https://servizi.comune-qa.bugliano.pi.it/lang/api/doc
Calcolo del basepath per recuperare il token
L'url per API auth deve essere estratta da campo url
di ogni attachment:
quindi dal seguente documento avremo una post auth all'endpoint https://servizi.comune-di-vicenza.it/lang/api/auth
con credenziali fornite tramite variabili d'ambiente(utente e password). Mentre nel secondo caso avremo una post auth all'endpoint https://servizi.comune-di-bugliano.it/lang/api/auth
con credenziali fornite tramite variabili d'ambiente(utente e password).
NOTA BENE: nel seguente esempio abbiamo un array di attachments in cui due url appartengono a due comuni diversi. Questo nel mondo reale non può succedere. Il base url è lo stesso per tutto il documento(cioè un documento appartiene ad un solo comune). Sono stati utilizzati due comuni diversi allo scopo di sottolineare la parte di url che cambia nella costruzione dell'endpoint della API di autenticazione.
{
"attachments": [
{
"name": "dummy.pdf",
"description": "Allegato - dummy.pdf",
"mime_type": "application/pdf",
"url": "https://servizi.comune-di-vicenza.it/lang/api/applications/1a30529e-9c41-4352-9198-1fac44c5e911/attachments/a4c81464-ee91-449a-b2a3-7010c59fdee7?version=1",
"md5": "",
"filename": "dummy.pdf"
},
{
"name": "dummy2.pdf",
"description": "Allegato - dummy2.pdf",
"mime_type": "application/pdf",
"url": "https://servizi.comune-di-bugliano.it/lang/api/applications/1a30529e-9c41-4352-9198-1fac44c5e911/attachments/d284be8a-2700-48a9-aa4b-fc0136674548?version=1",
"md5": "",
"filename": "dummy2.pdf"
}
],
}
Tutti i microservizi che compongono la piattaforma devo rispettare i seguenti standard e regole generali
Tutti i servizi devono rispettare i 12factor, descritti in modo anche più dettagliato nell'articolo An illustrated guide to 12 Factor Apps
Ogni microservizio deve poter essere eseguito facilmente in locale, anche da chi non conosce a fondo il progetto. L’obiettivo del packaging è:
facilitare l’avvio dell’applicativo;
semplificare lo sviluppo e il debugging;
rendere accessibile un ambiente funzionante con un semplice docker-compose up
.
Nella root del repository devono essere presenti i seguenti file:
Dockerfile
Build dell’immagine base del servizio
docker-compose.yml
Definisce i container minimi per l’esecuzione in produzione o ambienti CI
docker-compose.dev.yml
Contiene configurazioni utili per lo sviluppo locale (es. build
, mount volumi, strumenti debug)
.dockerignore
(opzionale) Esclude file e cartelle non rilevanti dalla build
Il file docker-compose.yml
deve essere autosufficiente: chi lo scarica deve poter eseguire il servizio senza dover clonare l’intero repository.
Le immagini Docker utilizzate devono essere:
disponibili su un registry pubblico (es. DockerHub, GitHub Container Registry ecc.);
oppure facilmente costruibili in locale tramite override.
⚠️ Non includere la chiave
build:
neldocker-compose.yml
principale, per evitare conflitti nei contesti di esecuzione remota (es. CI/CD).
Per lo sviluppo in locale:
Copia o rinomina il file docker-compose.dev.yml
in docker-compose.override.yml
Esegui:
docker-compose up
Docker gestirà automaticamente il merge tra docker-compose.yml
e docker-compose.override.yml
(vedi: Docker Docs – Multiple Compose Files).
✅ In questo modo, chiunque può avviare l’ambiente di sviluppo senza modificare i file originali.
docker-compose.yml
(semplificato)
version: "3.9"
services:
myservice:
image: registry.gitlab.com/opencity-labs/myservice:1.2.3
ports:
- "8080:8080"
docker-compose.dev.yml
version: "3.9"
services:
myservice:
build:
context: .
dockerfile: Dockerfile
volumes:
- ./:/app
environment:
- ENVIRONMENT=local
Dopo aver rinominato docker-compose.dev.yml
in docker-compose.override.yml
, il comando:
docker-compose up
...utilizzerà in automatico l’immagine build
locale con i volumi montati per lo sviluppo.
Ogni microservizio deve essere in grado di gestire correttamente la terminazione, in modo da:
garantire la consistenza dei dati;
evitare la perdita di eventi o operazioni incomplete;
chiudere le risorse in uso in maniera sicura (DB, Kafka, file, socket...).
La gestione corretta della terminazione è essenziale sia in ambiente Docker che in Kubernetes, dove il segnale di terminazione (SIGTERM
) viene inviato prima di un restart o di un downscaling.
SIGTERM
Richiesta di terminazione gentile
Il servizio avvia lo shutdown controllato
SIGKILL
Terminazione forzata e immediata
Non gestibile dal codice; da evitare
✅ Il servizio DEVE catturare
SIGTERM
e avviare una sequenza di terminazione controllata.
Alla ricezione di SIGTERM
, il servizio deve:
smettere di accettare nuove richieste o eventi;
completare le operazioni critiche in corso;
liberare risorse (connessioni, file, lock...);
loggare la terminazione a livello INFO
;
uscire con exit code 0 entro un tempo ragionevole.
Cattura SIGTERM
Il servizio intercetta correttamente il segnale SIGTERM
☐
Terminazione controllata
Avvia una sequenza di chiusura ordinata
☐
Timeout ragionevole
Si chiude in max 10s (o valore configurato)
☐
Operazioni critiche completate
Nessuna perdita o corruzione dati
☐
Chiusura delle risorse
Socket, consumer, file, connessioni DB...
☐
Log di terminazione
Esempio: "SIGTERM ricevuto, shutdown in corso..."
☐
Nessuna nuova richiesta
I listener vengono disattivati
☐
Exit code 0
Il processo si chiude correttamente
☐
uvicorn + asyncio
import asyncio
import signal
import uvicorn
from myservice import app
APP_HOST = "0.0.0.0"
APP_PORT = 8080
def run_server():
"""Esegue il server intercettando SIGINT e SIGTERM per uno shutdown pulito"""
config = uvicorn.Config(
app,
host=APP_HOST,
port=int(APP_PORT),
proxy_headers=True,
forwarded_allow_ips='*',
access_log=False,
)
server = uvicorn.Server(config)
loop = asyncio.get_event_loop()
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, lambda: asyncio.create_task(server.shutdown()))
try:
loop.run_until_complete(server.serve())
except asyncio.CancelledError:
pass # Evita traceback in console
if __name__ == '__main__':
run_server()
signal.NotifyContext
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
log.Println("Server in avvio su :8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Errore durante l'avvio del server: %v", err)
}
}()
<-ctx.Done()
log.Println("SIGTERM ricevuto, avvio terminazione...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Fatalf("Errore durante la terminazione: %v", err)
}
log.Println("Terminazione completata.")
}
Usare questo script per validare la terminazione pulita in locale o integrarlo nella CI.
#!/bin/bash
set -euo pipefail
SERVICE_NAME="test_shutdown_service"
IMAGE_NAME="your-image-name"
LOG_FILE="shutdown_test.log"
echo "▶️ Avvio container..."
docker run --rm --name "$SERVICE_NAME" -d "$IMAGE_NAME" > /dev/null
sleep 2 # attesa minima per stabilità
echo "🛑 Invio SIGTERM..."
docker kill --signal=SIGTERM "$SERVICE_NAME"
sleep 2
echo "🔍 Verifica terminazione..."
if docker ps -a --format '{{.Names}}' | grep -q "$SERVICE_NAME"; then
echo "❌ Il container è ancora in esecuzione"
exit 1
else
echo "✅ Terminazione completata correttamente"
fi
Se si usa docker-compose
, si può integrare il test nel CI lanciando:
docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d
docker-compose kill -s SIGTERM
La configurazione dei microservizi deve avvenire:
principalmente tramite variabili d’ambiente;
opzionalmente tramite:
file .env
(per ambienti locali);
parametri CLI (per job schedulati o debug interattivo).
Variabili d’ambiente
Default per tutti i deployment (prod/dev/test)
.env
file
Sviluppo locale o test manuali
CLI parametri
Script cron, job schedulati, test/debug
❗ Le configurazioni non devono essere hardcoded nel codice. Utilizzare sempre default sicuri e sovrascrivibili.
Permettere al sistema di orchestrazione (Docker/Kubernetes) di verificare che il servizio sia vivo e funzionante.
Deve esporre un endpoint /status
:
200 OK
se tutto è funzionante;
codice diverso in caso di errore.
Se il servizio non è HTTP, l’healthcheck può essere:
la presenza di un file di stato;
la verifica di un processo in esecuzione.
✅ L’healthcheck DEVE essere incluso nel
Dockerfile
.
Tutti i microservizi devono implementare un sistema di log coerente, strutturato e facilmente aggregabile.
Riferimento normativo: Allegato 4 delle Linee Guida di Interoperabilità AgID.
I log devono supportare almeno i livelli: DEBUG
, INFO
, ERROR
. Una gestione completa prevede sei livelli (vedi sotto).
Ogni log di errore o anomalia deve essere su una singola riga, facilmente parsabile (plaintext key=value
o JSON).
Evitare log verbosi e non strutturati: ostacolano il monitoraggio e la correlazione cross-microservizio.
⚠️ Non mischiare log di tipo HTTP e stacktrace multilinea.
Scrivere i log a singola riga su
stdout
.Scrivere stacktrace (se necessario) separatamente su
stderr
.
Preferibile l’uso di log in formato JSON puro, ma solo se interamente strutturato.
È vietato:
Mischiare testo libero e JSON nello stesso log.
Includere payload JSON grezzi come stringa in un campo JSON (es. loggare interamente l’evento Kafka).
level
✅
Definisce il tipo di log (INFO, ERROR, DEBUG, CRITICAL, WARNING, ecc.)
timestamp
✅
In UTC, formato , con T
e Z
maiuscoli
environment
✅
Definisce l'ambiente in cui il microservizio è in esecuzione
event_id
⚠️
Identificatore univoco dell’evento (se applicabile)
event_type
✅
Tipo di evento o operazione ricevuta
call_type
⚠️
Tipo chiamata: HTTP
, CLI
, EVENT
topic
⚠️
Kafka topic da cui è stato letto l’evento (se applicabile)
log_message
✅
Messaggio del log (errore o info)
client_ip
⚠️
IP della chiamata in ingresso. Usare X-Forwarded-For
quando presente
tenant
✅
identificativo univoco dell'ente a partire dal quale è stato generato il log
provider
, application
, service
✅
Contesto operativo (se disponibili)
user_id
❌
Se presente, deve essere un riferimento anonimo (es. ID utente). Mai dati personali in chiaro
All’avvio, ogni servizio DEVE loggare la versione in esecuzione e l’ambiente (es. local
, boat-qa
, boat-prod
).
CRITICAL
Fallimento grave, con perdita di dati o che comporta lo l'uscita dal flusso di esecuzione (shutdown) del servizio per impossibilità a proseguire o perché è più safe non proseguire.
No
ERROR
Anomalia che non è possibile gestire e che avrà effetti sul risultato. A ERROR, il rate dei log che si alza dovrebbe essere indice che ci sono problemi per i quali è importante attrarre l'attenzione degli amministratori. Attenzione a non confondere errori che si verificano su sistemi esterni e che non sono nostri errori. Date sempre per scontato che una riga di errore prodotta dovrebbe sempre essere letta da qualcuno, altrimenti meglio esporre un livello di warning o non loggare proprio. Una anomalia dovrebbe sempre dare origine a una e una sola riga di log.
Si
WARNING
Anomalia che non avrebbe dobuto presentarsi ma che è stata gestita correttamente per non comportare errori nel nostro servizio. Ad esempio ho ricevuto un evento con una versione negativa, ma l'ho ignorato. Oppure non sono riuscito a collegarmi al db la prima volta, ma solo dopo 2 tentativi.
Si
INFO
Comunica un cambiamento di stato del servizio. Un evento significativo produce una riga di log per ogni evento o chiamata ricevuta dall'applicativo. Se una transazione o un evento gestito presenta errori NON si deve produrre la riga di INFO e la riga di ERROR, ma solo la seconda.
Si
DEBUG
Questo è il livello in cui comunicare informazioni diagnostiche, non necessarie se non si sta investigando un errore specifico. Devono essere informazioni utili a comprendere errori, non semplicemente a comprendere il flusso interno del software, per il quale esiste il livello apposito (TRACE). Una informazione utile è dare tutto il contesto che permette di comprendere perché il servizio si sta comportando in un certo modo.
No
TRACE
Questo livello è inteso per tracciare il flusso interno del servizio, in modo molto dettagliato. Per esempio si può inserire un log a questo livello per capire in quale ramo del codice siamo finiti con l'esecuzione, oppure si può inserire una riga all'inizio e una alla fine di certe porzioni di codice rilevanti.
No
✅ Ogni anomalia deve generare una e una sola riga di log a INFO, i dettagli vanno a DEBUG.
JSON strutturato
{
"level": "ERROR",
"timestamp": "2025-04-18T10:27:52Z",
"environment": "boat-prod",
"event_id": "abcd-1234",
"event_type": "PaymentRequestReceived",
"topic": "pagopa-incoming-events",
"version": "1.3.2",
"environment": "boat-prod",
"tenant": "9bee12a4-...",
"application": "50b4f598-...",
"service": "0a28427f-...",
"provider": "sicraweb-wsprotocollodm",
"client_ip": "10.12.3.1",
"user_id": "user_92348",
"status": "error",
"message": "Impossibile trovare dati di logon validi per l'utente"
}
Plaintext key=value
ERROR 2025-04-18T10:27:52Z event_id=abcd-1234 tracker=252603771125040 provider=sicraweb-wsprotocollodm tenant=9bee12a4 application=50b4f598 service=0a28427f topic=pagopa-incoming-events microservice=payment-handler version=1.3.2 environment=boat-prod client_ip=10.12.3.1 user_id=user_92348 error="Impossibile trovare dati di logon validi per l'utente; nested exception: Connection aborted (Connection reset by peer)"
ERROR
Usare questa checklist in review e PR:
Raccogliere centralmente gli errori per individuare rapidamente problemi in produzione.
Ogni microservizio DEVE integrare Sentry.
Una volta configurato:
abilitare l’integrazione con Sentry;
definire regole di alert personalizzate, se quelle standard non bastano.
📬 Usare
SENTRY_DSN
come variabile d’ambiente per collegare il servizio a Sentry.
Esportare metriche di stato e performance per il monitoraggio continuo.
Se HTTP, il microservizio DEVE esporre un endpoint /metrics
in formato Prometheus.
Error counters
✅
Numero errori critici/intermittenti
Response histograms
✅
Tempi di risposta di servizi esterni
Status code metrics
⚠️
Solo se utili (es. 500 frequenti) – altrimenti usiamo Traefik
404 o bot traffic
❌
NON deve essere esposto come metrica – produce rumore inutile
Non reinventare un sistema di schedulazione dentro l’applicazione. Usare invece l’orchestratore.
NON implementare schedulatori interni (es. cron manuale, loop time-based).
Il servizio deve essere idempotente: rieseguibile sugli stessi dati senza effetti collaterali.
Implementare task cron come comandi CLI, eseguibili con la stessa immagine Docker:
docker run your-image-name python cron_jobs/protocol_batch.py
Il job verrà poi schedulato tramite clustered cron (es. crazy-max/cronjob
).
Niente overengineering: Docker impone già un mapping tra esterno e interno.
Path interni
Usa path fissi tipo /data
, senza preoccuparsi del mapping esterno
Configs
Usare per file di configurazione
Secrets
Usare per credenziali e dati sensibili
NO volumes
Evitare l’uso di volume mapping per la configurazione
✅ Questo approccio garantisce sicurezza, semplicità e prevedibilità.
Standardizzare la CI tra i progetti, garantendo efficienza, velocità e qualità.
Ogni progetto DEVE includere la CI condivisa:
stages:
- build
- test
- deploy
- review
- dast
- staging
- canary
- production
- incremental rollout 10%
- incremental rollout 25%
- incremental rollout 50%
- incremental rollout 100%
- performance
- cleanup
include:
- project: 'opencity-labs/product'
ref: main
file: '.gitlab/ci/devops.yml'
Eventuali step personalizzati (es. test, lint, coverage) vanno inseriti dopo lo include
.
Devono riutilizzare la build già fatta tramite artifact, evitando rebuild inutili.
Durata massima della CI: 15 minuti
Durata consigliata: ≤ 5 minuti
⚡ Ottimizzare usando:
build multi-stage Docker;
test selettivi.
publiccode.yml
Per progetti pubblicati su Developers Italia:
publiccode:
stage: test
allow_failure: false
image:
name: italia/publiccode-parser-go
entrypoint: [""]
script:
- publiccode-parser /dev/stdin < publiccode.yml
only:
- master
Questo documento descrive gli aspetti tecnici dell'importazione delle Pratiche Lite, uno strumento che permette di integrare le pratiche provenienti da altri gestionali.
La nostra piattaforma permette al cittadino di interagire sia con le pratiche generate sulla piattaforma stessa sia con pratiche generate su altri gestionali. Cittadini e Operatori potranno interagire da un'unica interfaccia con un elenco uniforme di pratiche.
Per realizzare questa integrazione è stato ideato il concetto di Pratiche Lite, che rappresenta una pratica con un set minimo di dati, utili per permettere al Cittadino di visualizzarle e interagire con esse in maniera uniforme a quanto accade con le Pratiche generate direttamente nel nostro sistema. Ma non solo, è possibile visualizzare anche informazioni relative ai documenti correlati alla pratica e i pagamenti.
Sarà necessario fornirci un elenco di Pratiche Lite per esporle sulla nostra piattaforma. Questi dati potranno essere forniti tramite API ReST in formato JSON.
Integrare le pratiche generare da altri gestionali nell'area personale permette al cittadino di:
consultare in un unico posto tutte le pratiche relative a un cittadino, anche se generate da altri sistemi, come previsto dal modello del sito comunale del dipartimento;
seguire l'iter della pratica;
consultare i dati essenziali di una pratica, come il richiedente e il beneficiario;
consultare i documenti associati alla pratica e lo stato della protocollazione;
consultare i pagamenti associati alla pratica;
Ma l'area personale non è solo consultazione, il cittadino, se l'integrazione lo prevede, potrà comunque interagire con le pratiche, anche se generate da sistemi esterni. Operazioni come la visualizzazione completa della pratica, la modifica, l'integrazione dei documenti o l'annullamento della richiesta, saranno eseguite direttamente sul gestionale esterno, invocando gli opportuni endpoint HTTP corrispondenti alle azioni da eseguire. Questi endpoint verranno specificati pratica per pratica all'atto dell'importazione.
Per permettere un'integrazione basilare è necessario inviare solo:
titolo e stato della pratica
nome, cognome e codice fiscale del richiedente
un link per raggiungere la pagina di dettaglio della pratica
Ma è comunque possibile aggiungere ulteriori dati per rendere l'esperienza utente più completa:
Pratica: titolo e stato della pratica
Richiedente: nome, cognome, codice fiscale e contatti del richiedente
Beneficiario : nome, cognome, codice fiscale e contatti del beneficiario
Servizio: servizio collegato alla pratica
Endpoint delle azioni: endpoint http delle azioni eseguibili sulla pratica
Documenti: documenti collegati alla pratica e stato di protocollazione
Pagamenti: pagamenti collegati alla pratica
Nel capitolo Flusso di import delle Applications Lite vengono riportati i tracciati dettagliati per una corretta importazione.
Il seguente diagramma mostra i possibili stati in cui può trovarsi una pratica e un possibile flusso di gestione. Come specificato più avanti, le transizioni da uno stato all'altro sono solo indicative, il controllo sui cambi di stato resta al gestionale eterno.
Bozza: in questo stato la pratica non è considerata ancora completamente compilata e può essere modificabile
Inviata: la pratica è stata compilata in ogni sua parte da cittadino e definitivamente inviata. In questo stato non è più possibile modificare i dati delle pratica
Presa in carico: la pratica è stata presa in carico da un Operatore, il quale effettuerà le normali operazioni d'ufficio necessarie a gestirla
In attesa di integrazione: l'operatore ha richiesto di integrare la pratica con informazioni e/o documenti mancanti. La pratica resta in questo stato fino a quando non si ottiene una risposta dal Cittadino
Rifiutata: la pratica non è stata accettata dall'operatore che l'ha gestita
Ritirata: la pratica è stata ritirata dal cittadino prima che l'ufficio si sia pronunciata
Annullata: quando l'ufficio definisce che non si procederà ulteriormente con la pratica e non si pronuncerà su ulteriore approvazione o rifiuto
L'invio dei dati al nostro sistema avviene in modalità push tramite API: è possibile inviarci tramite apposita API REST i dati delle pratiche per inserire nuove pratiche o per aggiornarne lo stato. Questa modalità di aggiornamento è utile per mantenere costantemente aggiornati i dati delle pratiche tra due sistemi, avendo quindi modifiche immediate per il cittadino.
È comunque possibile valutare altre modalità di integrazione, per maggiori dettagli visiona il capitolo Progetti di integrazione ad-hoc con area personale
Il dialogo tra l'Area personale e il gestionale esterno è bidirezionale: il gestionale importa costantemente sull'area personale i dati aggiornati; il Cittadino, agendo su una Application Lite dall'Area personale, atterra su una pagina del gestionale. I due casi prevedono due autenticazioni differenti.
Il primo caso rappresenta una comunicazione server-to-server che avviene mediante token JWT. Verranno fornite delle credenziali di autenticazione che permetteranno di ottenere un token; il token verrà utilizzato per effettuare le richieste successive. È possibile seguire questa guida per le istruzioni di autenticazione https://docs.opencityitalia.it/developers/integrazioni/integrazioni-con-il-flusso-delle-pratiche/api-rest
Il secondo caso è più complesso del primo perché bisogna garantire che l'utente effettui un solo login anche qualora passi dall'area personale al gestionale. Per questo mettiamo a disposizione un server OAuth2 che permette al Cittadino di avere una sessione attiva su tutti i client autorizzati.
Questa è la rotta per effettuar il singolo import di Pratica, Documenti e Pagamenti
POST /api/application-lite/import
Content-Type: application/json
Authorization: Bearer {{auth_token}}
{ vedi tracciato di import }
Come è possibile vedere dal tracciato riportato nel paragrafo successivo, è possibile inviare in un solo colpo i dati relativi a pratiche, documenti e pagamenti. Questi tre elementi si considerano correlati tra di loro, quindi, documenti e pagamenti, si intendono relativi alla pratica inviata e verranno relazionati a essa.
Le pratiche verranno identificate in maniera univoca dal campo external_id che rappresenta un identificativo del sistema sorgente, se non presente, verrà creata con i dati passati.
La riconciliazione di richiedente e beneficiario (applicant e beneficiary) verrà effettuata basandosi sul codice fiscale (tax_code). Se nel sistema esiste già un utente con quel codice fiscale, verranno utilizzati i dati già presente nel database ignorando i dati di nome, cognome e contatti che vengono passati. Al contrario, se non presenti, verrà importato un nuovo utente con i dati specificati.
Lo stato della pratica segue il flusso definito nei capitoli precedenti, tuttavia non è stato implementato un controllo stringente sul flusso della pratica, in controllo dello stato della pratica è demandato al sistema sorgente.
Lo stato della pratica regola però le azioni che il cittadino potrà effettuare. Nella sezione links è possibile specificare le azioni permesse sulla pratica è l'URL da invocare al click del cittadino. Se una specifica azione non è specificata non verrà proposta al cittadino. Le azioni verranno proposte al cittadino anche in base allo stato della pratica, per esempio, una pratica in stato inviata non può più essere modificata, quindi, anche se indicata l'azione MODIFY, non verrà comunque proposta.
{
"application": {
"external_id": "EXT123456",
"subject": "Richiesta Contributo Startup",
"applicant": {
"given_name": "Mario",
"family_name": "Rossi",
"identifiers": {
"tax_code": "RSSMRA85M01H501Z"
},
"contacts": {
"mobile": "+393331234567",
"email": "[email protected]",
"pec": "[email protected]"
}
},
"beneficiary": {
"given_name": "Luca",
"family_name": "Bianchi",
"identifiers": {
"tax_code": "BNCGLC85M01H501Z"
},
"contacts": {
"phone": "+390612345678",
"email": "[email protected]"
}
},
"status": SUBMITTED,
"service": {
"id": "123e4567-e89b-12d3-a456-426614174000"
},
"actions": [
{
"action": "view",
"url": "https://sitoweb/other-path?do=view"
},
{
"action": "request_integration",
"url": "https://sitoweb/other-path?do=request_integration"
}
],
"process_history": [
{
"status": "DRAFT",
"status_changed_at": "2025-05-19T10:22:54+02:00"
},
{
"status": "SUBMITTED",
"status_changed_at": "2025-05-19T10:28:14+02:00"
}
]
},
"documents": [
{
"external_id": "00000000-1111-2222-3333-0334ecfb527d",
"title": "Richiesta: REALIZZAZIONE TETTOIA DEPOSITO-GARAGE, PERMESSO DI COSTRUIRE",
"description": "REALIZZAZIONE TETTOIA DEPOSITO-GARAGE, PERMESSO DI COSTRUIRE",
"short_description": "REALIZZAZIONE TETTOIA DEPOSITO-GARAGE, PERMESSO DI COSTRUIRE",
"folder": "Comunicazione Inizio Lavori-Saverio MMMSSS98D29H123T",
"has_organization": null,
"main_document": "https://localhost/comune-di-bugliano/lang/api/applications/00000000-1111-2222-3333-b167c5038be3/attachments/00000000-1111-2222-3333-50413d60ff4e?version=1",
"registration_data": {
"date": "2024-10-04T14:35:15+02:00",
"document_number": "298O/520/25"
},
"type": "application-request",
"valid_from": null,
"valid_to": null
}
]
}
Questo è lo JSON Schema della richiesta
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ApplicationLite import request",
"type": "object",
"additionalProperties": false,
"properties": {
"application": {
"type": "object",
"properties": {
"external_id": {
"type": "string",
"description": "Identificativo pratica sul sistema esterno",
"minLength": 1
},
"subject": {
"type": "string",
"description": "Oggetto della pratica",
"minLength": 3,
"maxLength": 255
},
"applicant": {
"$ref": "#/definitions/person",
"description": "Richiedente della pratica"
},
"beneficiary": {
"anyOf": [
{ "type": "null" },
{ "$ref": "#/definitions/person" }
],
"description": "Beneficiario della pratica"
},
"status": {
"type": "string",
"description": "Stato della pratica. Valori possibili: DRAFT (Bozza), SUBMITTED (Inviata), PROCESSING (Presa in carico), REQUEST_INTEGRATION (In attesa di integrazioni), APPROVED (Approvata), REJECTED (Rifiutata), WITHDRAW (Ritirata), CANCELLED (Annullata)",
"enum": [
"DRAFT",
"SUBMITTED",
"PROCESSING",
"REQUEST_INTEGRATION",
"APPROVED",
"REJECTED",
"WITHDRAW",
"CANCELLED"
]
},
"service": {
"type": "object",
"description": "Servizio correlato alla pratica",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
"description": "Identificativo interno del servizio"
}
},
"required": [
"id"
]
},
"actions": {
"type": "array",
"description": "Azioni che è possibile effettuare sulla Pratica con relativo URL",
"items": {
"type": "object",
"properties": {
"action": {
"type": "string",
"description": "Azione associata al link (es. view, request integration)",
"enum": [
"view",
"request_integration"
]
},
"url": {
"type": "string",
"format": "uri",
"description": "URL per accedere alla risorsa"
}
},
"required": [
"action",
"url"
]
}
},
"process_history": {
"type": "array",
"description": "An array representing the workflow of the process. Each entry includes the status and the date and time when the status was changed.",
"items": {
"properties": {
"status": {
"type": "string",
"description": "Stato della pratica. Valori possibili: DRAFT (Bozza), SUBMITTED (Inviata), PROCESSING (Presa in carico), REQUEST_INTEGRATION (In attesa di integrazioni), APPROVED (Approvata), REJECTED (Rifiutata), WITHDRAW (Ritirata), CANCELLED (Annullata)",
"enum": [
"DRAFT",
"SUBMITTED",
"PROCESSING",
"REQUEST_INTEGRATION",
"APPROVED",
"REJECTED",
"WITHDRAW",
"CANCELLED"
]
},
"status_changed_at": {
"type": "string",
"format": "date-time",
"description": "Date and time when the status change occurred, formatted using ISO 8601 standard",
"examples": [
"2025-01-20T14:30:00Z",
"2025-01-20T14:30:00+02:00"
]
}
}
}
}
},
"required": [
"external_id",
"subject",
"applicant",
"status",
"service",
"actions",
"process_history"
]
},
"documents": {
"type": "array",
"items": {
"$ref": "#/definitions/document"
}
}
},
"required": [
"application"
],
"definitions": {
"person": {
"type": "object",
"properties": {
"given_name": {
"type": "string",
"description": "Nome della persona"
},
"family_name": {
"type": "string",
"description": "Cognome della persona"
},
"identifiers": {
"type": "object",
"description": "Sistemi di identificazione digitale della persona (attualmente supportiamo solo il codice fiscale)",
"properties": {
"tax_code": {
"type": "string",
"description": "Codice fiscale",
"minLength": 16,
"maxLength": 16
},
"spid_code": {
"type": "string",
"description": "Codice SPID"
},
"id_eidas": {
"type": "string",
"description": "Codice eIDAS"
},
"anpr_id": {
"type": "string",
"description": "Codice unico dell'anagrafe"
},
"inps_id": {
"type": "string",
"description": "Codice INPS"
},
"io_wallet_id": {
"type": "string",
"description": "Codice di PagoPA"
},
"email": {
"type": "string",
"description": "Email se usata come identificativo utente"
}
},
"required": [ "tax_code" ],
"anyOf": [
{ "required": [ "tax_code" ] },
{ "required": [ "spid_code" ] },
{ "required": [ "id_eidas" ] },
{ "required": [ "anpr_id" ] },
{ "required": [ "inps_id" ] },
{ "required": [ "io_wallet_id" ] },
{ "required": [ "email" ] }
]
},
"contacts": {
"type": "object",
"properties": {
"mobile": {
"type": "string",
"description": "Numero di cellulare"
},
"phone": {
"type": "string",
"description": "Numero di telefono"
},
"email": {
"type": "string",
"description": "Email"
},
"pec": {
"type": "string",
"description": "PEC"
}
}
}
},
"required": [
"given_name",
"family_name",
"identifiers"
]
},
"document": {
"title": "Document",
"type": "object",
"additionalProperties": false,
"properties": {
"external_id": { "type": "string" },
"title": { "type": "string" },
"description": { "type": "string" },
"folder": { "type": "string" },
"main_document": {
"name": { "type": "string" },
"url": { "type": "string", "format": "uri" }
},
"type": {
"type": "string",
"enum": [
"application-request",
"integration-request",
"integration-response",
"application-outcome",
"application-withdraw",
"application-revocation"
]
},
"registration_data": {
"type": "object",
"properties": {
"date": { "type": "string" },
"registry_code": { "type": "string" }
},
"required": ["date", "registry_code"]
},
"sender": {
"email": "[email protected]"
}
},
"required": [
"external_id",
"title",
"description",
"folder",
"main_document",
"type"
]
}
}
}
Il seguente endpoint serve per il cambio di stato di una pratica lite. Permette opzionalmente di inviare un messaggio al richiedente contestualmente al cambio stato
POST /api/application-lite/{id}/status
Content-Type: application/json
Authorization: Bearer {{auth_token}}
{
"new_status": 3000,
"message": "Messaggio da inviare al richiedente al relativo al cambio stato"
}
La modalità push tramite API REST e json rappresenta un modo standard e diffuso di interazione tra sistemi, tuttavia, non sempre è possibile implementarlo. In questi casi possiamo valutare altre modalità di integrazione mediante un progetto personalizzato. Di seguito vengono riportate a scopo esplicativo due modalità di integrazione alternative che è possibile valutare:
Modalità di invio massivo (batch): permette di importare una grande quantità di dati ad ogni invocazione ma non garantisce l'esecuzione in tempo reale. È utile sopratutto per le prime importazioni, o in tutti quei casi in cui non ho necessità di avere uno stato aggiornato in tempo reale. È possibile fornire i dati nei formati CSV o JSON.
Modalità polling: il sistema esterno espone un API da invocare a scadenza regolare che permette al nostro sistema di recuperare gli aggiornamenti. Fondamentale che API dia la possibilità di recuperare gli aggiornamenti incorsi in un intervallo di tempo
Confronto tra le possibili modalità:
aggiornamento del dato per il cittadino
in tempo reale
latenza di ore
a intervalli di tempo (minuti/ore)
gestione degli errori latenza per correggere un errore
errori gestiti in tempo reale
errori gestiti alla fine del batch
errore gestito alla fine dell'intervallo
I widget dell'area personale sono una serie di componenti js/html/css che sono inseribili in una qualunque pagina web. In particolare alcuni di questi sono servizi built-in che i comuni devono offrire
Widget generali:
Login Box
Satisfy
I servizi built-in implementati sono:
Richiedi assistenza
Segnala problema in città
Prenota appuntamento
Pagamenti dovuti
I servizi sono presenti in ogni tenant fin dalla sua creazione e non sono modificabili dagli amministratori. Inoltre possono generare pratiche del tutto simili a quelle dei servizi tradizionali, ma il modulo è standard e come amministratore non è possibile modificarlo.
Ogni tenant (ente) punta in automatico alla sua versione widget compatibile in modo automatico
Tutti i widget possono essere caricati con bootstrap-italia 2 se il sito che lo contiene non carica già una versione di bts2 oppure senza
Esempio
Se in una pagina ci sono più widget lo script con le variabili si inserisce una volta
Codice da inserire nella pagina per il caricamento del widget, sostituire <domain> con il link dell'area personale dell’ente es: https://servizi.comune.bugliano.pi.it
Il widget restituisce come pagina di default /
oppure /#
la pagina di invio nuova segnalazione
Sotto la path /#/segnalazioni
restituisce la pagina della lista di segnalazioni Sotto la path /#/segnalazioni/{id}
restituisce la pagine del dettaglio di una segnalazione
Dalla versione 1.5.0 è stata aggiunta una nuova variabile per limitare la ricerca degli indirizzi tramite zone, configurando un bounding_box (x1,y1,x2,y2) x = longitudine, y = latitudine
Dalla versione 1.6.0 sono state aggiunte due variabili d'ambiante per la configurazione del provider delle mappe basato su nominatim
di default il provider è configurato su nominatim.openstreetmap.org
.
OC_MAP_SEARCH_PROVIDER
configura il provider di ricerca es. ricerca indirizzi da select
All'occorrenza può essere cambiato con un'altro provider custom basato su nominatim
OC_MAP_REVERSE_PROVIDER
configura il provider di ricerca inversa, ovvero restituisce l'indirizzo da punto su mappa, es. quando sposto un marker da mappa mi restituisce un indirizzo testuale.
All'occorrenza può essere cambiato con un'altro provider custom basato su nominatim
Configurazione per mostrare il campo Valuta l'importanza del problema
Dalla versione 1.14.3
OC_SHOW_SEVERITY_FIELD
gestisce l'abilitazione del campo Valuta l'importanza del problema
nel form della segnalazione disservizio. Di default il campo è nascosto.
Personalizzazione campi obbligatori per la segnalazione disservizio.
Dalla versione 1.16.7
Per rendere obbligatori altri campi del form delle segnalazioni oltre a quelli già previsti di default è possibile popolarewindow.OC_REQUIRED_FIELDS
con la lista dei campi che si vuole rendere obbligatori.
lista dei campi che è possibile rendere obbligatori
Dalla versione 1.11.0 è stata aggiunta la variabile per rendere modificabile la lista delle categorie
Categorie con canali esterni (dalla versione 1.12.3)
Per le categorie configurate con link esterni, il widget invita l'utente a proseguire tramite i canali esterni e impedisce la prosecuzione della compilazione e dell'invio della segnalazione.
I link esterni vanno configurati nel json richiamato tramite window.OC_CATEGORIES_URL
popolando all'interno della categoria, il valore external_ref
(opzionale) con i seguenti campi:
channel
esempio:
Configurazione apertura dettaglio segnalazione (dalla versione 1.16.0)
Al termine della compilazione e di invio di una segnalazione se l'utente si è autenticato è presente un link per andare al dettaglio della segnalazione.
Di default il link punta al dettaglio della segnalazione sull'area personale, in alternativa è possibile configurare l'apertura del dettaglio rimanendo nel widget tramite variabile window.OC_SHOW_DETAIL_ON_WEBSITE=true
Nelle versione precedenti alla 1.17.0 il routing del widget funzionava solo tramite # HashRouting
Es.
Aggiungendo la nuova variabile d'ambiante window.OC_BASENAME
è possibile trasformare il routing in versione BrowserRouting
Es.
Per configurare la variabile window.OC_BASENAME
c'è bisogno del path dove è configurato il widget con eventuali sottodomini se il widget è configurato in path non di primo livello.
Es. https://sito.it -> window.OC_BASENAME='/'
Es. https://sito.it/segnalazioni -> window.OC_BASENAME='/segnalazioni'
Es. https://sito.it/segnalazioni/123 -> window.OC_BASENAME='/segnalazioni/123'
Sui servizi built-in:
Richiedi assistenza
Segnala problema in città
Prenota appuntamento
è stata aggiunta la possibilità di auto generasi un breadcrums semplice del tipo Home / Segnalazioni
dove Home
è la homepage del sito e segnalzioni
è la route del widget, aggiungendo la variabile:
window.OC_RENDER_BREADCRUMB=true
se la variabile non è presente o ha valore false
la breadcrums non verrà abilitata
Se si hanno altri tipi di autenticazione oltre a SPID è possibile personalizzare il titolo del popup.
Il pulsante Accedi dopo aver effettuato la login può mostrare un menù a tendina con vari voci che ti ridirigono all'area personale, per abilitarlo omettere o impostare come da variabile sotto a false
Per disabilitarlo e vedere solo al voce do logout impostare la variabile a true
Il widget Satisfy consente di raccogliere feedback da parte degli utenti. È disponibile in diverse versioni ed è consigliato utilizzare sempre l’ultima versione aggiornata per garantire compatibilità e funzionalità ottimali. Tutte le versioni che puntano alla Stanza del Cittadino si aggiornano automaticamente.
Tutte le versioni del widget sono disponibili sulla
Se il sito non utilizza già Bootstrap Italia 2 (BI2), includere la versione del widget che lo incorpora:
Sostituire
{version}
con la versione desiderata (es.1.6.0
). Questa versione è auto-aggiornante.
Se il sito utilizza già Bootstrap Italia 2, includere solamente il componente e lo script del widget:
Oppure, utilizzando una versione CDN:
data-entrypoints
è un attributo obbligatorio e contiene l’UUID della configurazione globale del widget.
È possibile specificare endpoint alternativi per i salvataggi e per le chiamate API:
Parametri disponibili
Versione con bootstrap-italia 2
"https://servizi.comune.bugliano.pi.it/widgets/bookings/bootstrap-italia@2/js/bookings.js"
"https://servizi.comune.bugliano.pi.it/widgets/bookings/bootstrap-italia@2/css/bookings.css"
Versione senza bootstrap-italia 2
"https://servizi.comune.bugliano.pi.it/widgets/bookings/js/bookings.js"
"https://servizi.comune.bugliano.pi.it/widgets/bookings/css/bookings.css"
<script>
window.OC_BASE_URL = 'https://servizi.comune.bugliano.pi.it/lang';
window.OC_PRIVACY_URL = 'https://www.comune.bugliano.pi.it/Privacy';
window.OC_AUTH_URL = 'https://servizi.comune.bugliano.pi.it/lang/login';
</script>
<div id="oc-paymentsDue"></div>
<script>
window.OC_BASE_URL = '<domain>/lang';
window.OC_PRIVACY_URL = 'https://www.comune.bugliano.pi.it/Privacy';
window.OC_AUTH_URL = '<domain>/lang/login';
window.OC_CHECKOUT_URL = 'https://api.stanzadelcittadino.it/pagopa/checkout';
</script>
<link rel="stylesheet" href="<domain>/widgets/payments-due/css/paymentsDue.css" />
<script src="<domain>/widgets/payments-due/js/paymentsDue.js"></script>
<div id="oc-bookings"></div>
<script>
window.OC_BASE_URL = 'https://servizi.comune.bugliano.pi.it/lang';
window.OC_PRIVACY_URL = 'https://www.comune.bugliano.pi.it/Privacy';
window.OC_AUTH_URL = 'https://servizi.comune.bugliano.pi.it/lang/login';
</script>
<link rel="stylesheet" href="https://servizi.comune.bugliano.pi.it/widgets/bookings/css/bookings.css" />
<script src="https://servizi.comune.bugliano.pi.it/widgets/bookings/js/bookings.js"></script>
<div id="oc-inefficiencies"></div>
<link rel="stylesheet" type="text/css" href="https://servizi.comune.bugliano.pi.it/widgets/inefficiencies/css/inefficiencies.css" />
<script>
window.OC_BASE_URL = 'https://servizi.comune.bugliano.pi.it/lang';
window.OC_PRIVACY_URL = 'https://www.comune.bugliano.pi.it/Privacy';
window.OC_AUTH_URL = 'https://servizi.comune.bugliano.pi.it/lang/login';
</script>
<script src="https://servizi.comune.bugliano.pi.it/widgets/inefficiencies/js/inefficiencies.js"></script>
window.OC_BB = '8.66272,44.52197,9.09805,44.37590';
window.OC_MAP_SEARCH_PROVIDER = 'nominatim.openstreetmap.org';
window.OC_MAP_REVERSE_PROVIDER = 'nominatim.openstreetmap.org';
window.OC_SHOW_SEVERITY_FIELD=true;
window.OC_REQUIRED_FIELDS=['address','type','fiscal_code','severity']
nome
descrizione
type
Tipo di disservizio (categoria)
address
Luogo (indirizzo)
fiscal_code
Codice fiscale
severity
Valuta l'importanza del problema
window.OC_CATEGORIES_URL = 'https://static.opencityitalia.it/widgets/inefficiencies/data/categories-multi.json';
nome
descrizione
title
titolo
text
descrizione
channels
[channel]
nome
descrizione
type
web, email, phone, ios o android
href
link href
label
link label
...
{
"label": "Manutenzione",
"value": "8f06c823-e476-4ec1-a208-6fd4956e5a56",
"external_ref": {
"title": "Contatta questo contatto esterno",
"text": "Per la manutenzione di:\n \u2022 Strade \n \u2022 Marciapiedi...",
"channels": [
{
"type": "web",
"label": "vai al sito www.google.it",
"href": "https://www.google.it"
},
{
"type": "email",
"label": "Scrivi un'email a [email protected]",
"href": "mailto:[email protected]"
},
{
"type": "phone",
"label": "Telefona al numero verde 12345789",
"href": "tel://12345789"
},
{
"type": "ios",
"label": "Scarica l'app dedicata",
"href": "https://apple-store.com/mia-app"
},
{
"type": "android",
"label": "Scarica l'app dedicata",
"href": "https://play.google.com/mia-app"
}
]
}
},
Versione con bootstrap-italia 2
<div id="oc-helpdesk"></div>
<link rel="stylesheet" href="https://servizi.comune.bugliano.pi.it/widgets/helpdesk/bootstrap-italia@2/css/helpdesk.css" />
<script>
window.OC_BASE_URL = 'https://servizi.comune.bugliano.pi.it/lang';
window.OC_PRIVACY_URL = 'https://www.comune.bugliano.pi.it/Privacy';
window.OC_AUTHURL = 'https://servizi.comune.bugliano.pi.it/lang/login';
</script>
<script src="https://servizi.comune.bugliano.pi.it/widgets/helpdesk/bootstrap-italia@2/js/helpdesk.js"></script>
Versione senza bootstrap-italia 2
<div id="oc-helpdesk"></div>
<link rel="stylesheet" href="https://servizi.comune.bugliano.pi.it/widgets/helpdesk/css/helpdesk.css" />
<script>
window.OC_BASE_URL = 'https://servizi.comune.bugliano.pi.it/lang';
window.OC_PRIVACY_URL = 'https://www.comune.bugliano.pi.it/Privacy';
window.OC_AUTHURL = 'https://servizi.comune.bugliano.pi.it/lang/login';
</script>
<script src="https://servizi.comune.bugliano.pi.it/widgets/helpdesk/js/helpdesk.js"></script>
<div id="oc-login-box"></div>
<script>
window.OC_BASE_URL = 'https://servizi.comune.bugliano.pi.it/lang';
window.OC_AUTH_URL = 'https://servizi.comune.bugliano.pi.it/lang/login';
window.OC_SPID_BUTTON = 'true' // da usare se si vuole utilizzare la versione senza popup login spid
window.OC_AUTH_LABEL = 'SPID/CNS/EIDAS/CIE'
</script>
<script src="https://servizi.comune.bugliano.pi.it/widgets/login-box/bootstrap-italia@2/js/login-box.js"></script>
window.OC_AUTH_LABEL = 'Testo personalizzato'
window.OC_USER_MENU_DISABLED = false
window.OC_USER_MENU_DISABLED = true
<script src="https://static.opencityitalia.it/widgets/satisfy/version/{version}/bootstrap-italia@2/js/satisfy.js"></script>
<app-widget data-entrypoints="{UUID}"></app-widget>
<script src="https://servizi.comune.bugliano.pi.it/widgets/satisfy/js/satisfy.js"></script>
<app-widget data-entrypoints="{UUID}"></app-widget>
<script src="https://static.opencityitalia.it/widgets/satisfy/version/{version}/js/satisfy.js"></script>
<app-widget
data-entrypoints="{UUID}"
data-api-ratings="https://satisfy.opencityitalia.it/api/v1/ratings"
data-api-configs="https://satisfy.opencityitalia.it/v1/graphql"
></app-widget>
<script src="https://servizi.comune.bugliano.pi.it/widgets/satisfy/bootstrap-italia@2/js/satisfy.js"></script>
data-entrypoints
Obbligatorio. UUID della configurazione del widget.
data-api-ratings
URL personalizzato per le richieste POST di salvataggio dei feedback. Se non specificato, usa il default https://satisfy.opencityitalia.it/api/v1/ratings
.
data-api-configs
URL personalizzato per le chiamate API GraphQL. Se non specificato, usa https://satisfy.opencityitalia.it/v1/graphql
.
Il pdnd-connector
è un microservizio che funge da intermediario tra OpenCity e la piattaforma PDND, gestendo la complessità dell'autenticazione (ottenimento del voucher) e la comunicazione con gli enti erogatori di e-service (come ANPR e INPS). Espone delle API per OpenCity che consentono la fruizione e la validazione dei dati.
E' il token che collegato all'utenza del cittadino. Il contenuto è il seguente
username (codice fiscale utente)
roles (USER_ROLE)
exp (data scadenza)
tenant_id
L'audit assertion è un token JWT che viene utilizzato per tracciare e verificare le operazioni effettuate sui servizi dell'ente erogatore (es. ANPR). Esso, oltre alle solite info contenute in un token JWT, è composto anche dalle seguenti informazioni di contesto:
aud (audience): destinatario del token (il servizio dell'ente erogatore)
purposeId: identificativo dello scopo della richiesta
iss (issuer): identificativo del client che fa la richiesta
sub (subject): identificativo del client
userID: identificativo dell'utente che fa la richiesta
userLocation: codice IPA dell'ente
LoA (Level of Assurance): livello di autenticazione
L'audit assertion viene utilizzato in due modi principali:
Come header Agid-JWT-TrackingEvidence
nelle richieste HTTP verso i servizi dell'ente erogatore
Come base per generare il client assertion (viene fatto l'hash SHA-256 dell'audit assertion)
Questo meccanismo serve a:
Tracciare chi ha fatto la richiesta
Verificare l'autenticità della richiesta
Garantire la non ripudiabilità delle operazioni
Mantenere un audit trail delle operazioni effettuate
Il token viene firmato usando una chiave privata RSA del client PDND per garantire l'autenticità e l'integrità dei dati.
Il client assertion è un token JWT che viene generato dopo l'audit assertion e serve in fase di richiesta dati dall'ente erogatore come prova di autenticazione del client verso il servizio di autenticazione PDND.
E' una firma digitale che viene utilizzata per garantire che i dati inviati dal cittadino all'invio del modulo non siano stati alterati rispetto a quando sono stati restituiti al cittadino dal pdnd-connector dopo averli richiesti al servizio dell'ente erogatore. La firma contiene:
Digest SHA256 del payload
Headers firmati (Content-Type, Digest)
Autenticazione Iniziale Il flusso ha inizio quando un cittadino si autentica su Opencity attraverso il proprio sistema di identità digitale (SPID, CIE, ecc.). Questa autenticazione genera un JWT token che contiene:
Username (codice fiscale)
Tenant ID
Ruoli
Timestamp di scadenza
Avvio della Richiesta Quando il cittadino inizia a compilare un modulo che utilizza un Nested Form abilitato PDND, il PDND Connector riceve una richiesta che include tre elementi fondamentali:
Config ID (identifica la configurazione del servizio)
Codice fiscale
Format (formato di output richiesto)
Configurazione del Connector Il PDND Connector, che funge da intermediario sicuro tra Opencity e la PDND, utilizza le configurazioni precedentemente salvate per preparare la richiesta. Queste configurazioni includono
Client ID
KID (Key ID)
Chiave privata RSA
Service Audience
Purpose ID
Endpoint del servizio
Il Connector gestisce in modo sicuro sia le chiavi pubbliche che private, esponendo solo le chiavi pubbliche attraverso le API.
Autenticazione a Due Livelli Il Connector inizia un processo di autenticazione a due livelli con la PDND:
Primo Livello (Audit Assertion): Un token JWT firmato con la chiave privata del client, contenente: iat, exp, nbf, aud, purposeId, iss, sub, jti, dnonce, userID, userLocation, LoA. Il token ha una validità di 60 minuti.
Secondo Livello (Client Assertion): Un token JWT che include l'hash SHA-256 dell'Audit Assertion, esso contiene: iss, aud, jti, iat, nbf, exp, purposeId, sub, digest. Il token ha una validità di 24 ore.
Ottenimento del Voucher Il Client Assertion viene utilizzato per ottenere dalla PDND il Voucher di accesso al servizio. Il Voucher rappresenta l'autorizzazione effettiva ad accedere al servizio richiesto all'ente erogatore. Per motivi di compliance legale, il Voucher viene salvato localmente o su S3, creando un audit trail delle operazioni effettuate.
Chiamata al Servizio Erogatore Con il Voucher in mano, il PDND Connector contatta direttamente l'API dell'Ente Erogatore (come ANPR). La richiesta viene arricchita con diversi header di sicurezza:
Header Authorization: Bearer <voucher>
Header Agid-JWT-TrackingEvidence: <audit_assertion>
Header Digest: SHA-256=<digest64>
Header Agid-JWT-Signature: <signature>
Il payload contiene i parametri della richiesta (es. codice fiscale)
Elaborazione della Risposta Quando l'Ente Erogatore risponde, il PDND Connector:
Elabora la risposta per renderla compatibile con il formato atteso dal Nested Form
Aggiunge una sezione "meta" che include una firma digitale dei dati ricevuti
La firma viene generata usando SHA-256 per l'hashing dei dati e la chiave privata RSAper la firma
Verifica e Restituzione La risposta finale, che include sia i dati formattati che la sezione meta con la firma, viene restituita all'area personale. Il client può verificare la firma usando la chiave pubblica, garantendo che:
I dati non siano stati modificati durante la trasmissione
I dati provengano effettivamente dalla fonte dichiarata
Ricezione dei Dati Il flusso di validazione inizia quando l'area personale (backend) riceve i dati e la firma digitale dalla chiamata GET al PDND Connector. Questi dati includono:
Il campo data
contenente le informazioni richieste (es. dati di residenza o stato famiglia)
Il campo meta
che contiene la firma digitale (signature
) generata dal Connector
Preparazione della Richiesta di Validazione L'area personale prepara una richiesta di validazione che include:
I dati da validare (data
)
La firma digitale (signature
)
Il Config ID che identifica la configurazione del servizio
Il formato dei dati
Il codice fiscale dell'utente
Invio al Connector La richiesta di validazione viene inviata al PDND Connector, che funge da validatore ufficiale dei dati. Questo passaggio è cruciale perché il Connector è l'unico componente che possiede le chiavi necessarie per verificare l'autenticità della firma.
Caricamento della Chiave Pubblica Il Connector:
Carica la configurazione del servizio usando il Config ID
Recupera il Client ID associato
Carica la chiave pubblica RSA del client Questa chiave pubblica è essenziale per la verifica della firma, poiché è la controparte della chiave privata usata per firmare i dati.
Verifica della Firma Il Connector esegue la verifica della firma attraverso un processo in più fasi:
Serializza i dati in formato JSON
Calcola l'hash SHA-256 dei dati
Verifica la firma usando la chiave pubblica RSA
Confronta il risultato con la firma ricevuta
Gestione del Risultato Il Connector gestisce il risultato della verifica:
Se la verifica ha successo, significa che i dati non sono stati alterati
Se la verifica fallisce, significa che i dati potrebbero essere stati modificati
In caso di errori durante il processo di verifica, viene restituito un errore specifico
Risposta di Validazione Il Connector risponde con un semplice booleano:
true
se la firma è valida e i dati non sono stati alterati
false
se la firma non è valida o i dati sono stati modificati
Questo flusso di validazione è fondamentale perché:
Garantisce l'integrità dei dati tra il momento in cui vengono ottenuti dal Connector e il momento in cui vengono utilizzati da Opencity
Previene la manipolazione dei dati durante la trasmissione
Fornisce una prova verificabile dell'autenticità dei dati
Contribuisce alla creazione di un audit trail completo delle operazioni
La validazione dei dati PDND avviene in un contesto specifico: quando un utente compila un form in OpenCity che include campi pre-compilati con dati provenienti dalla PDND (nested form con pdndField: true
). Questi dati, essendo certificati dalla PDND, devono mantenere la loro integrità durante l'intero processo di compilazione del form.
La validazione serve a garantire che i dati certificati dalla PDND non siano stati alterati dopo essere stati forniti dal pdnd-connector. Questo è fondamentale perché:
Mantiene l'affidabilità delle informazioni certificate
Previene la manipolazione manuale dei dati da parte dell'utente
Garantisce la tracciabilità e la non ripudiabilità delle informazioni
Hai ragione, ecco una versione più dettagliata del processo di validazione della firma digitale, con particolare attenzione al collegamento tra la firma generata in fase di fruizione e quella verificata in fase di validazione:
Quando il pdnd-connector restituisce i dati in risposta a una richiesta GET, genera una firma digitale attraverso questi passaggi:
Preparazione dei Dati:
I dati vengono serializzati in JSON
L'ordine dei campi è importante per la consistenza della firma
Generazione della Firma:
Viene calcolato l'hash SHA-256 dei dati JSON
La firma viene generata usando la chiave privata RSA
La firma viene codificata in Base64
Meccanismo di Firma
Algoritmo di Hash: SHA-256
Algoritmo di Firma: RSA con padding PKCS1v15
Codifica: Base64
Posizione: Campo meta.signature nella risposta JSO
Inclusione nella risposta:
La firma viene inclusa nel campo meta.signature
Vengono aggiunti altri metadati per il contesto
Quando arriva una richiesta di validazione, il pdnd-connector verifica che i dati non siano stati alterati:
Ricezione della Richiesta:
Riceve i dati da validare
Riceve la firma originale nel campo meta.signature
Preparazione per la Verifica:
I dati ricevuti vengono serializzati in JSON
È fondamentale che la serializzazione avvenga nello stesso modo della fase di fruizione
Verifica della Firma:
Viene decodificata la firma Base64
Viene calcolato l'hash SHA-256 dei dati ricevuti
La firma viene verificata usando la chiave pubblica RSA
Risposta di Validazione:
Se la verifica fallisce, restituisce false
Se la verifica ha successo, restituisce true
È cruciale comprendere che:
La firma verificata in fase di validazione deve essere esattamente la stessa generata in fase di fruizione
Qualsiasi modifica ai dati, anche minima, causerà il fallimento della validazione
Il processo di serializzazione JSON deve essere identico in entrambe le fasi
Questo processo garantisce che:
I dati non siano stati alterati durante la trasmissione
La firma sia autentica e proveniente dal pdnd-connector
L'integrità dei dati sia mantenuta durante l'intero ciclo di vita
Durata: Conservazione obbligatoria per 24 mesi
Scopo: Garantire la tracciabilità delle operazioni
Base Legale: Requisito di compliance
I dati originali e le firme vengono conservati per:
Audit trail
Verifica post-facto
Compliance normativa
L'ente fruitore deve dichiarare:
Finalità legali dell'accesso
Tipologia di dati personali
Basi giuridiche del trattamento
Misure di sicurezza adottate
L'ente è titolare del trattamento
Deve fornire informative privacy appropriate
Deve garantire la sicurezza dei dati
Questa struttura rende più chiara la separazione tra i vari aspetti della validazione, evidenziando il contesto, lo scopo, il processo e le regole di conservazione in modo distinto e logico.
L'admin, dall'interfaccia di configurazione dei pagamenti della Stanza del Cittadino compila la configurazione mediante una form, il cui json schema è servito dall'API /tenants/schema
Lo schema della form sopra riportata è il seguente
Premendo poi il bottone Salva, viene eseguita una POST /tenants
servita dal proxy, con payload
Per modificare una configurazione esistente, il proxy serve l'API PUT /tenants/{tenant_id}
e PATCH /tenants/{tenant_id}
Per eliminare una configurazione esistente, il proxy serve l'API DELETE /tenants/{tenant_id}
. In questo caso l'eliminazione è una soft-delete, ovvero la configurazione viene semplicemente disattivata settando il parametro active
a false
ed eliminando la configurazione dalla memoria ma non dallo storage.
L'admin, dall'interfaccia di configurazione dei pagamenti per un servizio compila la configurazione mediante una form, il cui json schema è servito dall'API /services/schema
Lo schema della form soprariportata è il seguente
Premendo poi il bottone Salva, viene eseguita una POST /services
servita dal proxy, con payload
Per modificare una configurazione esistente, il proxy serve l'API PUT /services/{service_id}
e PATCH /services/{service_id}
Per eliminare una configurazione esistente, il proxy serve l'API DELETE /services/{service_id}
. In questo caso l'eliminazione è una soft-delete, ovvero la configurazione viene semplicemente disattivata settando il parametro active
a false
.
Le configurazioni di tenant e servizi vengono salvate con la seguente alberatura
root
|____tenant_id_1
| |____tenant.json
| |____service_id_1.json
| |____service_id_2.json
| |____.....
| |____service_id_n.json
|____tenant_id_2
| |____tenant.json
| |____service_id_1.json
| |____service_id_2.json
| |____.....
| |____service_id_n.json
|____tenant_id_n
|____tenant.json
|____service_id_1.json
|____service_id_2.json
|____.....
|____service_id_n.json
Ogni microservizio deve rispettare gli
Ogni proxy deve soddisfare i requisiti suggeriti dal
Esposizione di un endpoint/status
per l'healthcheck: esso deve essere restituire 200 se il sistema è "healthy" e 503 in caso si presentino un problemi di connessione con kafka
Esposizione delle metriche mediante l'endpoint /metrics
con Prometheus utilizzando
Terminazione pulita del servizio (timeout di 15 secondi)
Esposizione di un endpoint /docs
per la consultazione della documentazione delle API esposte dal servizio secondo lo standard OpenAPI
Integrazione con per l'error reporting
Il sistema di logging deve rispettare il formato Apache e descrivere nei tre livelli debug, error e info. Ogni log, quando possibile, deve loggare il remote_id
e l'id
del documento processato
Il README deve seguire le
il nome del progetto e del servizio devono essere nella seguente forma : "Protocol Proxy <Nome del Protocollo Specifico>"
Inserire il file .gitlab-ci.yml
riportando in esso pipeline
Il Protocol Proxy deve esporre le seguenti metriche:
Avere una specifica in formato OpenAPI v3
Rispettare la : in particolare per quanto riguarda
(4.2)
(4.3) optando per la scelta di snake_case
per i nomi degli attributi
logging (4.4)
risultare valido nel
Il servizio deve essere multi tentant e multi servizio: deve quindi essere in grado di gestire le confiugurazioni per più tenant a cui possono essere attribuiti più servizi con il corrente sistema di protocollazione.
Ogni richiesta sia essa appartenente alla fase 1 o alla fase 2 descritte nella sezione "" devono essere trattate in maniera atomica.
Lo storage deve essere compatibile con Aws s3, Azure e local file system.
Il servizio deve essere configurabile attraverso le seguenti variabili d'ambiente.
Se le varibili senza valore di default non vengono settate, il servizio deve notificare la mancata impostazione attraverso un log di livello error e terminare in maniera pulita la propria esecuzione.
Se si sta facendo il deploy di un nuovo microservizio per la prima volta o si sta aggiungendo una nuova API o una nuova pagina a una interfaccia esistente, è necessario aggiungere alcuni controlli di qualità minima prima del rilascio.
Test automatici delle API nella CI (usare , un esempio di utilizzo e un esempio di integrazione nelle CI)
Test flusso standard
Inserire la configurazione del tenant
Inserire la configurazione del servizio
Inserire un esempio di documento non protocollato nel topic documents e verificare che venga correttamente protocollato
Inserire un esempio di documento protocollato nel topic documents e verificare che venga ignorato
Inserire un esempio di documento non protocollato nel topic documents per cui non esiste una configurazione di tenant e/o di servizio e verificare che venga ignorato
Test flusso di errore
Modificare la configurazione del servizio in modo che sia errata (mettendo ad esempio credenziali errate)
Inserire un esempio di documento non protocollato nel topic documents e verificare che, a seguito del fallimento, venga prodotto un evento nel topic di retry
Correggere la configurazione del servizio
Inserire il documento prodotto nel topic di retry all'interno del topic documents e verificare che venga correttamente protocollato
{
"display": "form",
"components": [
{
"label": "UUID del Tenant",
"placeholder": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"spellcheck": false,
"attributes": {
"readonly": "readonly"
},
"hidden": true,
"tableView": false,
"validate": {
"required": true
},
"key": "id",
"type": "textfield",
"input": true
},
{
"label": "Nome dell'ente",
"placeholder": "Comune di Bugliano",
"tableView": true,
"validate": {
"required": true
},
"key": "name",
"type": "textfield",
"input": true
},
{
"label": "Codice IPA dell'ente",
"placeholder": "A000",
"description": "Codice (tutto maiuscolo) assegnato all'Ente nel registro <a href='http://www.indicepa.gov.it' target='__blank'>indice PA</a>",
"spellcheck": false,
"tableView": true,
"validate": {
"required": true
},
"key": "IPA_code",
"type": "textfield",
"input": true
},
{
"label": "Password",
"placeholder": "************",
"description": "Password assegnata da MyPay all’Ente",
"spellcheck": false,
"tableView": false,
"validate": {
"required": true
},
"key": "password",
"type": "textfield",
"input": true
},
{
"label": "Password carrello dovuti",
"placeholder": "************",
"description": "Password assegnata all’ente creditore. Tale password va richiesta all’amministratore dell’ente creditore.<br>ATTENZIONE: questo dato NON coincide con la password (principale) dell’ente.",
"spellcheck": false,
"tableView": true,
"key": "cart_password",
"type": "textfield",
"input": true
},
{
"label": "Codice applicazione",
"spellcheck": false,
"tableView": true,
"key": "application_code",
"type": "textfield",
"input": true,
"hidden": true,
"defaultValue": "SDC"
},
{
"label": "Abilitato",
"key": "active",
"type": "checkbox",
"input": true,
"hidden": true,
"defaultValue": true
},
{
"label": "Salva",
"showValidations": false,
"tableView": false,
"key": "submit",
"type": "button",
"input": true
}
]
}
{
"name": "Comune di Bugliano",
"IPA_code": "P_TN",
"password": "4CKSILO7FQPZ",
"cart_password": "",
"submit": true,
"id": "60e35f02-1509-408c-b101-3b1a28109329",
"application_code": "",
"active": true
}
{
"display": "form",
"components": [
{
"label": "UUID del Servizio",
"placeholder": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"spellcheck": false,
"attributes": {
"readonly": "readonly"
},
"hidden": true,
"tableView": false,
"validate": {
"required": true
},
"key": "id",
"type": "textfield",
"input": true
},
{
"label": "UUID del Tenant",
"placeholder": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"spellcheck": false,
"attributes": {
"readonly": "readonly"
},
"hidden": true,
"tableView": false,
"validate": {
"required": true
},
"key": "tenant_id",
"type": "textfield",
"input": true
},
{
"label": "Scegli il tipo di pagamento",
"widget": "choicesjs",
"tableView": true,
"data": {
"values": [
{
"label": "PagoPA",
"value": "pagopa"
},
{
"label": "Marca da Bollo Digitale",
"value": "stamp"
},
{
"label": "PagoPA + Marca da Bollo Digitale",
"value": "pagopa_and_stamp"
}
]
},
"validate": {
"required": true
},
"key": "payment_type",
"type": "select",
"input": true,
"defaultValue": "pagopa"
},
{
"label": "Tipo dovuto",
"placeholder": "TD_CIE",
"description": "Riferimento alla tipologia del dovuto secondo la classificazione data dal beneficiario.",
"applyMaskOn": "change",
"spellcheck": false,
"tableView": true,
"validate": {
"required": true,
"maxLength": 64
},
"key": "management_id",
"customConditional": "show = data.payment_type == 'pagopa' || data.payment_type == 'pagopa_and_stamp'",
"type": "textfield",
"input": true
},
{
"label": "Dati specifici riscossione",
"placeholder": "9/3300.1",
"description": "Rappresenta l’indicazione dell’imputazione della specifica entrata ed è così articolato:<br><b>TIPO_CONTABILITA / CODICE_CONTABILITA</b>, dove TIPO_CONTABILITA ha il seguente significato:<ul><li>0 (Capitolo e articolo di Entrata del Bilancio dello Stato)</li><li>1 (Numero della contabilità speciale)</li><li>2 (Codice SIOPE)</li><li>9 (Altro codice ad uso dell’amministrazione)</li></ul>",
"applyMaskOn": "change",
"spellcheck": false,
"tableView": false,
"defaultValue": "9/3300.1",
"validate": {
"required": true,
"maxLength": 140
},
"key": "collection_data",
"customConditional": "show = data.payment_type == 'pagopa' || data.payment_type == 'pagopa_and_stamp'",
"type": "textfield",
"input": true
},
{
"label": "Abilitato",
"key": "active",
"type": "checkbox",
"input": true,
"hidden": true,
"defaultValue": true,
"tableView": false
},
{
"label": "Bilancio",
"tableView": false,
"defaultValue": true,
"key": "split_enable",
"customConditional": "show = data.payment_type == 'pagopa' || data.payment_type == 'pagopa_and_stamp'",
"type": "checkbox",
"input": true
},
{
"label": "Bilancio",
"reorder": false,
"addAnotherPosition": "bottom",
"layoutFixed": false,
"enableRowGroups": false,
"initEmpty": true,
"tableView": false,
"defaultValue": [
{
"split_id": "",
"split_budget_chapter": "",
"split_office_code": "",
"split_assessment": "",
"split_amount": ""
}
],
"key": "split",
"conditional": {
"show": true,
"when": "split_enable",
"eq": "true"
},
"customConditional": "show = (data.payment_type == 'pagopa' || data.payment_type == 'pagopa_and_stamp') && data.split_enable",
"type": "datagrid",
"input": true,
"components": [
{
"label": "ID",
"description": "Identificativo univoco della voce di bilancio. Testo libero",
"placeholder": "c_1",
"tableView": true,
"validate": {
"required": true
},
"key": "split_id",
"type": "textfield",
"input": true
},
{
"label": "Capitolo",
"placeholder": "Capitolo di bilancio c1",
"validate": {
"required": true
},
"tableView": true,
"key": "split_budget_chapter",
"type": "textfield",
"input": true
},
{
"label": "Ufficio",
"placeholder": "Codice ufficio bilancio c1",
"tableView": true,
"key": "split_office_code",
"type": "textfield",
"input": true
},
{
"label": "Accertamento",
"placeholder": "Accertamento c1",
"tableView": true,
"key": "split_assessment",
"type": "textfield",
"input": true
},
{
"label": "Importo",
"description": "Importo della voce di bilancio. NB: La somma degli importi delle voci DEVE equivalere all'importo totale",
"placeholder": "16.00",
"validate": {
"required": true
},
"mask": false,
"tableView": false,
"delimiter": false,
"requireDecimal": false,
"inputFormat": "plain",
"truncateMultipleSpaces": false,
"key": "split_amount",
"type": "number",
"input": true
}
]
},
{
"label": "hidden",
"calculateValue": "if (!data.split || data.split == 'undefined') {\n data.split = []\n} else if (typeof data.split==='object' && Object.keys(data.split).length === 0) {\n data.split = [];\n}\n\nif (!data.stamps || data.stamps == 'undefined') {\n data.stamps = []\n} else if (typeof data.stamps==='object' && Object.keys(data.stamps).length === 0) {\n data.stamps = [];\n}\n\nfor (i = 0; i < data.stamps.length; i++) {\n if (data.stamps[i].id) {\n data.stamps[i].amount = submission.metadata.selectData.stamps[i].id.label\n } else {\n data.stamps[i].amount = \"\"\n }\n} ",
"key": "hidden",
"type": "hidden",
"input": true,
"tableView": false
},
{
"label": "Informazioni Pagamento MBD",
"attrs": [
{
"attr": "",
"value": ""
}
],
"content": "<b>NOTA BENE:</b> Nel caso in cui si venga configurato <b>solamente</b> il pagamento la marca da bollo digitale, il valore dell'importo e/o del campo payment_amount nel modulo saranno ignorati",
"refreshOnChange": false,
"key": "stamp_info",
"conditional": {
"show": true,
"when": "payment_type",
"eq": "stamp"
},
"type": "htmlelement",
"input": false,
"tableView": false
},
{
"label": "Marca da Bollo Digitale",
"reorder": false,
"addAnotherPosition": "bottom",
"layoutFixed": false,
"enableRowGroups": false,
"initEmpty": true,
"tableView": false,
"defaultValue": [
{
"id": "01",
"amount": "16.00",
"collection_data": "9/3300.1"
}
],
"validate": {
"maxLength": "5",
"custom": "valid = !(data.stamps.length > 4 && data.payment_type == 'pagopa_and_stamp') ? true : 'Puoi inserire un massimo di 4 marche da bollo'"
},
"key": "stamps",
"customConditional": "show = data.payment_type == 'stamp' || data.payment_type == 'pagopa_and_stamp'",
"type": "datagrid",
"input": true,
"components": [
{
"label": "Taglio",
"widget": "choicesjs",
"tableView": true,
"data": {
"values": [
{
"label": "16.00",
"value": "01"
}
]
},
"template": "<span>{{ item.label }}</span> €",
"validate": {
"required": true
},
"key": "id",
"type": "select",
"input": true
},
{
"label": "Dati specifici riscossione",
"placeholder": "9/3300.1",
"description": "Rappresenta l’indicazione dell’imputazione della specifica entrata ed è così articolato:<br><b>TIPO_CONTABILITA / CODICE_CONTABILITA</b>, dove TIPO_CONTABILITA ha il seguente significato:<ul><li>0 (Capitolo e articolo di Entrata del Bilancio dello Stato)</li><li>1 (Numero della contabilità speciale)</li><li>2 (Codice SIOPE)</li><li>9 (Altro codice ad uso dell’amministrazione)</li></ul>",
"applyMaskOn": "change",
"spellcheck": false,
"tableView": false,
"defaultValue": "9/3300.1",
"validate": {
"required": true,
"maxLength": 140
},
"key": "collection_data",
"type": "textfield",
"input": true
}
]
},
{
"label": "Salva",
"showValidations": false,
"tableView": false,
"key": "submit",
"type": "button",
"input": true
}
]
}
{
"payment_type": "pagopa_and_stamp",
"management_id": "COSAP_TOSAP",
"collection_data": "9/0301109AP/",
"split_enable": true,
"split": [
{
"split_id": "c_1",
"split_budget_chapter": "Cap1",
"split_office_code": "Uff1",
"split_assessment": "Acc1",
"split_amount": 1
}
],
"hidden": "",
"submit": true,
"id": "e64a3bd1-0096-4b8e-8676-e53f7d6b4b9a",
"tenant_id": "60e35f02-1509-408c-b101-3b1a28109329",
"active": true,
"stamps": [
{
"id": "01",
"amount": "16.00",
"collection_data": "9/3300.1"
}
]
}
oc_document_validation_errors_total
cluster, env, app_name
la metrica deve misurare gli errori di validazione sull'evento letto (es. il transmission_type
contiene un valore diverso da Inbound
o Outbound
)
oc_api_requests_total
cluster, env, method, app_name, status_code
la metrica deve monitorare le chiamate http indicandone lo status code
oc_document_success_events_total
cluster, env, app_name
la metrica deve misurare solo gli eventi per cui il proxy ha una configurazione e che sono stati processati con successo
oc_document_failed_events_total
cluster, env, app_name
la metrica deve misurare gli eventi validi di cui però è fallito il processing per qualsiasi motivo (escluso il caso in cui non esiste una configurazione per esso)
oc_document_provider_errors_total
cluster, env, app_name
la metrica deve misurare gli eventi validi di cui però è fallito il processing a causa di un errore sul provider
oc_document_internal_errors_total
cluster, env, app_name
la metrica deve misurare gli eventi validi di cui però è fallito il processing per errori interni al codice
oc_document_provider_latency_bucket
cluster, env, app_name
istogramma che mostra la distribuzione di latenza delle risposte del provider
ENVIRONMENT
local
Indica l'ambiente di sviluppo (locale, dev, prod, ecc.) utilizzato da Sentry.
DEBUG
true
...
SENTRY_ENABLED
false
...
SENTRY_DSN
nessun valore
Endpoint per il monitoraggio di Sentry.
KAFKA_SERVER
kafka:9092
Lista di lunghezza variabile i cui elementi devono essere separati da virgola. lista degli Indirizzi dei broker Kafka per connettersi al cluster.
KAFKA_CONSUMER_GROUP
<nome_del_servizio>
Consumer group per Kafka.
KAFKA_CONSUMER_TOPIC
documents
Identifica il topic da cui consumare gli eventi Kafka.
KAFKA_PRODUCER_TOPIC
documents
Identifica il topic a cui inviare gli eventi Kafka.
KAFKA_RETRY_TOPIC
nessun valore
topic in cui produrre gli eventi consumati dal meccanismo di retry
SERVER_ADDRESS_PORT
0.0.0.0:8080
Indica l'indirizzo e la porta utilizzati per l'healthcheck.
CACHE_EXPIRATION
5m
...
CACHE_EVICTION
10m
...
STORAGE_TYPE
local
Tipo di storage dei pagamenti: s3, azure, local
STORAGE_ENDPOINT
nessun valore
Indirizzo di accesso allo storage.
STORAGE_ACCESS_S3_KEY
nessun valore
Chiave di accesso allo storage.
STORAGE_KEY_S3_ACCESS_SECRET
nessun valore
Chiave segreta di accesso allo storage.
STORAGE_S3_REGION
nessun valore
Location del cloud storage
STORAGE_BUCKET
nessun valore
Nome dello storage
STORAGE_BASE_PATH
"/data/"
Basepath dello storage
STORAGE_AZURE_ACCOUNT
nessun valore
Chiave dello storage AZURE
STORAGE_AZURE_KEY
nessun valore
Password dello storage AZURE
STORAGE_LOCAL_PATH
/data/
SDC_AUTH_TOKEN_USER
nessun valore
utente autenticazione per recuperare il token dalla piattaforma
SDC_AUTH_TOKEN_PASSWORD
nessun valore
password autenticazione per recuperare il token dalla piattaforma
{
"id": "68dada78-2398-4a11-b80a-98aaede3371c",
"user_id": "5b00796e-ef10-4c98-b9f5-b0f7ffd7a17d",
"type": "PAGOPA",
"tenant_id": "60e35f02-1509-408c-b101-3b1a28109329",
"service_id": "b21c4429-95e4-45d5-930f-44eb74136625",
"created_at": "2023-06-06T11:59:11+02:00",
"updated_at": "2023-06-12T00:10:16+02:00",
"status": "PAYMENT_STARTED",
"reason": "79501b2a-c9ad-41f8-a9e7-a885f2d570a2 - BNRMHL75C06G702B",
"remote_id": "79501b2a-c9ad-41f8-a9e7-a885f2d570a2",
"payment": {
"transaction_id": null,
"paid_at": null,
"expire_at": "2023-09-04T11:59:05+02:00",
"amount": 1.34,
"currency": "EUR",
"notice_code": "001550000000024427",
"iud": "68dada7823984a11b80a98aaede3371c",
"iuv": "550000000024427",
"split": null
},
"links": {
"online_payment_begin": {
"url": "https://iris-proxy-qa.boat.opencontent.io/online-payment/68dada78-2398-4a11-b80a-98aaede3371c?gw=https://iristest.rete.toscana.it/gateway/PaymentAuthentication?token=1686045553436001U132&expire_at=2023-06-06T12:13:13",
"last_opened_at": "2023-06-06T11:59:26+02:00",
"method": "GET"
},
"online_payment_landing": {
"url": "https://servizi.comune-qa.bugliano.pi.it/lang/it/pratiche/79501b2a-c9ad-41f8-a9e7-a885f2d570a2/detail",
"last_opened_at": "2023-06-06T11:59:57+02:00",
"method": "GET"
},
"offline_payment": {
"url": "https://iris-proxy-qa.boat.opencontent.io/notice/68dada78-2398-4a11-b80a-98aaede3371c",
"last_opened_at": "2023-06-06T11:59:20+02:00",
"method": "GET"
},
"receipt": {
"url": null,
"last_opened_at": null,
"method": "GET"
},
"notify": [
{
"url": "https://servizi.comune-qa.bugliano.pi.it/lang/api/applications/79501b2a-c9ad-41f8-a9e7-a885f2d570a2/payment",
"method": "POST",
"sent_at": null
}
],
"update": {
"url": "http://iris-proxy-qa.boat-backplane.opencontent.io/update/68dada78-2398-4a11-b80a-98aaede3371c",
"last_check_at": "2023-06-12T00:10:16+02:00",
"next_check_at": "2023-06-12T01:10:16+02:00",
"method": "GET"
}
},
"payer": {
"type": "human",
"tax_identification_number": "BNRMHL75C06G702B",
"name": "Michelangelo",
"family_name": "Buonarroti",
"street_name": "Cesare Battisti",
"building_number": "",
"postal_code": "38010",
"town_name": "Bugliano",
"country_subdivision": "PI",
"country": "IT",
"email": "[email protected]"
},
"event_id": "e0b25e33-9d38-4781-823a-ed04753aea01",
"event_version": "1.0",
"event_created_at": "2023-06-12T00:10:16+02:00",
"app_id": "iris-payment-proxy-qa:1.2.8"
}
id
UUID
user_id
UUID
type
string(50)
Ogni proxy deve implementare un tipo di pagamento. Se per esempio, il proxy in questione gestisce pagamenti pagopa allora il campo va validato verificando che esso sia valorizzato a PAGOPA
. In caso contrario si scarta l'evento e si emette un log di errore.
tenant_id
UUID
service_id
UUID
created_at
Datetime
ISO8601
updated_at
Datetime
ISO8601
status
Enum
Valori permessi: CREATION_PENDING CREATION_FAILED PAYMENT_PENDING PAYMENT_STARTED PAYMENT_CONFIRMED PAYMENT_FAILED NOTIFICATION_PENDING
COMPLETE EXPIRED
reason
string(140)
Causale del pagamento. Non può eccedere i 140 caratteri.
remote_id
UUID
payment
PaymentData
links
Links
payer
Payer
event_id
UUID
event_version
string(10)
L'attuale versione dell'evento deve essere "1.0". Ogni proxy deve validare la versione dell'evento in modo da sapere se processarlo o meno.
event_created_at
Datetime
ISO8601
app_id
string(100)
Valorizzarlo nel formato <nome-proxy>:<versione-proxy>
Es.
iris-payment-proxy:1.3.0
transaction_id
string(255)
Fornito dall'intermediario di pagamento. La lunghezza dunque può essere variabile
paid_at
Datetime
ISO8601
expire_at
Datetime
ISO8601 E' già valorizzato a priori, quindi non viene gestito dal proxy
amount
float
E' già valorizzato a priori, quindi non viene gestito dal proxy
currency
string(3)
ISO4217
notice_code
string(50)
iud
string(50)
è il valore del campo id
del pagamento in formato esadecimale
Es.
id: 4a263efb-300b-437a-ab50-116fa9aff8a7
iud: 4a263efb30
0b-437aab50116fa9aff8a7
iuv
string(50)
split
jsonArray
online_payment_begin
UrlData
online_payment_landing
UrlData
offline_payment
UrlData
receipt
UrlData
notify
List[Notify]
update
Update
url
string
last_opened_at
Datetime
ISO8601
method
Enum
Valori permessi: GET POST
url
string
method
Enum
Valori permessi: GET POST
sent_at
Datetime
ISO8601
url
string
last_check_at
Datetime
ISO8601
next_check_at
Datetime
ISO8601
method
Enum
Valori permessi: GET POST
type
Enum
Valori permessi: human legal
tax_identification_number
string(255)
name
string(255)
family_name
string(255)
street_name
string(255)
building_number
string(255)
postal_code
string(255)
town_name
string(255)
country_subdivision
string(2)
ISO 3166-2
country
string(2)
ISO 3166-1 alpha-2
email
string(255)
Con la versione 2 delle API di configurazione sono stati introdotti dei campi da introdurre obbligatoriamente in ogni integrazione.
I campi da introdurre obbligatoriamente a livello di configurazione del tenant sono:
id
: identificativo univoco del tenant
name
: nome del tenant
tax_identification_number
: codice fiscale dell'ente
id
UUID
Identificativo univoco
name
String
Non vuoto, massimo 255 caratteri
tax_identification_number
String
Codice fiscale valido
I campi da introdurre obbligatoriamente a livello di configurazione del pagamento sono:
payment_type
: tipo di pagamento, può assumere i valori pagopa
o stamp
, serve a distinguere tra un pagamento ordinario (es. TARI) e una marca da bollo digitale. Questa distinzione è necessaria in quanto in alcuni intermediare i metodi di creazione di un pagamento di una marca da bollo digitale differiscono da quelli di creazione di un pagamento ordinario.
remote_collection.id
: identificativo della collezione di provenienza della configurazione di pagamento.
remote_collection.type
: tipo di collezione di provenienza della configurazione di pagamento, può assumere valori quali application
, service
o altro.
amount
: importo del pagamento configurato.
reason
: causale del pagamento configurato
expire_at
: numero di giorni di validità del pagamento
receiver.tax_identification_number
: codice fiscale del beneficiario del pagamento, da usare in caso di pagamenti multibeneficiario
receiver.name
: nome del beneficiario del pagamento, da usare in caso di pagamenti multibeneficiario
collection_data
: tassonomia di pagoPA dei pagamenti
payment_type
Enum
Valori: PAGOPA
, STAMP
remote_collection.id
UUID
Identificativo univoco
remote_collection.type
String
Valori: application, service, altro
amount
float
Maggiore di 0
reason
String
Non vuoto, massimo 255 caratteri
expire_at
int
Maggiore di 0
receiver.tax_identification_number
String
Codice fiscale valido
receiver.name
String
Non vuoto, massimo 255 caratteri
collection_data
String
Tassonomia pagoPA valida
L'admin, dall'interfaccia di configurazione dei pagamenti della Stanza del Cittadino compila la configurazione mediante una form, il cui json schema è servito dall'API /tenants/schema
Lo schema della form sopra riportata è il seguente
{
"display": "form",
"components": [
{
"label": "UUID del Tenant",
"placeholder": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"spellcheck": false,
"attributes": {
"readonly": "readonly"
},
"hidden": true,
"tableView": false,
"validate": {
"required": true
},
"key": "id",
"type": "textfield",
"input": true
},
{
"label": "Nome dell'ente",
"placeholder": "Comune di Bugliano",
"tableView": true,
"validate": {
"required": true
},
"key": "name",
"type": "textfield",
"input": true
},
{
"label": "Codice IPA dell'ente",
"placeholder": "A000",
"description": "Codice (tutto maiuscolo) assegnato all'Ente nel registro <a href='http://www.indicepa.gov.it' target='__blank'>indice PA</a>",
"spellcheck": false,
"tableView": true,
"validate": {
"required": true
},
"key": "IPA_code",
"type": "textfield",
"input": true
},
{
"label": "Codice fiscale dell'ente",
"placeholder": "123456789",
"spellcheck": false,
"key": "tax_identification_number",
"type": "textfield",
"validate": {
"required": true
},
"input": true,
"tableView": true
},
{
"label": "Password",
"placeholder": "************",
"description": "Password assegnata da MyPay all\u2019Ente",
"spellcheck": false,
"tableView": false,
"validate": {
"required": true
},
"key": "password",
"type": "textfield",
"input": true
},
{
"label": "Password carrello dovuti",
"placeholder": "************",
"description": "Password assegnata all\u2019ente creditore. Tale password va richiesta all\u2019amministratore dell\u2019ente creditore.<br>ATTENZIONE: questo dato NON coincide con la password (principale) dell\u2019ente.",
"spellcheck": false,
"tableView": true,
"key": "cart_password",
"type": "textfield",
"input": true
},
{
"label": "Codice applicazione",
"spellcheck": false,
"tableView": true,
"key": "application_code",
"type": "textfield",
"input": true,
"hidden": true,
"defaultValue": "SDC"
},
{
"label": "Abilitato",
"key": "active",
"type": "checkbox",
"input": true,
"hidden": true,
"defaultValue": true
},
{
"label": "Salva",
"showValidations": false,
"tableView": false,
"key": "submit",
"type": "button",
"input": true
}
]
}
Premendo poi il bottone Salva, viene eseguita una POST /tenants
servita dal proxy, con payload
{
"name": "Comune di Bugliano",
"IPA_code": "p_tn",
"tax_identification_number": "00337460224",
"password": "4CKSILO7FQPZ",
"cart_password": "",
"application_code": "",
"active": true,
"enable_checkout": false,
"id": "60e35f02-1509-408c-b101-3b1a28109329"
}
Per modificare una configurazione esistente, il proxy serve l'API PUT /tenants/{tenant_id}
e PATCH /tenants/{tenant_id}
Per eliminare una configurazione esistente, il proxy serve l'API DELETE /tenants/{tenant_id}
. In questo caso l'eliminazione è una soft-delete, ovvero la configurazione viene semplicemente disattivata settando il parametro active
a false
ed eliminando la configurazione dalla memoria ma non dallo storage.
L'admin, dall'interfaccia di configurazione dei pagamenti per un servizio compila la configurazione mediante una form, il cui json schema è servito dall'API /configs/schema
Lo schema della form soprariportata è il seguente
{
"display": "form",
"components": [
{
"label": "UUID del Tenant",
"placeholder": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"spellcheck": false,
"attributes": {
"readonly": "readonly"
},
"hidden": true,
"tableView": false,
"validate": {
"required": true
},
"key": "tenant_id",
"type": "textfield",
"input": true
},
{
"label": "Container",
"tableView": false,
"validateWhenHidden": false,
"key": "remote_collection",
"type": "container",
"input": true,
"components": [
{
"label": "UUID del Servizio",
"placeholder": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"applyMaskOn": "change",
"hidden": true,
"spellcheck": false,
"tableView": true,
"validate": {
"required": true
},
"validateWhenHidden": false,
"key": "id",
"attributes": {
"readonly": "readonly"
},
"type": "textfield",
"input": true
},
{
"label": "Tipo Collezione",
"applyMaskOn": "change",
"hidden": true,
"tableView": true,
"validateWhenHidden": false,
"key": "type",
"type": "textfield",
"input": true
}
]
},
{
"label": "Lista Versamenti",
"reorder": false,
"addAnother": "Aggiungi Versamento",
"addAnotherPosition": "bottom",
"layoutFixed": false,
"enableRowGroups": false,
"initEmpty": false,
"tableView": false,
"defaultValue": [
{
"payment_type": "pagopa",
"management_id": "",
"reason": "",
"expire_at": "",
"receiver_enable": false,
"split": [],
"hidden": "",
"collection_data": "",
"receiver": {
"tax_identification_number": "",
"name": "",
"iban": ""
},
"pagopa_category": ""
}
],
"validate": {
"minLength": "1",
"maxLength": "1",
"custom": "valid = true;\n\nif (input && Array.isArray(input)) {\n for (let i = 0; i < input.length; i++) {\n const row = input[i] || {};\n\n const splitRows = row.split || [];\n if (Array.isArray(splitRows) && splitRows.length > 0) {\n const totalSplit = splitRows.reduce((sum, s) => sum + (Number(s.amount) || 0), 0);\n const expectedAmount = Number(row.amount);\n\n if (!isNaN(expectedAmount)) {\n if (totalSplit !== expectedAmount) {\n valid = `Errore nel versamento ${i + 1}: somma split (${totalSplit}) diversa da amount (${expectedAmount}).`;\n break;\n }\n }\n }\n }\n}\n"
},
"validateWhenHidden": false,
"key": "items",
"type": "datagrid",
"input": true,
"components": [
{
"label": "Versamenti",
"tableView": false,
"key": "versamenti",
"type": "fieldset",
"input": true,
"components": [
{
"label": "Scegli il tipo di pagamento",
"widget": "choicesjs",
"tableView": true,
"data": {
"values": [
{
"label": "PagoPA",
"value": "pagopa"
},
{
"label": "Marca da Bollo Digitale",
"value": "stamp"
}
]
},
"validate": {
"required": true
},
"key": "payment_type",
"type": "select",
"input": true,
"defaultValue": "pagopa"
},
{
"label": "Tipo dovuto",
"placeholder": "TD_CIE",
"description": "Riferimento alla tipologia del dovuto secondo la classificazione data dal beneficiario. Nel caso di una marca da bollo digitale indicare MARCA_BOLLO_DIGITALE",
"applyMaskOn": "change",
"spellcheck": false,
"tableView": true,
"validate": {
"required": true,
"maxLength": 64
},
"key": "management_id",
"type": "textfield",
"input": true
},
{
"label": "Importo",
"placeholder": "12.34",
"description": "Le cifre decimali vanno separate dalla parte intera tramite il punto.",
"applyMaskOn": "change",
"mask": false,
"tableView": false,
"delimiter": false,
"requireDecimal": false,
"inputFormat": "plain",
"truncateMultipleSpaces": false,
"validate": {
"required": true,
"custom": "valid = (row.payment_type == 'stamp' && row.amount == 16) || (row.payment_type == 'pagopa') ? true : \"In caso di marca da bollo digitale l'unico importo permesso è di 16 euro\""
},
"validateWhenHidden": false,
"key": "amount",
"type": "number",
"input": true
},
{
"label": "Causale",
"applyMaskOn": "change",
"tableView": true,
"validate": {
"required": true,
"maxLength": 60
},
"key": "reason",
"type": "textfield",
"input": true
},
{
"label": "Validità del pagamento (in gg)",
"applyMaskOn": "change",
"mask": false,
"tableView": false,
"delimiter": false,
"requireDecimal": false,
"inputFormat": "plain",
"truncateMultipleSpaces": false,
"validate": {
"required": true,
"min": 1
},
"validateWhenHidden": false,
"key": "expire_at",
"type": "number",
"input": true
},
{
"label": "Dati specifici riscossione",
"placeholder": "9/3300.1",
"description": "Rappresenta l'indicazione dell'imputazione della specifica entrata ed è così articolato:<br><b>TIPO_CONTABILITA / CODICE_CONTABILITA</b>, dove TIPO_CONTABILITA ha il seguente significato:<ul><li>0 (Capitolo e articolo di Entrata del Bilancio dello Stato)</li><li>1 (Numero della contabilità speciale)</li><li>2 (Codice SIOPE)</li><li>9 (Altro codice ad uso dell'amministrazione)</li></ul>",
"applyMaskOn": "change",
"spellcheck": false,
"tableView": false,
"defaultValue": "9/3300.1",
"validate": {
"required": true,
"maxLength": 140
},
"key": "collection_data",
"type": "textfield",
"input": true
},
{
"label": "Categoria",
"widget": "choicesjs",
"description": "Selezione dalla Tassonomia di PagoPA dei pagamenti.",
"tableView": true,
"dataSrc": "url",
"data": {
"url": "https://api.opencityitalia.it/datasets/pagopa",
"headers": [
{
"key": "",
"value": ""
}
]
},
"dataType": "object",
"idPath": "",
"valueProperty": "DATI SPECIFICI DI INCASSO",
"template": "<span>{{ item[\"NOME MACRO AREA\"] }}: {{ item[\"TIPO SERVIZIO\"] }}</span>",
"validate": {
"required": true
},
"key": "pagopa_category",
"type": "select",
"disableLimit": false,
"noRefreshOnScroll": false,
"input": true
},
{
"label": "Bilancio",
"reorder": false,
"addAnother": "Aggiungi bilancio",
"addAnotherPosition": "bottom",
"layoutFixed": false,
"enableRowGroups": false,
"initEmpty": true,
"tableView": false,
"calculateValue": "if (!row.split || row.split == 'undefined') {\n row.split = []\n} else if (typeof row.split==='object' && Object.keys(row.split).length === 0) {\n row.split = [];\n}",
"validateWhenHidden": false,
"key": "split",
"type": "datagrid",
"input": true,
"components": [
{
"label": "ID",
"description": "Identificativo univoco della voce di bilancio. Testo libero",
"placeholder": "c_1",
"tableView": true,
"validate": {
"required": true
},
"key": "id",
"type": "textfield",
"input": true
},
{
"label": "Capitolo",
"placeholder": "Capitolo di bilancio c1",
"validate": {
"required": true
},
"tableView": true,
"key": "budget_chapter",
"type": "textfield",
"input": true
},
{
"label": "Ufficio",
"placeholder": "Codice ufficio bilancio c1",
"tableView": true,
"key": "office_code",
"type": "textfield",
"input": true
},
{
"label": "Accertamento",
"placeholder": "Accertamento c1",
"tableView": true,
"key": "assessment",
"type": "textfield",
"input": true
},
{
"label": "Importo",
"placeholder": "16.00",
"description": "Importo della voce di bilancio. NB: La somma degli importi delle voci DEVE equivalere all'importo totale",
"applyMaskOn": "change",
"mask": false,
"tableView": false,
"delimiter": false,
"requireDecimal": false,
"inputFormat": "plain",
"truncateMultipleSpaces": false,
"validate": {
"required": true
},
"validateWhenHidden": false,
"key": "amount",
"type": "number",
"input": true
}
]
}
]
}
]
},
{
"label": "Abilitato",
"key": "active",
"type": "checkbox",
"input": true,
"hidden": true,
"defaultValue": true,
"tableView": false
},
{
"type": "button",
"label": "Salva",
"key": "submit",
"disableOnInvalid": true,
"input": true,
"tableView": false
}
]
}
Premendo poi il bottone Salva, viene eseguita una POST /configs
servita dal proxy, con payload
{
"tenant_id": "60e35f02-1509-408c-b101-3b1a28109329",
"remote_collection": {
"id": "37dbc4a5-be4c-4246-bfad-15b9f5f5e7f3",
"type": "service"
},
"items": [
{
"payment_type": "pagopa",
"management_id": "SH_OPENCONTENT",
"amount": 3.0,
"reason": "Pagamento immediato MyPay 4 Lombardia",
"expire_at": 180,
"collection_data": "9/123",
"pagopa_category": "9/123",
"split": [],
"receiver": {
"tax_identification_number": "80034840167",
"name": "Comune di Bugliano",
"iban": "",
"address": null,
"building_number": null,
"postal_code": null,
"town_name": null,
"country_subdivision": null,
"country": null
}
}
],
"active": true,
"id": "37dbc4a5-be4c-4246-bfad-15b9f5f5e7f3"
}
Per ottenere una lista di configurazioni di pagamento (fino a un massimo di 5), il proxy serve l'API GET /configs?config_id=config_id1&config_id=config_id2
Per modificare una configurazione esistente, il proxy serve l'API PUT /configs/{config_id}
e PATCH /configs/{config_id}
Per eliminare una configurazione esistente, il proxy serve l'API DELETE /configs/{config_id}
. In questo caso l'eliminazione è una soft-delete, ovvero la configurazione viene semplicemente disattivata settando il parametro active
a false
.
fruizione e-service Accertamento Cittadinanza
italian fiscal code
b212c4b4-db26-4404-8c7c-47dab99dd2e6
id that identify a specific configuration to call an e-service
b212c4b4-db26-4404-8c7c-47dab99dd2e6
identify the output payload structure
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
OK
Not Found
GET /anpr/accertamento-cittadinanza HTTP/1.1
Host:
Accept: */*
{
"data": {
"cittadinanza": [
{
"country": "text",
"nationality": "text"
}
]
},
"meta": {
"call_url": "text",
"created_at": "text",
"format": "text",
"signature": "text",
"source": "text"
}
}
Validate Accertamento Cittadinanza payload
italian fiscal code
b212c4b4-db26-4404-8c7c-47dab99dd2e6
id that identify a specific configuration to call an e-service
b212c4b4-db26-4404-8c7c-47dab99dd2e6
identify the output payload structure
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
OK
Not Found
POST /anpr/accertamento-cittadinanza HTTP/1.1
Host:
Content-Type: application/json
Accept: */*
Content-Length: 165
{
"Data": {
"cittadinanza": [
{
"country": "text",
"nationality": "text"
}
]
},
"Meta": {
"call_url": "text",
"created_at": "text",
"format": "text",
"signature": "text",
"source": "text"
}
}
{
"result": true
}
fruizione e-service Accertamento Residenza
italian fiscal code
b212c4b4-db26-4404-8c7c-47dab99dd2e6
id that identify a specific configuration to call an e-service
b212c4b4-db26-4404-8c7c-47dab99dd2e6
identify the output payload structure
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
OK
Not Found
GET /anpr/accertamento-residenza HTTP/1.1
Host:
Accept: */*
{
"data": {
"address": "text",
"address_data": {
"cap": "text",
"codice_comune": 1,
"comune": "text",
"luogo_istat": "text",
"pr": "text"
},
"codice_istat_comune": "text",
"country": "text",
"county": "text",
"house_number": "text",
"locator_within": "text",
"municipality": "text",
"postal_code": "text"
},
"meta": {
"call_url": "text",
"created_at": "text",
"format": "text",
"signature": "text",
"source": "text"
}
}
Validate Accertamento Residenza payload
italian fiscal code
b212c4b4-db26-4404-8c7c-47dab99dd2e6
id that identify a specific configuration to call an e-service
b212c4b4-db26-4404-8c7c-47dab99dd2e6
identify the output payload structure
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
OK
Not Found
POST /anpr/accertamento-residenza HTTP/1.1
Host:
Content-Type: application/json
Accept: */*
Content-Length: 373
{
"Data": {
"address": "text",
"address_data": {
"cap": "text",
"codice_comune": 1,
"comune": "text",
"luogo_istat": "text",
"pr": "text"
},
"codice_istat_comune": "text",
"country": "text",
"county": "text",
"house_number": "text",
"locator_within": "text",
"municipality": "text",
"postal_code": "text"
},
"Meta": {
"call_url": "text",
"created_at": "text",
"format": "text",
"signature": "text",
"source": "text"
}
}
{
"result": true
}
Retrieve Stato Famiglia for a specific fiscal code with specific configuration
italian fiscal code
b212c4b4-db26-4404-8c7c-47dab99dd2e6
id that identify a specific configuration to call an e-service
b212c4b4-db26-4404-8c7c-47dab99dd2e6
identify the output payload structure
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
OK
Not Found
GET /anpr/stato-famiglia HTTP/1.1
Host:
Accept: */*
{
"data": null,
"meta": {
"call_url": "text",
"created_at": "text",
"format": "text",
"signature": "text",
"source": "text"
}
}
Validate Stato Famiglia payload
italian fiscal code
b212c4b4-db26-4404-8c7c-47dab99dd2e6
id that identify a specific configuration to call an e-service
b212c4b4-db26-4404-8c7c-47dab99dd2e6
identify the output payload structure
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
data to validate. Can be different depending by the format param
OK
Not Found
POST /anpr/stato-famiglia HTTP/1.1
Host:
Content-Type: application/json
Accept: */*
Content-Length: 111
{
"Data": null,
"Meta": {
"call_url": "text",
"created_at": "text",
"format": "text",
"signature": "text",
"source": "text"
}
}
{
"result": true
}
Retrieve the list of e-services available through the service
the starting point or the index from which the data should be retrieved
6
The limit parameter specifies the maximum number of items to be returned in a single page or request
...
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
OK
Bad Request
GET /e-services HTTP/1.1
Host:
Accept: */*
{
"data": [
{
"aud_client_assertion": "text",
"audience": "text",
"endpoint": "text",
"id": "text",
"name": "text",
"slug": "text",
"version": "text"
}
],
"links": {
"next": "text",
"prev": "text",
"self": "text"
},
"meta": {
"page": {
"limit": 1,
"offset": 1,
"sort": "text"
},
"total": 1
}
}
...
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
123456
nome del tenant
comune di Bugliano
Created
Bad Request
POST /tenants HTTP/1.1
Host:
Content-Type: application/json
Accept: */*
Content-Length: 93
{
"id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6",
"ipa_code": "123456",
"name": "comune di Bugliano"
}
{
"created_at": "text",
"id": "text",
"ipa_code": "text",
"name": "text",
"updated_at": "text"
}
...
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
OK
Bad Request
GET /tenants/{tenant_id} HTTP/1.1
Host:
Accept: */*
{
"created_at": "text",
"id": "text",
"ipa_code": "text",
"name": "text",
"updated_at": "text"
}
...
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
123456
nome del tenant
comune di Bugliano
OK
Bad Request
PUT /tenants/{tenant_id} HTTP/1.1
Host:
Content-Type: application/json
Accept: */*
Content-Length: 49
{
"ipa_code": "123456",
"name": "comune di Bugliano"
}
{
"created_at": "text",
"id": "text",
"ipa_code": "text",
"name": "text",
"updated_at": "text"
}
...
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
No Content
Bad Request
DELETE /tenants/{tenant_id} HTTP/1.1
Host:
Accept: */*
{
"created_at": "text",
"id": "text",
"ipa_code": "text",
"name": "text",
"updated_at": "text"
}
...
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
123456
nome del tenant
comune di Bugliano
OK
Bad Request
PATCH /tenants/{tenant_id} HTTP/1.1
Host:
Content-Type: application/json
Accept: */*
Content-Length: 49
{
"ipa_code": "123456",
"name": "comune di Bugliano"
}
{
"created_at": "text",
"id": "text",
"ipa_code": "text",
"name": "text",
"updated_at": "text"
}
Get available operations for the tenant resource.
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
OK
OPTIONS /tenants/{tenant_id} HTTP/1.1
Host:
Accept: */*
OK
No content
Get Clients Configuration list for specific tenant
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
The limit parameter specifies the maximum number of items to be returned in a single page or request
...
the starting point or the index from which the data should be retrieved
6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
OK
Bad Request
GET /tenants/{tenant_id}/clients HTTP/1.1
Host:
Accept: */*
{
"data": [
{
"created_at": "text",
"env": "text",
"id": "text",
"key_id": "text",
"key_pair_id": "text",
"name": "text",
"public_key": "text",
"tenant_id": "text",
"updated_at": "text"
}
],
"links": {
"next": "text",
"prev": "text",
"self": "text"
},
"meta": {
"page": {
"limit": 1,
"offset": 1,
"sort": "text"
},
"total": 1
}
}
Save client pdnd Configuration of specific tenant
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
ambiente del client
collaudo
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
key id del client
b212c4b4-db26-4404-8c7c-47dab99dd2e6
id interno del materiale critografico
nome del tenant
comune di Bugliano
Created
Bad Request
Forbidden
POST /tenants/{tenant_id}/clients HTTP/1.1
Host:
Content-Type: application/json
Accept: */*
Content-Length: 159
{
"env": "collaudo",
"id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6",
"key_id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6",
"key_pair_id": "text",
"name": "comune di Bugliano"
}
{
"created_at": "text",
"env": "text",
"id": "text",
"key_id": "text",
"key_pair_id": "text",
"name": "text",
"public_key": "text",
"tenant_id": "text",
"updated_at": "text"
}
Get available operations for the Clients resource.
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
The limit parameter specifies the maximum number of items to be returned in a single page or request
...
the starting point or the index from which the data should be retrieved
6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
OK
OPTIONS /tenants/{tenant_id}/clients HTTP/1.1
Host:
Accept: */*
OK
No content
Get client pdnd Configuration of specific tenant
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
OK
Bad Request
Forbidden
GET /tenants/{tenant_id}/clients/{client_id} HTTP/1.1
Host:
Accept: */*
{
"created_at": "text",
"env": "text",
"id": "text",
"key_id": "text",
"key_pair_id": "text",
"name": "text",
"public_key": "text",
"tenant_id": "text",
"updated_at": "text"
}
Update client pdnd Configuration of specific tenant
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
ambiente del client
collaudo
key id del client
b212c4b4-db26-4404-8c7c-47dab99dd2e6
id interno del materiale critografico
nome del tenant
comune di Bugliano
OK
Bad Request
Forbidden
PUT /tenants/{tenant_id}/clients/{client_id} HTTP/1.1
Host:
Content-Type: application/json
Accept: */*
Content-Length: 115
{
"env": "collaudo",
"key_id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6",
"key_pair_id": "text",
"name": "comune di Bugliano"
}
{
"created_at": "text",
"env": "text",
"id": "text",
"key_id": "text",
"key_pair_id": "text",
"name": "text",
"public_key": "text",
"tenant_id": "text",
"updated_at": "text"
}
Delete client pdnd Configuration of specific tenant
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
No Content
Bad Request
Forbidden
DELETE /tenants/{tenant_id}/clients/{client_id} HTTP/1.1
Host:
Accept: */*
No content
Update client pdnd Configuration of specific tenant
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
ambiente del client
collaudo
key id del client
b212c4b4-db26-4404-8c7c-47dab99dd2e6
id interno del materiale critografico
nome del tenant
comune di Bugliano
OK
Bad Request
Forbidden
PATCH /tenants/{tenant_id}/clients/{client_id} HTTP/1.1
Host:
Content-Type: application/json
Accept: */*
Content-Length: 115
{
"env": "collaudo",
"key_id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6",
"key_pair_id": "text",
"name": "comune di Bugliano"
}
{
"created_at": "text",
"env": "text",
"id": "text",
"key_id": "text",
"key_pair_id": "text",
"name": "text",
"public_key": "text",
"tenant_id": "text",
"updated_at": "text"
}
Get available operations for the Clients resource.
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
OK
OPTIONS /tenants/{tenant_id}/clients/{client_id} HTTP/1.1
Host:
Accept: */*
OK
No content
...
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
The limit parameter specifies the maximum number of items to be returned in a single page or request
...
the starting point or the index from which the data should be retrieved
6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
OK
Bad Request
GET /tenants/{tenant_id}/configs HTTP/1.1
Host:
Accept: */*
{
"data": [
{
"call_url": "text",
"client_id": "text",
"created_at": "text",
"eservice_id": "text",
"id": "text",
"is_active": true,
"purpose_id": "text",
"tenant_id": "text",
"updated_at": "text"
}
],
"links": {
"next": "text",
"prev": "text",
"self": "text"
},
"meta": {
"page": {
"limit": 1,
"offset": 1,
"sort": "text"
},
"total": 1
}
}
...
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
false
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
Created
Bad Request
POST /tenants/{tenant_id}/configs HTTP/1.1
Host:
Content-Type: application/json
Accept: */*
Content-Length: 175
{
"client_id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6",
"eservice_id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6",
"is_active": false,
"purpose_id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6"
}
{
"call_url": "text",
"client_id": "text",
"created_at": "text",
"eservice_id": "text",
"id": "text",
"is_active": true,
"purpose_id": "text",
"tenant_id": "text",
"updated_at": "text"
}
Get available operations for the Config resource.
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
Created
OPTIONS /tenants/{tenant_id}/configs HTTP/1.1
Host:
Accept: */*
Created
No content
Get config of specific tenant by id
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
OK
Bad Request
GET /tenants/{tenant_id}/configs/{config_id} HTTP/1.1
Host:
Accept: */*
{
"call_url": "text",
"client_id": "text",
"created_at": "text",
"eservice_id": "text",
"id": "text",
"is_active": true,
"purpose_id": "text",
"tenant_id": "text",
"updated_at": "text"
}
Update config of specific tenant
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
false
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
OK
Bad Request
PUT /tenants/{tenant_id}/configs/{config_id} HTTP/1.1
Host:
Content-Type: application/json
Accept: */*
Content-Length: 175
{
"client_id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6",
"eservice_id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6",
"is_active": false,
"purpose_id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6"
}
{
"call_url": "text",
"client_id": "text",
"created_at": "text",
"eservice_id": "text",
"id": "text",
"is_active": true,
"purpose_id": "text",
"tenant_id": "text",
"updated_at": "text"
}
Soft deleting config of specific tenant
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
No Content
Bad Request
DELETE /tenants/{tenant_id}/configs/{config_id} HTTP/1.1
Host:
Accept: */*
No content
Patch config of specific tenant
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
false
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
OK
Bad Request
PATCH /tenants/{tenant_id}/configs/{config_id} HTTP/1.1
Host:
Content-Type: application/json
Accept: */*
Content-Length: 175
{
"client_id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6",
"eservice_id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6",
"is_active": false,
"purpose_id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6"
}
{
"call_url": "text",
"client_id": "text",
"created_at": "text",
"eservice_id": "text",
"id": "text",
"is_active": true,
"purpose_id": "text",
"tenant_id": "text",
"updated_at": "text"
}
Get available operations for the Config resource.
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
OK
OPTIONS /tenants/{tenant_id}/configs/{config_id} HTTP/1.1
Host:
Accept: */*
OK
No content
Get key unused key for a specific tenant
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
OK
Bad Request
GET /tenants/{tenant_id}/keys HTTP/1.1
Host:
Accept: */*
{
"id": "text",
"public_key": "text"
}
Delete key used by specific tenant
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
...
b212c4b4-db26-4404-8c7c-47dab99dd2e6
No Content
Bad Request
DELETE /tenants/{tenant_id}/keys/{keys_id} HTTP/1.1
Host:
Accept: */*
No content
Get Status
Successful Response
Validation Error
Service Unavailable
GET /registry-proxy/agspr/v1/status HTTP/1.1
Host: api.qa.stanzadelcittadino.it
Accept: */*
{
"type": "text",
"title": "text",
"status": 1,
"detail": "text",
"instance": "text"
}
Create New Tenant Configuration
""
""
Successful Response
Validation Error
POST /registry-proxy/agspr/v1/tenants/ HTTP/1.1
Host: api.qa.stanzadelcittadino.it
Content-Type: application/json
Accept: */*
Content-Length: 143
{
"aoo_code": "text",
"base_url": "text",
"created_at": "",
"description": "text",
"institution_code": "text",
"modified_at": "",
"slug": "text",
"id": "text"
}
No content
Save Service Configuration
""
""
""
""
""
Successful Response
Validation Error
POST /registry-proxy/agspr/v1/services/ HTTP/1.1
Host: api.qa.stanzadelcittadino.it
Content-Type: application/json
Accept: */*
Content-Length: 402
{
"is_active": true,
"ws_url_fascicolo": "text",
"ws_url_protocol": "text",
"username": "text",
"password": "text",
"codente": "text",
"codice_amministrazione": "text",
"email": "text",
"class_cod": "text",
"denominazione": "text",
"id_unit_org": "text",
"codice_aoo": "text",
"dispatch_type": "text",
"tipo_documento": "",
"folder_year": "",
"folder_number": "",
"competence_unit": "",
"creation_unit": "",
"id": "text",
"tenant_id": "text"
}
No content
b212c4b4-db26-4404-8c7c-47dab99dd2e6
Successful Response
Validation Error
GET /payment-proxy/iris/v2/tenants/{tenant_id} HTTP/1.1
Host: api.stanzadelcittadino.it
Accept: */*
{
"tax_identification_number": "01234567891",
"name": "Comune di Bugliano",
"code": "CBugliano",
"e2e_code": "CBugliano",
"receiver_code": "RTIRIS",
"e2e_receiver_code": "RTIRIS",
"receiver_sil_id": "SIL_IRIS_ITR",
"sil_id": "SIL_CBUGLIANO_OPEN",
"password": "PASSWORD",
"cert": "-----BEGIN CERTIFICATE REQUEST-----MIIB9TCCAWACAQAwgbgxGTAXBgNVBAoMEFF1b1ZhZGlzIExpbWl0ZWQxHDAaBgNVBAsME0RvY3VtZW50IERlcGFydG1lbnQxOTA3BgNVBAMMMFdoeSBhcmUgeW91IGRl-----END CERTIFICATE REQUEST-----",
"application_code": "",
"active": true,
"id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6"
}
b212c4b4-db26-4404-8c7c-47dab99dd2e6
""
Successful Response
Validation Error
PUT /payment-proxy/iris/v2/tenants/{tenant_id} HTTP/1.1
Host: api.stanzadelcittadino.it
Content-Type: application/json
Accept: */*
Content-Length: 559
{
"tax_identification_number": "01234567891",
"name": "Comune di Bugliano",
"code": "CBugliano",
"e2e_code": "CBugliano",
"receiver_code": "RTIRIS",
"e2e_receiver_code": "RTIRIS",
"receiver_sil_id": "SIL_IRIS_ITR",
"password": "PASSWORD",
"stamp_code": "CBugliano_SIL_E_BOLLO",
"stamp_password": "PASSWORD",
"sil_id": "SIL_CBUGLIANO_OPEN",
"cert": "-----BEGIN CERTIFICATE REQUEST-----MIIB9TCCAWACAQAwgbgxGTAXBgNVBAoMEFF1b1ZhZGlzIExpbWl0ZWQxHDAaBgNVBAsME0RvY3VtZW50IERlcGFydG1lbnQxOTA3BgNVBAMMMFdoeSBhcmUgeW91IGRl-----END CERTIFICATE REQUEST-----",
"application_code": "",
"active": true
}
No content
b212c4b4-db26-4404-8c7c-47dab99dd2e6
Successful Response
Validation Error
PATCH /payment-proxy/iris/v2/tenants/{tenant_id} HTTP/1.1
Host: api.stanzadelcittadino.it
Content-Type: application/json
Accept: */*
Content-Length: 42
{
"tax_identification_number": "1234567891"
}
No content
""
Successful Response
Validation Error
POST /payment-proxy/iris/v2/tenants HTTP/1.1
Host: api.stanzadelcittadino.it
Content-Type: application/json
Accept: */*
Content-Length: 538
{
"tax_identification_number": "01234567891",
"name": "Comune di Bugliano",
"code": "CBugliano",
"e2e_code": "CBugliano",
"receiver_code": "RTIRIS",
"e2e_receiver_code": "RTIRIS",
"receiver_sil_id": "SIL_IRIS_ITR",
"sil_id": "SIL_CBUGLIANO_OPEN",
"password": "PASSWORD",
"cert": "-----BEGIN CERTIFICATE REQUEST-----MIIB9TCCAWACAQAwgbgxGTAXBgNVBAoMEFF1b1ZhZGlzIExpbWl0ZWQxHDAaBgNVBAsME0RvY3VtZW50IERlcGFydG1lbnQxOTA3BgNVBAMMMFdoeSBhcmUgeW91IGRl-----END CERTIFICATE REQUEST-----",
"application_code": "",
"active": true,
"id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6"
}
No content
Lista id delle configurazioni di pagamento
Successful Response
Validation Error
GET /payment-proxy/iris/v2/configs HTTP/1.1
Host: api.stanzadelcittadino.it
Accept: */*
[
{
"tenant_id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6",
"payment_type": "pagopa",
"code": "CONTR_PRTPM",
"description": "Abbonamento aree di sosta",
"active": true,
"split": [
{
"split_id": "c_1",
"split_type": "Tipo c1",
"split_code": "Codice c1",
"split_description": "Descrizione c1",
"split_amount": 0.5,
"split_budget_chapter": "Capitolo di bilancio c1",
"split_assessment": "Accertamento c1"
}
],
"amount": 1,
"reason": "test",
"expire_at": 180,
"collection_data": "9/0101100IM/",
"office_code": "FSE",
"office_description": "Fondo Sociale Europeo",
"reference_code": "BOLLO_TIROCINI",
"notes": "",
"remote_collection": {
"id": "23d57b65-5eb9-4f0a-a507-fbcf3057b248",
"type": "service"
}
}
]
pagopa
Successful Response
Validation Error
POST /payment-proxy/iris/v2/configs HTTP/1.1
Host: api.stanzadelcittadino.it
Content-Type: application/json
Accept: */*
Content-Length: 770
{
"id": "3e68161e-9abb-43ac-99e6-39b0ece65b40",
"tenant_id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6",
"payment_type": "pagopa",
"code": "CONTR_PRTPM",
"description": "Abbonamento aree di sosta",
"active": true,
"split": [
{
"split_id": "c_1",
"split_type": "Tipo c1",
"split_code": "Codice c1",
"split_description": "Descrizione c1",
"split_amount": 0.5,
"split_budget_chapter": "Capitolo di bilancio c1",
"split_assessment": "Accertamento c1"
}
],
"amount": 1,
"reason": "test",
"expire_at": 10,
"collection_data": "9/0101100IM/",
"office_code": "FSE",
"office_description": "Fondo Sociale Europeo",
"reference_code": "BOLLO_TIROCINI",
"notes": "",
"remote_collection": {
"id": "3e68161e-9abb-43ac-99e6-39b0ece65b40",
"type": "service"
},
"receiver": {
"tax_identification_number": "06363391001",
"name": "Agenzia delle Entrate"
}
}
No content
23d57b65-5eb9-4f0a-a507-fbcf3057b248
Successful Response
Validation Error
GET /payment-proxy/iris/v2/configs/{config_id} HTTP/1.1
Host: api.stanzadelcittadino.it
Accept: */*
{
"tenant_id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6",
"payment_type": "pagopa",
"code": "CONTR_PRTPM",
"description": "Abbonamento aree di sosta",
"active": true,
"split": [
{
"split_id": "c_1",
"split_type": "Tipo c1",
"split_code": "Codice c1",
"split_description": "Descrizione c1",
"split_amount": 0.5,
"split_budget_chapter": "Capitolo di bilancio c1",
"split_assessment": "Accertamento c1"
}
],
"amount": 1,
"reason": "test",
"expire_at": 180,
"collection_data": "9/0101100IM/",
"office_code": "FSE",
"office_description": "Fondo Sociale Europeo",
"reference_code": "BOLLO_TIROCINI",
"notes": "",
"remote_collection": {
"id": "23d57b65-5eb9-4f0a-a507-fbcf3057b248",
"type": "service"
}
}
23d57b65-5eb9-4f0a-a507-fbcf3057b248
pagopa
Successful Response
Validation Error
PUT /payment-proxy/iris/v2/configs/{config_id} HTTP/1.1
Host: api.stanzadelcittadino.it
Content-Type: application/json
Accept: */*
Content-Length: 727
{
"tenant_id": "b212c4b4-db26-4404-8c7c-47dab99dd2e6",
"payment_type": "pagopa",
"code": "CONTR_PRTPM",
"description": "Abbonamento aree di sosta",
"active": true,
"split": [
{
"split_id": "c_1",
"split_type": "Tipo c1",
"split_code": "Codice c1",
"split_description": "Descrizione c1",
"split_amount": 0.5,
"split_budget_chapter": "Capitolo di bilancio c1",
"split_assessment": "Accertamento c1"
}
],
"amount": 1,
"reason": "test",
"expire_at": 180,
"collection_data": "9/0101100IM/",
"office_code": "FSE",
"office_description": "Fondo Sociale Europeo",
"reference_code": "BOLLO_TIROCINI",
"notes": "",
"remote_collection": {
"id": "3e68161e-9abb-43ac-99e6-39b0ece65b40",
"type": "service"
},
"receiver": {
"tax_identification_number": "06363391001",
"name": "Agenzia delle Entrate"
}
}
No content
23d57b65-5eb9-4f0a-a507-fbcf3057b248
Successful Response
Validation Error
PATCH /payment-proxy/iris/v2/configs/{config_id} HTTP/1.1
Host: api.stanzadelcittadino.it
Content-Type: application/json
Accept: */*
Content-Length: 589
{
"payment_type": "pagopa",
"code": "CONTR_PRTPM",
"description": "Abbonamento aree di sosta",
"active": true,
"split": [
{
"split_id": "c_1",
"split_type": "Tipo c1",
"split_code": "Codice c1",
"split_description": "Descrizione c1",
"split_amount": 0.5,
"split_budget_chapter": "Capitolo di bilancio c1",
"split_assessment": "Accertamento c1"
}
],
"amount": 1,
"reason": "test",
"expire_at": 10,
"collection_data": "9/0101100IM/",
"office_code": "FSE",
"office_description": "Fondo Sociale Europeo",
"reference_code": "BOLLO_TIROCINI",
"notes": "",
"remote_collection": {
"id": "3e68161e-9abb-43ac-99e6-39b0ece65b40",
"type": "service"
}
}
No content
NOTA 1: per gli sviluppatori python è stata definita una libreria a cui fare riferimento per la validazione del nuovo evento, è consultabile qui.
NOTA 2: per gli sviluppatori python, il repository da cui prendere spunto per applicare le modifiche riportate in questa sezione e in quelle successive è consultabile qui.
Con la versione 2.0 aggiornata del pagamento sono stati aggiunti nuovi campi che arricchiscono la descrizione del pagamento, in particolare i campi sono:
type
: serve a distinguere tra un pagamento ordinario e una marca da bollo digitale, può avere PAGOPA
o STAMP
come valori
receiver
: definisce il beneficiario del pagamento, non è un campo obbligatorio ma può essere utilizzato nel caso di pagamenti con beneficiario differente dall'ente su cui viene creato il pagamento. Se viene valorizzato è obbligatorio valorizzare i campi receiver.tax_identification_number
e receiver.name
due_type
: indica la categorizzazione del dovuto sull'intermediario di pagamento, e viene valorizzato dalla configurazione del pagamento
pagopa_category
: indica la categorizzazione del dovuto su pagoPA, e viene valorizzato dalla configurazione del pagamento
document
: è un campo che serve a descrivere il documento su cui viene apposta la marca da bollo digitale, se viene valorizzato è obbligatorio valorizzare il campo document.hash
.
split
: con la versione 2.0 dell'evento, questo campo, che viene alimentato dal bilancio contenuto nella configurazione del pagamento, avrà un tipo ben definito, composto da
split.code
: indica l'identificativo della voce di bilancio
split.amount
: indica l'importo della voce di bilancio
split.meta
: oggetto contenente il resto dei campi presenti nella voce di bilancio
links.confirm
: è un campo che serve a contenere l'API per forzare il completamento di un pagamento, per il momento tutti i proxy mancano di questa API, ma il campo è stato aggiunto per utilizzi futuri.
links.cancel
: è un campo che serve a contenere l'API per forzare l'annullamento di un pagamento.
debtor
: in aggiunta al payer
è stato aggiunto un campo equivalente debtor
, e serve a distinguere tra il debitore e il pagatore di un pagamento, ad esempio è possibile che un cittadino paghi per conto di terzi e in questo caso e debitore e pagatore sono distinti. Per ora non viene valorizzato né utilizzato.
Nota importante
Alcuni dei campi che vengono popolati in fase di creazione del pagamento sull'intermediario si leggevano solo dall'evento payment. Con l'upgrade delle API (i cui aggiornamenti sono consultabili qui), questi campi potrebbero non essere valorizzati nell'evento, in tal caso, vanno letti invece dalla configurazione inserita via API. I campi in questione sono:
payment.reason
payment.payment.amount
payment.payment.expire_at
payment.payment.receiver
In sintesi quindi: se questi campi sono valorizzati nell'evento payment, allora li si legge da lì, altrimenti li si legge dalla configurazione inserita via API.
{
"id": "68dada78-2398-4a11-b80a-98aaede3371c",
"user_id": "5b00796e-ef10-4c98-b9f5-b0f7ffd7a17d",
"type": "PAGOPA",
"tenant_id": "60e35f02-1509-408c-b101-3b1a28109329",
"service_id": "b21c4429-95e4-45d5-930f-44eb74136625",
"created_at": "2023-06-06T11:59:11+02:00",
"updated_at": "2023-06-12T00:10:16+02:00",
"status": "PAYMENT_STARTED",
"reason": "79501b2a-c9ad-41f8-a9e7-a885f2d570a2 - BNRMHL75C06G702B",
"remote_id": "79501b2a-c9ad-41f8-a9e7-a885f2d570a2",
"payment": {
"type": "PAGOPA",
"transaction_id": null,
"paid_at": null,
"expire_at": "2023-09-04T11:59:05+02:00",
"amount": 1.34,
"currency": "EUR",
"notice_code": "001550000000024427",
"iud": "68dada7823984a11b80a98aaede3371c",
"iuv": "550000000024427",
"receiver": {
"tax_identification_number": "777777777",
"name": "Comune di BlaBla",
"iban": "IT60X0542811101000000123456",
"address": "Via Del Campo",
"building_number": "12",
"postal_code": "38022",
"town_name": "Roma",
"country_subdivision": "RM",
"country": "IT",
},
"due_type": "TARI",
"pagopa_category": "9/12129/TS",
"document": null,
"split": [
{
"code": "c_1",
"amount": 1.00,
"meta": {}
},
{
"code": "c_2",
"amount": 0.34,
"meta": {}
}
]
},
"links": {
"online_payment_begin": {
"url": "https://iris-proxy-qa.boat.opencontent.io/online-payment/68dada78-2398-4a11-b80a-98aaede3371c?gw=https://iristest.rete.toscana.it/gateway/PaymentAuthentication?token=1686045553436001U132&expire_at=2023-06-06T12:13:13",
"last_opened_at": "2023-06-06T11:59:26+02:00",
"method": "GET"
},
"online_payment_landing": {
"url": "https://servizi.comune-qa.bugliano.pi.it/lang/it/pratiche/79501b2a-c9ad-41f8-a9e7-a885f2d570a2/detail",
"last_opened_at": "2023-06-06T11:59:57+02:00",
"method": "GET"
},
"offline_payment": {
"url": "https://iris-proxy-qa.boat.opencontent.io/notice/68dada78-2398-4a11-b80a-98aaede3371c",
"last_opened_at": "2023-06-06T11:59:20+02:00",
"method": "GET"
},
"receipt": {
"url": null,
"last_opened_at": null,
"method": "GET"
},
"notify": [
{
"url": "https://servizi.comune-qa.bugliano.pi.it/lang/api/applications/79501b2a-c9ad-41f8-a9e7-a885f2d570a2/payment",
"method": "POST",
"sent_at": null
}
],
"update": {
"url": "http://iris-proxy-qa.boat-backplane.opencontent.io/update/68dada78-2398-4a11-b80a-98aaede3371c",
"last_check_at": "2023-06-12T00:10:16+02:00",
"next_check_at": "2023-06-12T01:10:16+02:00",
"method": "GET"
},
"confirm": {
"url": null,
"last_opened_at": null,
"method": "PATCH"
},
"cancel": {
"url": "https://api.qa.stanzadelcittadino.it/payment-proxy/mypay-trentino/payments/5f2c4be0-effb-47a9-a6dc-a972043580f3",
"last_opened_at": null,
"method": "PATCH"
}
},
"payer": {
"type": "human",
"tax_identification_number": "BNRMHL75C06G702B",
"name": "Michelangelo",
"family_name": "Buonarroti",
"street_name": "Cesare Battisti",
"building_number": "",
"postal_code": "38010",
"town_name": "Bugliano",
"country_subdivision": "PI",
"country": "IT",
"email": "[email protected]"
},
"debtor": {
"type": "human",
"tax_identification_number": "BNRMHL75C06G702B",
"name": "Michelangelo",
"family_name": "Buonarroti",
"street_name": "Cesare Battisti",
"building_number": "",
"postal_code": "38010",
"town_name": "Bugliano",
"country_subdivision": "PI",
"country": "IT",
"email": "[email protected]"
},
"event_id": "e0b25e33-9d38-4781-823a-ed04753aea01",
"event_version": "2.0",
"event_created_at": "2023-06-12T00:10:16+02:00",
"app_id": "iris-payment-proxy-qa:1.2.8"
}
id
UUID
user_id
UUID
type
string(50)
Ogni proxy deve implementare un tipo di pagamento. Se per esempio, il proxy in questione gestisce pagamenti pagopa allora il campo va validato verificando che esso sia valorizzato a PAGOPA
. In caso contrario si scarta l'evento e si emette un log di errore.
tenant_id
UUID
service_id
UUID
created_at
Datetime
ISO8601
updated_at
Datetime
ISO8601
status
Enum
Valori permessi: CREATION_PENDING CREATION_FAILED PAYMENT_PENDING PAYMENT_STARTED PAYMENT_CONFIRMED PAYMENT_FAILED NOTIFICATION_PENDING
COMPLETE EXPIRED
reason
string(140)
Causale del pagamento. Non può eccedere i 140 caratteri.
remote_id
UUID
payment
PaymentData
links
Links
payer
Payer
Soggetto Versante
debtor
Debtor
Soggetto Pagatore
event_id
UUID
event_version
string(10)
L'attuale versione dell'evento deve essere "1.0". Ogni proxy deve validare la versione dell'evento in modo da sapere se processarlo o meno.
event_created_at
Datetime
ISO8601
app_id
string(100)
Valorizzarlo nel formato <nome-proxy>:<versione-proxy>
Es.
iris-payment-proxy:1.3.0
type
Enum
Valori permessi:
PAGOPA
nel caso in cui si tratto di un dovuto standard
STAMP
nel caso in cui si tratta di un dovuto di tipo marca da bollo digitale
transaction_id
string(255)
Fornito dall'intermediario di pagamento. La lunghezza dunque può essere variabile
paid_at
Datetime
ISO8601
expire_at
Datetime
ISO8601 E' già valorizzato a priori, quindi non viene gestito dal proxy
amount
float
E' già valorizzato a priori, quindi non viene gestito dal proxy
reason
string(140)
Causale del pagamento. Non può eccedere i 140 caratteri.
currency
string(3)
ISO4217
notice_code
string(50)
iud
string(50)
è il valore del campo id
del pagamento in formato esadecimale
Es.
id: 4a263efb-300b-437a-ab50-116fa9aff8a7
iud: 4a263efb30
0b-437aab50116fa9aff8a7
iuv
string(50)
receiver
Receiver
Destinatario o beneficiario del versamento/dovuto
due_type
string(256)
Tipo di dovuto secondo la classificazione dell'intermediario di riferiemento
pagopa_category
string(12)
Tipo di dovuto secondo la ufficiale di pagoPA
document
Document
Contiene le informazioni riguardanti il documento informatico o la segnatura di protocollo cui è associata la marca da bollo digitale
split
List[PaymentDataSplit]
tax_identification_number
string(255)
name
string(255)
iban
string(34)
ISO 13616
street_name
string(255)
building_number
string(255)
postal_code
string(255)
town_name
string(255)
country_subdivision
string(2)
ISO 3166-2
country
string(2)
ISO 3166-1 alpha-2
id
UUID
Identificativo del documento
ref
string
Url per scaricare il documento
hash
string
Hash digest del documento informatico
code
string(50)
amount
float
meta
json
online_payment_begin
UrlData
online_payment_landing
UrlData
offline_payment
UrlData
receipt
UrlData
notify
List[Notify]
update
Update
confirm
UrlData
cancel
UrlData
url
string
last_opened_at
Datetime
ISO8601
method
Enum
Valori permessi:
GET
POST
PUT
PATCH
DELETE
url
string
method
Enum
Valori permessi: GET POST
sent_at
Datetime
ISO8601
url
string
last_check_at
Datetime
ISO8601
next_check_at
Datetime
ISO8601
method
Enum
Valori permessi: GET POST
type
Enum
Valori permessi: human legal
tax_identification_number
string(255)
name
string(255)
family_name
string(255)
street_name
string(255)
building_number
string(255)
postal_code
string(255)
town_name
string(255)
country_subdivision
string(2)
ISO 3166-2
country
string(2)
ISO 3166-1 alpha-2
email
string(255)
type
Enum
Valori permessi: human legal
tax_identification_number
string(255)
name
string(255)
family_name
string(255)
street_name
string(255)
building_number
string(255)
postal_code
string(255)
town_name
string(255)
country_subdivision
string(2)
ISO 3166-2
country
string(2)
ISO 3166-1 alpha-2
email
string(255)
Punti aperti
Determinazione delle URL di ritorno: Come si gestiscono le URL di ritorno? È possibile utilizzare l'indirizzo del proxy per gestire i redirect?
Gestione dell'happy path del pagamento: Come gestire il percorso ideale del pagamento e cosa accade se l'utente chiude la pagina di pagamento e poi ritorna?
Compilazione e invio pratica: L'utente compila la pratica per il servizio e invia la richiesta al Core.
Verifica pagamenti: Il Core verifica se, nella fase attuale della pratica, è necessario effettuare dei pagamenti (es. pagamento anticipato o posticipato).
Creazione pagamenti: Il Core invia i dettagli al servizio di pagamento, che restituisce gli identificativi dei pagamenti creati. Questo passaggio è fondamentale, poiché il Core deve salvare i dettagli dei pagamenti all'interno della pratica, assicurandosi che siano coerenti con il flusso (anticipato o posticipato).
Restituzione della pratica: Una volta ottenuti gli ID dei pagamenti, il Core restituisce all'utente l'intera pratica aggiornata con gli identificativi.
Gestione degli errori nella creazione dei pagamenti:
Cosa accade se la creazione di un pagamento fallisce?
Il personale dell'ufficio preposto dovrebbe essere notificato in caso di fallimento.
Stato "Creation Pending": Quando un pagamento viene creato, si ottiene comunque un ID associato al pagamento, ma questo potrebbe essere in stato di "creation pending".
Politiche di retry: Si potrebbe considerare l'implementazione di politiche di retry, ma è necessario consultare il Product Manager (PM) per decidere la miglior esperienza utente (UX).
Ipotesi: Al secondo o terzo tentativo di retry (o dopo un timeout di 10 secondi), potremmo mostrare un messaggio d'errore all'utente, suggerendo di riprovare o di avvisare l'assistenza (es. con un link "Clicca qui e avvisa"). Questo sarebbe un errore a livello ERROR.
Errore di configurazione: Se si verifica un errore di configurazione errata, è possibile fare affidamento sui messaggi restituiti dall'intermediario. In questo caso, si può evitare il retry e notificare direttamente il responsabile (es. Lorello). Questo tipo di errore potrebbe essere classificato come FATAL.
Il sequence diagram sopra riportato rappresenta il flusso di creazione e gestione dei pagamenti in un'applicazione, con particolare attenzione al retry in caso di fallimento e alla gestione dello stato del pagamento. Il flusso si svolge come segue:
Inizio del processo: L'utente invia la pratica e il Core richiama il Payment Dispatcher API per creare il pagamento. Se la creazione ha successo, si procede, altrimenti si ritenta fino a 3 volte.
Attesa di aggiornamenti dal Read Model: Quando la creazione del pagamento va a buon fine, il Core si mette in attesa di aggiornamenti dal Read Model.
Retry gestito dal Retry Orchestrator: Se il Read Model restituisce lo stato CREATION_FAILED
, il Core delega il processo di retry al Retry Orchestrator, che gestisce i retry tramite Kafka. Il Retry Orchestrator tenterà di risolvere il problema creando il pagamento sull'intermediario esterno.
Risultati del Retry Orchestrator:
Se il retry riesce, il Retry Orchestrator segnalerà il successo al Core, che mostrerà all'utente lo stato PAYMENT_PENDING
per procedere con il pagamento.
Se i retry falliscono, l'utente riceverà un alert di errore.
Conclusione: L'utente vedrà le informazioni di pagamento in caso di successo o un messaggio d'errore in caso di fallimento, dopo aver superato il numero massimo di tentativi.
Gestione di pagamenti parziali: Se due pagamenti vanno a buon fine e uno fallisce, come dovrebbe comportarsi il frontend? Attualmente, il processo di checkout potrebbe risultare incompleto.
Invio degli ID di pagamento: Durante il checkout, il sistema riceve tutti gli ID dei pagamenti che devono essere completati in un’unica operazione.
Il sequence diagram sopra riportato rappresenta il flusso di creazione e gestione dei pagamenti posticipati in fase approvazione della pratica da parte dell'operatore. Il flusso si svolge come segue:
Creazione delle configurazioni: L'utente modifica e invia le configurazioni, che vengono temporaneamente salvate tramite il Payment Proxy. Se fallisce, l'errore viene mostrato all'utente.
Creazione del pagamento tramite Payment Dispatcher: Il Core invia la richiesta di creazione pagamento al Payment Dispatcher API. Se fallisce, viene avviato un retry per un massimo di 3 tentativi. Se dopo 3 tentativi non riesce, viene mostrato un errore all'utente.
Attesa di aggiornamenti dal Read Model: Quando la creazione del pagamento va a buon fine, il Core si mette in attesa di aggiornamenti dal Read Model, che monitora lo stato del pagamento.
Stato CREATION_FAILED
e Retry Orchestrator:
Se il Read Model restituisce lo stato CREATION_FAILED
, il Core delega il retry al Retry Orchestrator, che gestisce i retry attraverso i topic di Kafka.
Se il Retry Orchestrator completa con successo la creazione, il Read Model aggiorna lo stato del pagamento, e l'utente può procedere con il pagamento (PAYMENT_PENDING
).
Se anche il Retry Orchestrator fallisce dopo vari tentativi, viene restituito un errore al Core e un alert viene mostrato all'utente.
Output finale: Se il pagamento va a buon fine (PAYMENT_PENDING
), l'utente riceve le informazioni per procedere con il pagamento. In caso di errore persistente, viene mostrato un alert.
Descrizione in dettaglio di un documento generato dalla piattaforma
Secondo l'AGID, il documento informatico è la “rappresentazione informatica di atti, fatti o dati giuridicamente rilevanti" in contrapposizione al documento analogico ("rappresentazione non informatica di atti, fatti o dati giuridicamente rilevanti")".
La piattaforma genera documenti informatici a partire da diversi tipi di servizi offerti.
Il documento informatico ci permette di avere un'unica struttura dati a partire da diverse tipologie di servizi. Questo ci dà il vantaggio di dover gestire un’unica entità durante il processo di protocollazione.
Il documento generato dall’area personale del cittadino è stato progettato seguendo le linee guida suggerite dall’AGID. Esso è composto dai seguenti elementi:
Documento principale: descrive in dettaglio il servizio o l'oggetto a cui il documento si riferisce.
Allegati del documento principale: rappresentano l'insieme dei file che accompagnano la documento principale, fornendo ulteriori dettagli, dati o documentazione di supporto.
metadati: includono informazioni utili per la comprensione del contesto del documento informatico. Ci forniscono dettagli sulle informazioni di base, come la data di creazione, il mittente, il destinatario, e altri dettagli pertinenti. Alcuni di essi non possono mancare(vedi sezione seguente).
Nella nostra piattaforma il documento è implementato in forma di evento Kafka, in formato JSON, descritto dal seguente JSON SCHEMA.
Per una migliore visualizzazione si consiglia di usare un viewer online.
{
"$schema": "https://json-schema.org/draft/2020-12/schema#",
"$id": "https://schema.opencontent.io/document.schema#",
"type": "object",
"title": "Documento",
"definitions": {
"registration_info": {
"type": "object",
"title": "Informazioni sul protocollo del documento",
"properties": {
"transmission_type": {
"title": "Tipo di trasmissione",
"type": "string",
"enum": [
"inbound",
"outbound",
"internal"
],
"examples": [
"inbound"
]
},
"date": {
"title": "Data protocollo",
"type": "string",
"format": "date-time",
"examples": [
"2022-08-24T11:59:57+02:00"
]
},
"document_number": {
"title": "Numero di protocollo",
"description": "Numero di protocollo assegnato dal sistema di protocollazione",
"type": "string",
"maxLength": 255,
"examples": [
"Prot-123-456-789",
"123"
]
}
},
"required": [
"transmission_type",
"date",
"document_number"
]
},
"folder_info": {
"type": "object",
"title": "Informazioni sul fascicolo",
"properties": {
"title": {
"title": "Oggetto del Fascicolo",
"type": [
"string",
"null"
],
"maxLength": 255
},
"id": {
"title": "Numero di Fascicolo",
"type": [
"string",
"null"
],
"maxLength": 255
}
},
"required": [
"title",
"id"
]
},
"file": {
"title": "File",
"type": "object",
"properties": {
"name": {
"title": "Titolo del file",
"type": "string"
},
"description": {
"title": "Descrizione del file",
"type": [
"string",
"null"
]
},
"mime_type": {
"title": "Mime type del file",
"type": "string"
},
"url": {
"title": "Download url della risorsa",
"type": "string",
"format": "uri"
},
"md5": {
"title": "Hash md5 del binario",
"type": "string"
},
"filename": {
"title": "Nome completo del file con estensione",
"type": "string"
},
"internal_url": {
"title": "Eventuale url interno della risorsa a disposizione dei sistemi dell'ente che sarà utilizzato per servire il binario se questo non viene salvato nello storage della piattaforma",
"type": [
"string",
"null"
],
"format": "uri"
}
},
"required": [
"name",
"filename",
"description",
"mime_type",
"url",
"md5"
]
},
"image": {
"type": "object",
"title": "Risorsa immagine",
"properties": {
"name": {
"title": "Titolo dell'immagine",
"type": "string"
},
"filename": {
"title": "Nome dell'immagine",
"type": "string"
},
"description": {
"title": "Descrizione dell'immagine",
"type": "string"
},
"license": {
"title": "Licenza di utilizzo",
"type": "string",
"enum": [
"?",
"??",
"???"
]
},
"type": {
"title": "Tipologia dell'immagine o mime-type",
"type": "string"
},
"url": {
"title": "Url della risorsa",
"type": "string",
"format": "uri"
}
},
"required": [
"name",
"filename",
"license",
"type",
"url",
"description"
]
},
"author": {
"type": "object",
"title": "Autore",
"properties": {
"type": {
"type": "string",
"enum": [
"human",
"legal"
]
},
"tax_identification_number": {
"type": "string"
},
"name": {
"type": "string"
},
"family_name": {
"type": [
"string",
"null"
]
},
"street_name": {
"type": [
"string",
"null"
]
},
"building_number": {
"type": [
"string",
"null"
]
},
"postal_code": {
"type": [
"string",
"null"
]
},
"town_name": {
"type": [
"string",
"null"
]
},
"country_subdivision": {
"type": [
"string",
"null"
]
},
"country": {
"type": [
"string",
"null"
]
},
"email": {
"type": "string",
"format": "email"
}
},
"required": [
"type",
"tax_identification_number",
"name",
"family_name",
"street_name",
"building_number",
"postal_code",
"town_name",
"country_subdivision",
"country",
"email",
"role"
]
}
},
"properties": {
"title": {
"type": "string",
"maxLength": 255,
"title": "Titolo del documento",
"description": "Nome del documento. Il nome del Documento deve essere facilmente comprensibile dai cittadini. Vincoli: massimo 160 caratteri spazi inclusi",
"examples": [
"Istanza di certificato anagrafico"
]
},
"id": {
"type": "string",
"format": "uuid",
"title": "Identificativo interno del documento",
"description": "Identificatore uuid ad uso interno",
"examples": [
"4a68415b-e1a8-4f3a-a229-3f6678fd81d1"
]
},
"app_id": {
"title": "App ID",
"type": "string",
"examples": [
"document-dispatcher:1.1.1"
]
},
"event_created_at": {
"title": "Data di creazione dell'evento",
"type": "string",
"format": "date-time",
"examples": [
"2023-11-23T14:14:23+00:00"
]
},
"event_id": {
"title": "ID dell'evento",
"type": "string",
"format": "uuid"
},
"event_version": {
"type": "integer",
"title": "Versione del documento",
"description": "Versione del documento",
"default": 1,
"examples": [
1
]
},
"external_id": {
"type": [
"string",
"null"
],
"title": "Identificativo del documento ad uso dell'ente",
"description": "Un numero identificativo del documento (es DOI, ISBN)",
"examples": [
"Cert567-2023",
"4a68415b-e1a8-4f3a-a229-3f6678fd81d1"
]
},
"registration_data": {
"title": "Protocollo",
"anyOf": [
{
"title": "Informazioni sul protocollo del documento",
"$ref": "#/definitions/registration_info"
},
{
"title": "Nessuna informazione sul protocollo",
"type": "null"
}
]
},
"folder": {
"title": "Fascicolo",
"anyOf": [
{
"title": "Informazioni sul fascicolo",
"$ref": "#/definitions/folder_info"
},
{
"title": "Nessuna informazione sul fascicolo",
"type": "null"
}
]
},
"status": {
"title": "Stato del documento",
"description": "Stato del documento: descrive lo stato di protocollazione del documento",
"type": "string",
"enum": [
"DOCUMENT_CREATED",
"REGISTRATION_PENDING",
"REGISTRATION_FAILED",
"PARTIAL_REGISTRATION",
"REGISTRATION_COMPLETE"
]
},
"type": {
"title": "Tipo di documento",
"description": "Tipologia del documento: viene utilizzata una tassonomia custom che va intesa come sotto tassonomia di Istanza https://schema.gov.it/lodview/controlled-vocabulary/classifications-for-documents/government-documents-types/8",
"type": "string",
"enum": [
"application-request",
"integration-request",
"integration-response",
"application-outcome",
"application-withdraw",
"application-revocation"
]
},
"remote_id": {
"title": "Pratica associata",
"description": "Identificativo della pratica associata",
"type": [
"string",
"null"
],
"format": "uuid",
"examples": [
"842b37e7-d835-49f0-92e2-a393ecda2e53"
]
},
"remote_collection": {
"title": "Servizio collegato",
"anyOf": [
{
"title": "Servizio collegato",
"description": "Se il documento non è collegato a una pratica è possibile indicare il servizio che ha generato il documento",
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"examples": [
"842b37e7-12a4-49f0-92e2-a393ecda2e53"
]
},
"type": {
"type": "string",
"enum": [
"service",
"service-group",
"subscription",
"calendar"
],
"examples": [
"service"
]
}
},
"required": [
"id",
"type"
]
},
{
"title": "Nessuna servizio collegato",
"type": "null"
}
]
},
"topics": {
"title": "Argomenti",
"description": "Argomenti collegabili",
"type": "array",
"minItems": 0,
"items": {
"title": "Argomento",
"type": "string",
"minItems": 0
}
},
"short_description": {
"title": "Descrizione breve",
"description": "Descrizione sintetica del documento (max 255 caratteri) utilizzando un linguaggio semplice che possa aiutare qualsiasi utente a identificare con chiarezza il documento. Non utilizzare un linguaggio ricco di riferimenti normativi",
"type": "string",
"maxLength": 255
},
"description": {
"title": "Descrizione",
"description": "L'oggetto del documento, utilizzando un linguaggio semplice che possa aiutare qualsiasi utente a identificare con chierazza il documento. Non utilizzare un linguaggio ricco di riferimenti normativi.",
"type": [
"string",
"null"
]
},
"main_document": {
"title": "URL documento",
"$ref": "#/definitions/file"
},
"image_gallery": {
"title": "Galleria di immagini",
"type": "array",
"items": {
"$ref": "#/definitions/image"
},
"minItems": 0
},
"has_organization": {
"title": "Ufficio responsabile del documento",
"anyOf": [
{
"title": "Link alla scheda dell'ufficio responsabile del documento",
"type": "string",
"format": "uri",
"examples": [
"https://example.com/offices/1"
]
},
{
"title": "Nessuna informazione sull'ufficio",
"type": "null"
}
]
},
"attachments": {
"title": "Allegati",
"description": "Altre risorse oltre alla principale",
"type": "array",
"minItems": 0,
"items": {
"$ref": "#/definitions/file"
}
},
"distribution_license_id": {
"title": "Licenza di distribuzione",
"type": [
"string",
"null"
]
},
"related_public_services": {
"title": "Servizi collegati",
"description": "Servizi che usano questo documento come input",
"type": "array",
"items": {
"type": "object",
"title": "Servizio collegato",
"properties": {
"name": {
"id": "Id del servizio",
"type": "string"
},
"type": {
"title": "Tipologia del servizio",
"type": "string"
},
"url": {
"title": "Descrizione del file",
"type": "string",
"format": "uri"
}
},
"required": [
"id",
"type",
"url"
]
},
"minItems": 0
},
"valid_from": {
"title": "Data inizio validità",
"description": "Data da cui il documento è valido",
"type": [
"string",
"null"
],
"format": "date-time",
"examples": [
"2022-08-24T11:59:57+02:00"
]
},
"valid_to": {
"title": "Data fine validità",
"description": "Data da cui il documento non è più valido",
"type": [
"string",
"null"
],
"format": "date-time",
"examples": [
"2022-08-24T11:59:57+02:00"
]
},
"removed_at": {
"title": "Data ultima disponibilità",
"description": "Data fino alla quale il documento sarà disponibile online",
"type": [
"string",
"null"
],
"format": "date-time",
"examples": [
"2022-08-24T11:59:57+02:00"
]
},
"expire_at": {
"title": "Data scadenza",
"description": "Solitamente uguale a fine validità, è la data entro la quale il documento deve essere eventualmente rinnovato",
"type": [
"string",
"null"
],
"format": "date-time",
"examples": [
"2022-08-24T11:59:57+02:00"
]
},
"more_info": {
"title": "Ulteriori informazioni",
"description": "Ulteriori informazioni sul documento",
"type": [
"string",
"null"
]
},
"normative_requirements": {
"title": "Riferimenti normativi",
"description": "Lista di link con riferimenti normativi utili per il documento",
"type": "array",
"minItems": 0,
"items": {
"type": "string",
"format": "uri"
}
},
"related_documents": {
"title": "Documenti collegati",
"description": "Lista di documenti allegati: link a quelli strutturati a loro volta come documenti",
"type": "array",
"minItems": 0,
"items": {
"type": "string",
"format": "uri"
}
},
"life_events": {
"title": "Life Events",
"description": "Life Events collegabili",
"type": "array",
"minItems": 0,
"items": {
"type": "string"
}
},
"business_events": {
"title": "Business Events",
"description": "Business Events collegabili",
"type": "array",
"minItems": 0,
"items": {
"type": "string"
}
},
"allowed_readers": {
"title": "Lista di identificatori di persone che possono accedere in lettura al documento",
"description": "??? Vedi onotologia...",
"type": "array",
"minItems": 0,
"items": {
"type": "string"
}
},
"tenant_id": {
"title": "Ente",
"type": "string",
"format": "uuid",
"description": "Uuid del tenant",
"examples": [
"842b37e7-d835-49f0-92e2-a393ecda2e53"
]
},
"owner_id": {
"title": "Proprietario",
"description": "Uuid del proprietario del documento",
"type": "string",
"format": "uuid"
},
"document_url": {
"title": "Url pubblico del documento",
"type": [
"string",
"null"
],
"format": "uri"
},
"created_at": {
"title": "Data di creazione",
"type": "string",
"format": "date-time",
"examples": [
"2022-08-24T11:59:57+02:00"
]
},
"updated_at": {
"title": "Data ultima modifica",
"type": "string",
"format": "date-time",
"examples": [
"2022-08-24T11:59:57+02:00"
]
},
"author": {
"title": "Autore/i",
"description": "Persone che hanno redatto il documento",
"type": "array",
"minItems": 0,
"items": {
"$ref": "#/definitions/author"
}
},
"source_type": {
"title": "Tipologia del soggetto che ha generato il documento",
"description": "In base a questo che si identifica la transmission type",
"type": "string",
"enum": [
"tenant",
"user"
]
},
"recipient_type": {
"title": "Tipologia di destinatario del documento",
"description": "In base a questo che si identifica la transmission type",
"type": "string",
"enum": [
"tenant",
"user"
]
},
"last_seen": {
"title": "Data ultima visualizzazione da parte del destinatario",
"type": [
"string",
"null"
],
"format": "date-time",
"examples": [
"2022-08-24T11:59:57+02:00"
]
}
},
"required": [
"title",
"id",
"event_version",
"app_id",
"status",
"event_created_at",
"event_id",
"type",
"short_description",
"main_document",
"tenant_id",
"owner_id",
"created_at",
"updated_at",
"source_type",
"recipient_type"
]
}
title
String
✅
MaxLength: 255, descrizione chiara del documento, massimo 160 caratteri spazi inclusi
id
String (UUID)
✅
Formato: UUID, identificativo univoco
app_id
String
✅
-
event_created_at
String (DateTime)
✅
Formato: DateTime
event_id
String (UUID)
✅
Formato: UUID
event_version
Integer
✅
Valore di default: 1
external_id
String, Null
❌
-
registration_data
Object, Null
❌
Informazioni di protocollo
folder
Object, Null
❌
Informazioni sul fascicolo
status
String
✅
Valori: DOCUMENT_CREATED, REGISTRATION_PENDING, REGISTRATION_FAILED, PARTIAL_REGISTRATION, REGISTRATION_COMPLETE
type
String
✅
Tassonomia di Istanza: application-request, integration-request, integration-response, etc.
remote_id
String (UUID), Null
❌
-
remote_collection
Object, Null
❌
-
topics
Array[String]
❌
MinItems: 0
short_description
String
✅
MaxLength: 255
description
String, Null
❌
-
main_document
Object
✅
-
image_gallery
Array[Object]
❌
MinItems: 0
has_organization
String (URI), Null
❌
-
attachments
Array[Object]
❌
MinItems: 0
distribution_license_id
String, Null
❌
-
related_public_services
Array[Object]
❌
-
valid_from
String (DateTime), Null
❌
-
valid_to
String (DateTime), Null
❌
-
removed_at
String (DateTime), Null
❌
-
expire_at
String (DateTime), Null
❌
-
more_info
String, Null
❌
-
normative_requirements
Array[URI]
❌
-
related_documents
Array[URI]
❌
-
life_events
Array[String]
❌
MinItems: 0
business_events
Array[String]
❌
MinItems: 0
allowed_readers
Array[String]
❌
MinItems: 0
tenant_id
String (UUID)
✅
Uuid del tenant
owner_id
String (UUID)
✅
Uuid del proprietario del documento
document_url
String (URI), Null
❌
-
created_at
String (DateTime)
✅
-
updated_at
String (DateTime)
✅
-
author
Array[Object]
❌
MinItems: 0
source_type
String
✅
Valori: tenant, user
recipient_type
String
✅
Valori: tenant, user
last_seen
String (DateTime), Null
❌
-
registration_data
transmission_type
String
✅
Valori: Inbound, Outbound
date
String (Date)
❌
Data valida in formato YYYY-MM-DD
document_number
String
❌
Numero del documento
folder
title
String
✅
Titolo descrittivo del fascicolo
id
String
❌
Identificativo del fascicolo
main_document
name
String
✅
Nome del file
description
String
✅
Descrizione del documento
mime_type
String
✅
Formato MIME, es: application/pdf
url
String (URI)
✅
URL del documento
md5
String
❌
Hash MD5 del file
filename
String
✅
Nome del file salvato sul server
attachments
name
String
✅
Nome del file allegato
description
String
✅
Descrizione dell'allegato
mime_type
String
✅
Formato MIME, es: application/pdf
url
String (URI)
✅
URL dell'allegato
md5
String
❌
Hash MD5 del file
filename
String
✅
Nome del file salvato sul server
author
type
String
✅
Valori: human, legal
tax_identification_number
String
✅
Codice fiscale
name
String
✅
Nome dell'autore
family_name
String
✅
Cognome dell'autore
street_name
String
✅
Indirizzo
postal_code
String
✅
Codice postale
String
✅
Formato: email valido
role
String
✅
Ruolo (es: sender, receiver)
Descrizione dettagliata di tutti i passaggi che portano alla creazione e alla chiusura di un pagamento a partire da una pratica.
Essendo il sistema in questione costruito su un'architettura a eventi, è fondamentale che il payment proxy
controlli la versione dell'evento Payment (campo event_version
) prima di processarlo e dichiari con quale versione dell'evento quest'ultimo è compatibile. L'evento qui descritto ha versione 2.0
.
L'utente compila e invia la pratica, la quale viene inserita come evento nel topic applications
Un esempio di evento è il seguente, si faccia particolare attenzione al blocco payment_data
:
{
"app_version": "2.6.0-rc.7",
"attachments": [],
"authentication": {
"authentication_method": "cps/cns",
"certificate": "DQpSZXN1bHQgZ29lcyBoZXJlLi4uDQpCYXNlNjQNCg0KQmFzZTY0IGlzIGEgZ2VuZXJpYyB0ZXJtIGZvciBhIG51bWJlciBvZiBzaW1pbGFyIGVuY29kaW5nIHNjaGVtZXMgdGhhdCBlbmNvZGUgYmluYXJ5IGRhdGEgYnkgdHJlYXRpbmcgaXQgbnVtZXJpY2FsbHkgYW5kIHRyYW5zbGF0aW5nIGl0IGludG8gYSBiYXNlIDY0IHJlcHJlc2VudGF0aW9uLiBUaGUgQmFzZTY0IHRlcm0gb3JpZ2luYXRlcyBmcm9tIGEgc3BlY2lmaWMgTUlNRSBjb250ZW50IHRyYW5zZmVyIGVuY29kaW5nLg==",
"certificate_issuer": "FAKE_issuerdn",
"certificate_subject": "FAKE_subjectdn",
"instant": "2000-01-01T00-00Z",
"session_id": "abc123abc123abc123abc123abc123abc123abc123",
"session_index": "abc123abc123abc123abc123abc123abc123abc123",
"spid_code": null,
"spid_level": null
},
"backoffice_data": null,
"compiled_modules": [],
"created_at": "2022-12-06T17:43:42+01:00",
"creation_time": 1670345022,
"data": {
"applicant.data.Born.data.natoAIl": "1976-09-01T00:00:00+02:00",
"applicant.data.Born.data.place_of_birth": "Ponte di Piave",
"applicant.data.address.data.address": "Via Gramsci, 1",
"applicant.data.address.data.county": "PI",
"applicant.data.address.data.house_number": "",
"applicant.data.address.data.municipality": "Bugliano",
"applicant.data.address.data.postal_code": "56056",
"applicant.data.completename.data.name": "Vittorino",
"applicant.data.completename.data.surname": "Coliandro",
"applicant.data.email_address": "[email protected]",
"applicant.data.fiscal_code.data.fiscal_code": "CLNVTR76P01G822Q",
"applicant.data.gender.data.gender": "maschio",
"payment_amount": "1,34"
},
"event_created_at": "2022-12-06T17:43:51+01:00",
"event_id": "01e86cd4-8263-4ac0-a229-8cada5916eae",
"event_version": 2,
"flow_changed_at": "2022-12-06T17:43:51+01:00",
"id": "a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e",
"integrations": [],
"latest_status_change_at": "2022-12-06T17:43:51+01:00",
"latest_status_change_time": 1670345031,
"links": [],
"meetings": [],
"outcome": null,
"outcome_attachments": [],
"outcome_file": null,
"outcome_motivation": null,
"outcome_protocol_document_id": null,
"outcome_protocol_number": null,
"outcome_protocol_numbers": null,
"outcome_protocol_time": null,
"outcome_protocolled_at": null,
"path": "/applications",
"payment_data": {
"amount": "1.34",
"expire_at": "2023-03-06T17:43:51+01:00",
"landing": {
"method": "GET",
"url": "https://servizi.comune.bugliano.pi.it/x/it/pratiche/a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e/detail"
},
"notify": {
"method": "POST",
"url": "https://servizi.comune.bugliano.pi.it/x/api/applications/a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e/payment"
},
"reason": "a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e - CLNVTR76P01G822Q",
"split": []
},
"payment_type": "efil",
"protocol_document_id": null,
"protocol_folder_code": null,
"protocol_folder_number": null,
"protocol_number": null,
"protocol_numbers": [],
"protocol_time": null,
"protocolled_at": null,
"service": "pagamento-immediato-efil",
"service_group_name": null,
"service_id": "daa0f528-b582-4d1c-9691-e226ac443424",
"service_name": "Pagamento immediato EFIL",
"source_type": "http",
"status": "1500",
"status_name": "status_payment_pending",
"subject": null,
"submission_time": null,
"submitted_at": null,
"tenant": "60e35f02-1509-408c-b101-3b1a28109329",
"tenant_id": "60e35f02-1509-408c-b101-3b1a28109329",
"timestamp": "2022-12-06T16:43:51.829146577Z",
"user": "1c340b05-0808-4dbe-ad12-81ebbf3b6abf",
"user_compilation_notes": null,
"user_name": "Vittorino Coliandro",
"x-forwarded-for": "172.31.73.11"
}
Il payment dispatcher legge l'evento della pratica dal topic applications
, e dopo aver controllato mediante query su KSQLDB
che il pagamento per tale pratica non sia già stato creato, estrae da esso i dati del pagamento, successivamente li riversa in un nuovo evento di tipo Payment
e lo inserisce nel topic payments
in stato CREATION_PENDING
, indicando come chiave dell'evento il valore del campo service_id
. Da qui in poi solo il proxy si occuperà di scrivere sul topic payments
.
{
"id": "2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"user_id": "1c340b05-0808-4dbe-ad12-81ebbf3b6abf",
"type": "PAGOPA",
"tenant_id": "60e35f02-1509-408c-b101-3b1a28109329",
"service_id": "daa0f528-b582-4d1c-9691-e226ac443424",
"created_at": "2022-12-06T17:43:52+01:00",
"updated_at": "2022-12-06T17:43:52+01:00",
"status": "CREATION_PENDING",
"reason": "a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e - CLNVTR76P01G822Q",
"remote_id": "a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e",
"payment": {
"transaction_id": null,
"paid_at": null,
"expire_at": "2023-03-06T17:43:51+01:00",
"amount": 1.34,
"currency": "EUR",
"notice_code": null,
"iud": "2cc87ee53f574f20a703e6718c21b95b",
"iuv": null,
"receiver": null,
"due_type": null,
"pagopa_category": "9/12129/TS",
"document": null,
"split": []
},
"links": {
"online_payment_begin": {
"url": null,
"last_opened_at": null,
"method": "GET"
},
"online_payment_landing": {
"url": "https://servizi.comune.bugliano.pi.it/x/it/pratiche/a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e/detail",
"last_opened_at": null,
"method": "GET"
},
"offline_payment": {
"url": null,
"last_opened_at": null,
"method": "GET"
},
"receipt": {
"url": null,
"last_opened_at": null,
"method": "GET"
},
"notify": [
{
"url": "https://servizi.comune.bugliano.pi.it/x/api/applications/a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e/payment",
"method": "POST",
"sent_at": null
}
],
"update": {
"url": null,
"last_check_at": null,
"next_check_at": null,
"method": "GET"
},
"confirm": {
"url": null,
"last_opened_at": null,
"method": null
},
"cancel": {
"url": null
"last_opened_at": null,
"method": null
}
},
"payer": {
"type": "human",
"tax_identification_number": "CLNVTR76P01G822Q",
"name": "Vittorino",
"family_name": "Coliandro",
"street_name": "Via Gramsci, 1",
"building_number": "",
"postal_code": "56056",
"town_name": "Bugliano",
"country_subdivision": "PI",
"country": "IT",
"email": "[email protected]"
},
"debtor": {
"type": "human",
"tax_identification_number": "BNRMHL75C06G702B",
"name": "Michelangelo",
"family_name": "Buonarroti",
"street_name": "Cesare Battisti",
"building_number": "",
"postal_code": "38010",
"town_name": "Bugliano",
"country_subdivision": "PI",
"country": "IT",
"email": "[email protected]"
},
"event_id": "c3ecf316-ca76-4bf3-ab72-26b941a5ce4f",
"event_version": "2.0",
"event_created_at": "2022-12-06T17:43:52+01:00",
"app_id": "payment-dispatcher:1.1.3"
}
Il proxy legge dal topic payments
e, se ha una configurazione attiva per quel pagamento (quindi se l'evento letto ha un tenant_id
e un service_id
per cui ha una configurazione salvata nello storage del proxy), crea una posizione debitoria sull'Intermediario di riferimento.
È possibile che la configurazione relativa al pagamento letto dal topic payments
abbia configurato al suo interno una marca da bollo digitale. In tal caso, sarà necessario, per ogni marca da bollo digitale, inserire come minimo le seguenti informazioni all'interno del payload da inviare all'intermediario di pagamento con cui ci si sta integrando (in base a poi all'intermediario potrebbero essere necessari altri campi):
Il taglio della marca da bollo o l'importo inserito nella configurazione (per ora l'unico importo supportato è quello da 16 €)
La provincia di residenza del pagatore, la quale si trova nel campo payer.country_subdivision
dell'evento payment
L'hash del documento informatico o della segnatura di protocollo cui è associata la marca da bollo digitale (per semplicità il documento in questione sarà il json dell'evento letto dal topic payments
)
Il valore del campo reason
letto dall'evento nel topic payments
o, in assenza di questo, la causale della marca da bollo inserita nella configurazione
Successivamente, sarà necessario:
Valorizzare il campo payment.amount
l'importo della marca da bollo inserita in configurazione
Valorizzare il campo payment.due_type
con il tipo di dovuto inserito nella configurazione
Valorizzare il campo payment.pagopa_category
con il valore della tassonomia di pagoPA contenuta nella configurazione (se presente)
Valorizzare il campo payment.document.hash
contenente l'hash del documento informatico o della segnatura di protocollo cui è associata la marca da bollo digitale
ATTENZIONE: questa struttura sarà presente solo sugli eventi payment con event_version
valorizzato a 2.0
, sarà necessario dunque controllare la versione dell'evento prima di processarlo
In alcuni casi è possibile che il pagamento che il proxy deve processare presenti un bilancio fisso, ovvero un bilancio i cui importi per ogni voce sono già predeterminati a priori in fase di configurazione del servizio. In tal caso il campo split
, quando letto dal proxy per la prima volta, si presenterà come un array vuoto []
, il proxy dunque:
Leggerà il campo service_id
e reperirà nel mediante esso la configurazione del servizio dentro il quale ci sono le informazioni del bilancio.
Utilizzerà le informazioni ricavate per popolare il payload di creazione della posizione debitoria sull'intermediario di riferimento e per arricchire l'evento appena letto con i dati del bilancio.
Nel proxy è configurato per il servizio A il seguente bilancio
{
"split": [
{
"split_id": "c_1",
"split_type": "Tipo c1",
"split_code": "Codice c1",
"split_description": "Descrizione c1",
"split_amount": "16.00",
"split_budget_chapter": "Capitolo di bilancio c1",
"split_assessment": "Accertamento c1"
},
{
"split_id": "c_2",
"split_type": "Tipo c2",
"split_code": "Codice c2",
"split_description": "Descrizione c12",
"split_assessment": "0.50",
"split_budget_chapter": "Capitolo di bilancio c2",
"split_assessment": "Accertamento c2"
}
]
}
Il bilancio da utilizzare per arricchire il campo split
del pagamento sarà:
{
"split": [
{
"code": "c_1",
"amount": "16.00",
"meta": {
"split_type": "Tipo c1",
"split_code": "Codice c1",
"split_description": "Descrizione c1",
"split_budget_chapter": "Capitolo di bilancio c1",
"split_assessment": "Accertamento c1"
}
},
{
"code": "c_2",
"amount": "0.50",
"meta": {
"split_type": "Tipo c2",
"split_code": "Codice c2",
"split_description": "Descrizione c12",
"split_budget_chapter": "Capitolo di bilancio c2",
"split_assessment": "Accertamento c2"
}
}
]
}
In alcuni casi è possibile che il pagamento che il proxy deve processare presenti un bilancio variabile, ovvero un bilancio i cui importi per ogni voce vengono determinati dinamicamente a seconda del cittadino. In tal caso il campo split
, quando letto dal proxy per la prima volta, si presenterà con un payload così strutturato
[
{
"code": "c_1",
"amount" : "14.00",
"meta": {}
},
{
"code": "c_2",
"amount" : "2.50",
"meta": {}
}
]
oppure così nel caso in cui il cittadino sia esentato dal pagamento di una certa voce del bilancio
[
{
"code": "c_1",
"amount" : null,
"meta": {}
},
{
"code": "c_2",
"amount" : "0.50",
"meta": {}
}
]
dove il campo code
indica un identificativo interno della voce di bilancio determinato in fase di configurazione del servizio, e il campo amount
indica l'importo per ogni voce di bilancio. Nel caso in cui il valore di amount
sia null
significa che quella riga deve essere eliminata in fase di creazione del pagamento sul gateway. Il proxy dunque:
Leggerà il campo service_id
e reperirà mediante esso la configurazione del servizio dentro il quale ci sono le informazioni del bilancio.
Per ogni voce di bilancio ricavata dalla configurazione del servizio verrà sostituito l'importo con quello letto nel campo split
del pagamento
Utilizzerà le informazioni ricavate per popolare il payload di creazione della posizione debitoria sull'intermediario di riferimento e per arricchire l'evento appena letto con i dati del bilancio.
Nel proxy è configurato per il servizio A il seguente bilancio
{
"split": [
{
"split_id": "c_1",
"split_type": "Tipo c1",
"split_code": "Codice c1",
"split_description": "Descrizione c1",
"split_amount": "16.00",
"split_budget_chapter": "Capitolo di bilancio c1",
"split_assessment": "Accertamento c1"
},
{
"split_id": "c_2",
"split_type": "Tipo c2",
"split_code": "Codice c2",
"split_description": "Descrizione c12",
"split_assessment": "0.50",
"split_budget_chapter": "Capitolo di bilancio c2",
"split_assessment": "Accertamento c2"
}
]
}
Nel messaggio del topic payments il campo split
è valorizzato a
[
{
"code": "c_1",
"amount" : "14.00",
"meta": {}
},
{
"code": "c_2",
"amount" : "2.50",
"meta": {}
}
]
Il bilancio da utilizzare per arricchire il campo split
del pagamento sarà:
{
"split": [
{
"code": "c_1",
"amount": "14.00",
"meta": {
"split_type": "Tipo c1",
"split_code": "Codice c1",
"split_description": "Descrizione c1",
"split_budget_chapter": "Capitolo di bilancio c1",
"split_assessment": "Accertamento c1"
}
},
{
"code": "c_2",
"amount": "2.50",
"meta": {
"split_type": "Tipo c2",
"split_code": "Codice c2",
"split_description": "Descrizione c12",
"split_budget_chapter": "Capitolo di bilancio c2",
"split_assessment": "Accertamento c2"
}
}
]
}
Nel proxy è configurato per il servizio a il seguente bilancio
{
"split": [
{
"split_id": "c_1",
"split_type": "Tipo c1",
"split_code": "Codice c1",
"split_description": "Descrizione c1",
"split_amount": "16.00",
"split_budget_chapter": "Capitolo di bilancio c1",
"split_assessment": "Accertamento c1"
},
{
"split_id": "c_2",
"split_type": "Tipo c2",
"split_code": "Codice c2",
"split_description": "Descrizione c12",
"split_assessment": "0.50",
"split_budget_chapter": "Capitolo di bilancio c2",
"split_assessment": "Accertamento c2"
}
]
}
Nel messaggio del topic payments
il campo split
è valorizzato a
[
{
"code": "c_1",
"amount" : null,
"meta": {}
},
{
"code": "c_2",
"amount" : "0.50",
"meta": {}
}
]
Il bilancio da utilizzare per arricchire il campo split
del pagamento sarà:
{
"split": [
{
"code": "c_2",
"amount": "0.50",
"meta": {
"split_type": "Tipo c2",
"split_code": "Codice c2",
"split_description": "Descrizione c12",
"split_budget_chapter": "Capitolo di bilancio c2",
"split_assessment": "Accertamento c2"
}
}
]
}
In caso di risposta positiva:
Il pagamento viene passato in stato PAYMENT_PENDING
modificando il campo status
,
Vengono compilate le informazioni del bilancio nel campo split
(se presenti),
Viene compilata l'url per pagare online (links.online_payment_begin.url
), la quale è data dalla concatenazione della variabile d'ambiente EXTERNAL_API_URL
alla stringa /online-payment/{payment_id}
Viene compilata l'url di ritorno dal pagamento (links.online_payment_landing.url
), la quale è data dalla concatenazione della variabile d'ambiente EXTERNAL_API_URL
alla stringa /landing/{payment_id}
Viene compilata l'url per pagare offline (links.offline_payment.url
), la quale è è data dalla concatenazione della variabile d'ambiente EXTERNAL_API_URL
alla stringa /offline-payment/{payment_id}
Viene compilata l'url della ricevuta di pagamento (links.receipt.url
), la quale è data dalla concatenazione della variabile d'ambiente EXTERNAL_API_URL
alla stringa /receipt/{payment_id}
Viene compilata l'url per richiedere l'aggiornamento del pagamento (links.update.url
), la quale è data dalla concatenazione della variabile d'ambiente INTERNAL_API_URL
alla stringa /update/{payment_id}
Nota: a differenza dell'altre url, questa viene valorizzata utilizzando una variabile d'ambiente separata in quanto deve essere possibile chiamarla pubblicamente, questo perchè è compito del solo payments-poller
richiedere periodicamente l'aggiornamento del pagamento.
Viene compilata l'url per forzare l'annullamento di un pagamento (links.cancel.url
), la quale è data dalla concatenazione della variabile d'ambiente EXTERNAL_API_URL
alla stringa /payments/{payment_id}
. Viene inoltre compilato il metodo con cui chiamare la url (links.cancel.method
) con il valore PATCH
.
Viene segnato il timestamp di aggiornamento dell'evento di pagamento (updated_at
)
Viene infine salvato il pagamento sullo storage e scritto il corrispondente evento aggiornato sul topic payments
, indicando come chiave dell'evento il valore del campo service_id
.
{
"id": "2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"user_id": "1c340b05-0808-4dbe-ad12-81ebbf3b6abf",
"type": "PAGOPA",
"tenant_id": "60e35f02-1509-408c-b101-3b1a28109329",
"service_id": "daa0f528-b582-4d1c-9691-e226ac443424",
"created_at": "2022-12-06T17:43:52+01:00",
"updated_at": "2022-12-06T17:43:53+01:00",
"status": "PAYMENT_PENDING",
"reason": "a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e - CLNVTR76P01G822Q",
"remote_id": "a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e",
"payment": {
"transaction_id": null,
"paid_at": null,
"expire_at": "2023-03-06T17:43:51+01:00",
"amount": 1.34,
"currency": "EUR",
"notice_code": "302872234000000343",
"iud": "2cc87ee53f574f20a703e6718c21b95b",
"iuv": "02872234000000343",
"receiver": null,
"due_type": "TARI",
"pagopa_category": "9/12129/TS",
"document": null,
"split": []
},
"links": {
"online_payment_begin": {
"url": "https://efil-proxy-qa.boat.opencontent.io/online-payment/2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"last_opened_at": null,
"method": "GET"
},
"online_payment_landing": {
"url": "https://servizi.comune.bugliano.pi.it/x/it/pratiche/a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e/detail",
"last_opened_at": null,
"method": "GET"
},
"offline_payment": {
"url": "https://efil-proxy-qa.boat.opencontent.io/notice/2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"last_opened_at": null,
"method": "GET"
},
"receipt": {
"url": "https://efil-proxy-qa.boat.opencontent.io/receipt/2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"last_opened_at": null,
"method": "GET"
},
"notify": [
{
"url": "https://servizi.comune.bugliano.pi.it/x/api/applications/a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e/payment",
"method": "POST",
"sent_at": null
}
],
"update": {
"url": "http://efil-proxy-qa.boat-backplane.opencontent.io/update/2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"last_check_at": null,
"next_check_at": "2022-12-06T17:43:53+01:00",
"method": "GET"
},
"confirm": {
"url": null,
"last_opened_at": null,
"method": "PATCH"
},
"cancel": {
"url": "https://efil-proxy-qa.boat.opencontent.io/payments/2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"last_opened_at": null,
"method": "PATCH"
}
},
"payer": {
"type": "human",
"tax_identification_number": "CLNVTR76P01G822Q",
"name": "Vittorino",
"family_name": "Coliandro",
"street_name": "Via Gramsci, 1",
"building_number": "",
"postal_code": "56056",
"town_name": "Bugliano",
"country_subdivision": "PI",
"country": "IT",
"email": "[email protected]"
},
"debtor": {
"type": "human",
"tax_identification_number": "BNRMHL75C06G702B",
"name": "Michelangelo",
"family_name": "Buonarroti",
"street_name": "Cesare Battisti",
"building_number": "",
"postal_code": "38010",
"town_name": "Bugliano",
"country_subdivision": "PI",
"country": "IT",
"email": "[email protected]"
},
"event_id": "aa716d45-f56c-4b38-8c62-668a48818a04",
"event_version": "2.0",
"event_created_at": "2022-12-06T17:43:53+01:00",
"app_id": "efil-payment-proxy-qa:1.2.2"
}
In caso di risposta negativa, il pagamento viene passato in stato CREATION_FAILED
, viene salvato il pagamento sullo storage e viene scritto l'evento aggiornato sul topic payments
{
"id": "7037158b-f990-46d9-9377-1392adb81186",
"user_id": "1c340b05-0808-4dbe-ad12-81ebbf3b6abf",
"type": "PAGOPA",
"tenant_id": "60e35f02-1509-408c-b101-3b1a28109329",
"service_id": "b2c33452-a741-4841-acc1-69a2c007e398",
"created_at": "2022-11-22T17:39:38+01:00",
"updated_at": "2022-11-22T17:39:38+01:00",
"status": "CREATION_FAILED",
"reason": "84a4bc4b-6eaf-4ea2-b4bc-0c4d73b05c6c - CLNVTR76P01G822Q",
"remote_id": "84a4bc4b-6eaf-4ea2-b4bc-0c4d73b05c6c",
"payment": {
"transaction_id": null,
"paid_at": null,
"expire_at": "2023-02-20T17:39:37+01:00",
"amount": 1,
"currency": "EUR",
"notice_code": null,
"iud": "7037158bf99046d993771392adb81186",
"iuv": null,
"receiver": null,
"due_type": null,
"pagopa_category": null,
"document": null,
"split": []
},
"links": {
"online_payment_begin": {
"url": null,
"last_opened_at": null,
"method": "GET"
},
"online_payment_landing": {
"url": "https://servizi.comune.bugliano.pi.it/x/it/pratiche/84a4bc4b-6eaf-4ea2-b4bc-0c4d73b05c6c/detail",
"last_opened_at": null,
"method": "GET"
},
"offline_payment": {
"url": null,
"last_opened_at": null,
"method": "GET"
},
"receipt": {
"url": null,
"last_opened_at": null,
"method": "GET"
},
"notify": [
{
"url": "https://servizi.comune.bugliano.pi.it/x/api/applications/84a4bc4b-6eaf-4ea2-b4bc-0c4d73b05c6c/payment",
"method": "POST",
"sent_at": null
}
],
"update": {
"url": null,
"last_check_at": null,
"next_check_at": null,
"method": "GET"
},
"confirm": {
"url": null,
"last_opened_at": null,
"method": "PATCH"
},
"cancel": {
"url": "https://efil-proxy-qa.boat.opencontent.io/payments/2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"last_opened_at": null,
"method": "PATCH"
}
},
"payer": {
"type": "human",
"tax_identification_number": "CLNVTR76P01G822Q",
"name": "Vittorino",
"family_name": "Coliandro",
"street_name": "Via Gramsci, 1",
"building_number": "",
"postal_code": "56056",
"town_name": "Bugliano",
"country_subdivision": "PI",
"country": "IT",
"email": "[email protected]"
},
"debtor": {
"type": "human",
"tax_identification_number": "BNRMHL75C06G702B",
"name": "Michelangelo",
"family_name": "Buonarroti",
"street_name": "Cesare Battisti",
"building_number": "",
"postal_code": "38010",
"town_name": "Bugliano",
"country_subdivision": "PI",
"country": "IT",
"email": "[email protected]"
},
"event_id": "8b3452d8-bd5d-4380-b8a6-7e4f551b09d8",
"event_version": "2.0",
"event_created_at": "2022-11-22T17:39:39+01:00",
"app_id": "pmpay-payment-proxy:1.0.0-rc.2"
}
Nel core intanto, la UI del cittadino monitora lo stato del pagamento interrogando una tabella su KSQLDB
creata a partire dal topic payments, e se in stato PAYMENT_PENDING
, vengono mostrate all'utente le opzioni per pagare
Verra richiamata l'url {EXTERNAL_API_URL}/online-payment/{payment_id}
servita dal proxy
Il proxy richiederà all'intermediario di pagamento il link per pagare
Sarà segnato il timestamp di apertura del link (links.online_payment_begin.last_opened_at
)
Sarà segnato il timestamp di aggiornamento dell'evento (updated_at
)
Il pagamento verrà salvato sullo storage
L'evento aggiornato verrà scritto sul topic payments
, indicando come chiave dell'evento il valore del campo service_id
Verrà restituito il link ritornato allo step 2 mediante il quale l'utente verrà rediretto al portale dell'intermediario
Al termine della procedura di pagamento, l'utente verrà fatto ritornare sul dettaglio della pratica mediante l'url di landing specificata in fase di creazione della posizione debitoria (links.online_payment_landing.url
) allo step 5, a questa url andrà aggiunto in query string un parametro payment
da valorizzare con OK
o KO
a seconda dell'esito del pagamento
Contemporaneamente, essendo che si passa per il proxy, verrà segnato il timestamp in cui l'utente ha aperto il link di ritorno (links.online_payment_landing.last_opened_at
)
Il pagamento verrà portato in stato PAYMENT_STARTED
Sarà segnato il timestamp di aggiornamento dell'evento (updated_at
)
Il pagamento verrà salvato sullo storage
L'evento aggiornato verrà scritto sul topic payments
, indicando come chiave dell'evento il valore del campo service_id
.
{
"id": "2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"user_id": "1c340b05-0808-4dbe-ad12-81ebbf3b6abf",
"type": "PAGOPA",
"tenant_id": "60e35f02-1509-408c-b101-3b1a28109329",
"service_id": "daa0f528-b582-4d1c-9691-e226ac443424",
"created_at": "2022-12-06T17:43:52+01:00",
"updated_at": "2022-12-06T17:44:51+01:00",
"status": "PAYMENT_STARTED",
"reason": "a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e - CLNVTR76P01G822Q",
"remote_id": "a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e",
"payment": {
"transaction_id": "7b32b299-67db-4f84-9419-f61962c3e202",
"paid_at": null,
"expire_at": "2023-03-06T17:43:51+01:00",
"amount": 1.34,
"currency": "EUR",
"notice_code": "302872234000000343",
"iud": "2cc87ee53f574f20a703e6718c21b95b",
"iuv": "02872234000000343",
"receiver": null,
"due_type": "TARI",
"pagopa_category": "9/12129/TS",
"document": null,
"split": []
},
"links": {
"online_payment_begin": {
"url": "https://efil-proxy-qa.boat.opencontent.io/online-payment/2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"last_opened_at": "2022-12-06T17:43:56+01:00",
"method": "GET"
},
"online_payment_landing": {
"url": "https://servizi.comune.bugliano.pi.it/x/it/pratiche/a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e/detail",
"last_opened_at": "2022-12-06T17:44:51+01:00",
"method": "GET"
},
"offline_payment": {
"url": "https://efil-proxy-qa.boat.opencontent.io/notice/2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"last_opened_at": null,
"method": "GET"
},
"receipt": {
"url": "https://efil-proxy-qa.boat.opencontent.io/receipt/2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"last_opened_at": null,
"method": "GET"
},
"notify": [
{
"url": "https://servizi.comune.bugliano.pi.it/x/api/applications/a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e/payment",
"method": "POST",
"sent_at": null
}
],
"update": {
"url": "http://efil-proxy-qa.boat-backplane.opencontent.io/update/2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"last_check_at": null,
"next_check_at": "2022-12-06T17:43:53+01:00",
"method": "GET"
},
"confirm": {
"url": null,
"last_opened_at": null,
"method": "PATCH"
},
"cancel": {
"url": "https://efil-proxy-qa.boat.opencontent.io/payments/2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"last_opened_at": null,
"method": "PATCH"
}
},
"payer": {
"type": "human",
"tax_identification_number": "CLNVTR76P01G822Q",
"name": "Vittorino",
"family_name": "Coliandro",
"street_name": "Via Gramsci, 1",
"building_number": "",
"postal_code": "56056",
"town_name": "Bugliano",
"country_subdivision": "PI",
"country": "IT",
"email": "[email protected]"
},
"debtor": {
"type": "human",
"tax_identification_number": "BNRMHL75C06G702B",
"name": "Michelangelo",
"family_name": "Buonarroti",
"street_name": "Cesare Battisti",
"building_number": "",
"postal_code": "38010",
"town_name": "Bugliano",
"country_subdivision": "PI",
"country": "IT",
"email": "[email protected]"
},
"event_id": "dcd203c7-a6e8-4084-b09b-f700e8fa94d8",
"event_version": "2.0",
"event_created_at": "2022-12-06T17:44:51+01:00",
"app_id": "efil-payment-proxy-qa:1.2.2"
}
Verra richiamata l'url {EXTERNAL_API_URL}/offline-payment/{payment_id}
servita dal proxy
Il proxy richiederà all'intermediario di pagamento il pdf dell'avviso di pagamento
Sarà segnato il timestamp di apertura del link (links.offline_payment.last_opened_at
)
Sarà segnato il timestamp di aggiornamento dell'evento (updated_at
)
Il pagamento verrà salvato sullo storage
L'evento aggiornato verrà scritto sul topic payments
, indicando come chiave dell'evento il valore del campo service_id
Verrà eseguito il download del pdf ritornato allo step 1
È possibile che l'operatore abbia necessità di annullare un pagamento mediante l'opzione di registrazione del pagamento, ciò può accadere ad esempio se il cittadino, dopo aver creato il pagamento, decide di non concluderlo, e di pagare invece mediante altri canali creando un nuovo pagamento diverso da quello presente sulla piattaforma. Per evitare quindi di lasciare il pagamento pendente per un tempo indefinito, e per permettere l'avanzamento della pratica all'interno della quale è stato creato tale pagamento, sarà possibile annullare quest'ultimo.
Gli step sono i seguenti:
Verra richiamata l'url PATCH {EXTERNAL_API_URL}/payments/{payment_id}
servita dal proxy con payload {"status": "CANCELED"}
Il proxy, dopo aver letto il pagamento dallo storage, segnerà il campo status
a CANCELED
Sarà segnato il timestamp di apertura del link (links.offline_payment.last_opened_at
)
Sarà segnato il timestamp di aggiornamento dell'evento (updated_at
)
Il pagamento verrà salvato sullo storage
L'evento aggiornato verrà scritto sul topic payments
, indicando come chiave dell'evento il valore del campo service_id
In alcuni casi è possibile che i pagamenti non siano generati a partire dall'area personale, ma siano invece importati da una fonte esterna. In questo caso gli eventi relativi a tali pagamenti saranno inseriti nel topic payments
già in status PAYMENT_PENDING
e conterranno al loro interno tutte le informazioni che il proxy normalmente inserisce quando fa passare un pagamento generato dall'area personale dallo status CREATION_PENDING
allo status PAYMENT_PENDING
.
In tal caso tuttavia, questi pagamenti non saranno presenti sullo storage del proxy, dal momento che questa è un'operazione che normalmente farebbe il proxy. Di conseguenza, nel momento in cui dovesse venir letto un evento in stato PAYMENT_PENDING
, ci fosse una configurazione attiva per quest'ultimo (ciò si determina controllando che ci sia sullo storage una configurazione con tenant_id
e service_id
equivalenti a quelli contenuti nel pagamento) ma non esistesse sullo storage, bisogna semplicemente salvare il pagamento sullo storage.
Nel core intanto, questo tipo di dovuti saranno mostrati nella sezione "Pagamenti" insieme a tutti i pagamenti (importati e non) generati dal cittadino, i dati qui presenti sono estratti mediante interrogazione di una tabella su KSQLDB
creata a partire dal topic payments.
Cliccando su "Vedi dettaglio" si andrà sul dettaglio dello specifico pagamento dove saranno mostrate le opzioni per pagare (online, offline)
Il payments poller nel frattempo:
Interroga la tabella PAYMENTS_DETAIL
per ottenere lo stato dei pagamenti, e se questi sono in stato PAYMENT_PENDING
o PAYMENT_STARTED
, eseguirà una chiamata http all'url {INTERNAL_API_URL}/update/{payment_id}
servita dal proxy (quella di aggiornamento menzionata allo step 5)
Il proxy a sua volta recupera dallo storage il pagamento, interroga l'intermediario di riferimento per verificare lo stato del pagamento, e in caso lo aggiorna in stato COMPLETE
o PAYMENT_STARTED
(questa casistica può verificarsi nel caso in cui l'utente paghi ma poi chiuda il browser anziche cliccare sul bottone contenente la landing url)
Se il pagamento viene portato in stato COMPLETE
, si dovrà salvare, se presente come informazione ritornata dall'intemediario di riferimento, il timestamp di avvenuto pagamento (payment.paid_at
), e l'id della transazione (payment.transaction_id
)
Il proxy segna il timestamp in cui è stato aperto il link di aggiornamento (links.update.last_opened_at
)
Il proxy segna il timestamp di aggiornamento del pagamento (updated_at
)
Il proxy salva il pagamento sullo storage e lo scrive sul topic payments
, indicando come chiave dell'evento il valore del campo service_id
.
Logica di polling
Più passa il tempo, più è probabile che il pagamento non venga eseguito dall'utente, di conseguenza ad ogni chiamata del poller verso il proxy, viene incrementato l'intervallo di tempo tra una chiamata e l'altra, ciò viene fatto in base al campo links.update.next_check_at.
Nello specifico il polling avviene con al seguente logica:
Condizione iniziale:
Se il campo last_check_at
è nullo (non è stato effettuato alcun controllo in precedenza), la funzione termina immediatamente senza aggiornare il prossimo controllo.
Checkpoint temporali: La funzione utilizza diversi checkpoint per classificare il tempo trascorso dalla creazione della risorsa:
5 minuti
15 minuti
7 giorni
30 giorni
365 giorni
Ciascun intervallo ha una frequenza di controllo associata:
Fino a 5 minuti: Controlli ogni 1 minuto.
Fino a 15 minuti: Controlli ogni 5 minuti.
Fino a 7 giorni: Controlli ogni ora.
Fino a 30 giorni: Controlli ogni 6 ore.
Fino a 365 giorni: Controlli settimanali, sempre alla fine della settimana corrente.
Oltre 365 giorni: La risorsa è considerata scaduta, lo stato è impostato su STATUS_EXPIRED
e il prossimo controllo è disabilitato (next_check_at
impostato a None
).
Impostazione del prossimo controllo:
Se determinato, il next_check
è aggiornato al valore calcolato
Aggiornamento dello stato:
Se il tempo dalla creazione supera i 365 giorni, lo stato della risorsa viene aggiornato a STATUS_EXPIRED
.
Nota 2: non tutti i proxy aggiornano i pagamenti mediante update, alcuni vengono aggiornati predisponendo un'apposita API che l'intermediario di pagamento PagoPA chiama inviando la notifica di completamento del pagamento. Questa viene quindi elaborata dal proxy il quale aggiorna il pagamento portandolo in stato COMPLETE
, segna il timestamp di aggiornamento del pagamento (updated_at
), lo salva sullo storage e lo scrive sul topic payments
, indicando come chiave dell'evento il valore del campo service_id
.
{
"id": "2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"user_id": "1c340b05-0808-4dbe-ad12-81ebbf3b6abf",
"type": "PAGOPA",
"tenant_id": "60e35f02-1509-408c-b101-3b1a28109329",
"service_id": "daa0f528-b582-4d1c-9691-e226ac443424",
"created_at": "2022-12-06T17:43:52+01:00",
"updated_at": "2022-12-07T09:47:35+01:00",
"status": "COMPLETE",
"reason": "a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e - CLNVTR76P01G822Q",
"remote_id": "a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e",
"payment": {
"transaction_id": "7b32b299-67db-4f84-9419-f61962c3e202",
"paid_at": null,
"expire_at": "2023-03-06T17:43:51+01:00",
"amount": 1.34,
"currency": "EUR",
"notice_code": "302872234000000343",
"iud": "2cc87ee53f574f20a703e6718c21b95b",
"iuv": "02872234000000343",
"receiver": null,
"due_type": "TARI",
"pagopa_category": "9/12129/TS",
"document": null,
"split": []
},
"links": {
"online_payment_begin": {
"url": "https://efil-proxy-qa.boat.opencontent.io/online-payment/2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"last_opened_at": "2022-12-06T17:43:56+01:00",
"method": "GET"
},
"online_payment_landing": {
"url": "https://servizi.comune.bugliano.pi.it/x/it/pratiche/a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e/detail",
"last_opened_at": "2022-12-06T17:44:51+01:00",
"method": "GET"
},
"offline_payment": {
"url": "https://efil-proxy-qa.boat.opencontent.io/notice/2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"last_opened_at": null,
"method": "GET"
},
"receipt": {
"url": "https://efil-proxy-qa.boat.opencontent.io/receipt/2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"last_opened_at": null,
"method": "GET"
},
"notify": [
{
"url": "https://servizi.comune.bugliano.pi.it/x/api/applications/a1bbcafb-ee78-4cd1-9b8e-7ddeb434d84e/payment",
"method": "POST",
"sent_at": null
}
],
"update": {
"url": "http://efil-proxy-qa.boat-backplane.opencontent.io/update/2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"last_check_at": "2022-12-07T09:47:35+01:00",
"next_check_at": "2022-12-07T10:47:35+01:00",
"method": "GET"
},
"confirm": {
"url": null,
"last_opened_at": null,
"method": "PATCH"
},
"cancel": {
"url": "https://efil-proxy-qa.boat.opencontent.io/payments/2cc87ee5-3f57-4f20-a703-e6718c21b95b",
"last_opened_at": null,
"method": "PATCH"
}
},
"payer": {
"type": "human",
"tax_identification_number": "CLNVTR76P01G822Q",
"name": "Vittorino",
"family_name": "Coliandro",
"street_name": "Via Gramsci, 1",
"building_number": "",
"postal_code": "56056",
"town_name": "Bugliano",
"country_subdivision": "PI",
"country": "IT",
"email": "[email protected]"
},
"debtor": {
"type": "human",
"tax_identification_number": "BNRMHL75C06G702B",
"name": "Michelangelo",
"family_name": "Buonarroti",
"street_name": "Cesare Battisti",
"building_number": "",
"postal_code": "38010",
"town_name": "Bugliano",
"country_subdivision": "PI",
"country": "IT",
"email": "[email protected]"
},
"event_id": "1a745224-37bb-4843-a85f-3011764395a6",
"event_version": "2.0",
"event_created_at": "2022-12-07T09:47:35+01:00",
"app_id": "efil-payment-proxy-qa:1.2.2"
}
Il payment updater monitora lo stato dei pagamenti leggendo dal topic payments
, e contestualmente aggiorna lo stato della pratica, se il pagamento è in statoPAYMENT_STARTED
, aggiorna lo stato della pratica a "In attesa dell'esito di pagamento", se invece il pagamento è in stato COMPLETE
, aggiorna lo stato della pratica a "Accettata" se il pagamento è posticipato, o Inviata se il pagamento è immediato.
Gli stati per i quali passa un pagamento durante il suo ciclo di vita sono riassunti nella seguente tabella
CREATION_PENDING
pagamento in attesa di essere creato sul provider di riferimento
CREATION_FAILED
pagamento di cui è fallita la creazione sul provider di riferimento
PAYMENT_PENDING
pagamento creato sul provider di riferimento e in attesa di essere eseguito dall'utente
PAYMENT_STARTED
procedura di pagamento iniziata dall'utente
COMPLETE
pagamento completato a seguito di conferma dal provider di riferimento
PAYMENT_FAILED
pagamento fallito a causa di scadenza del termine ultimo entro cui doveva essere eseguito
nei proxy sviluppati questo stato non è quasi mai stato utilizzato
CANCELED
pagamento annullato forzatamente da un operatore
Tutti i proxy devono:
Validare gli eventi in input e output
Integrazione con sentry
Healthcheck
Esporre metriche di monitoraggio in formato prometheus