Nexi XPay con Rails: test, verifica locale e produzione
L’episodio 1 ha costruito il lato della richiesta, l’episodio 2 il lato della risposta. 19 test automatici passano, e nessuno di essi tocca davvero la rete — che è esattamente il punto di una suite di test, ma significa anche che nessuno di essi dimostra davvero che i server reali di Nexi accettino ciò che questa app gli manda. Questo episodio chiude quel divario: un pagamento vero, sulla vera sandbox, con il webhook che arriva da solo.
Il codice è taggato episode-3 nel repo nexi-xpay-with-rails — questo episodio è più leggero sul nuovo codice applicativo e più pesante sul processo, dato che testare e verificare sono l’argomento vero e proprio.
Perché la sola suite di test non basta
Ogni test scritto nei due episodi precedenti costruisce il proprio MAC usando la mac_key dell’app stessa e verifica che l’app sappia validare la propria firma — un controllo reale e significativo, ma un circuito chiuso. Dimostra che l’algoritmo è implementato correttamente; non dice nulla su se i server di Nexi calcolino per caso lo stesso identico algoritmo allo stesso modo per la tua specifica configurazione di terminale, se il tuo alias sandbox funzioni davvero, o se una notifica possa davvero raggiungere il tuo server dall’esterno. Queste cose si confermano solo con un’esecuzione end-to-end sul sistema vero.
Nexi non può raggiungere localhost
Questo è il blocco pratico: /nexi/notify deve essere raggiungibile dai server di Nexi stessi, e localhost:3000 non è raggiungibile da nessun posto tranne la tua macchina. La soluzione è un tunnel pubblico:
brew install --cask ngrok
ngrok config add-authtoken <token> # account gratuito, da ngrok.com
bin/dev # oppure: bin/rails server -p 3000
ngrok http 3000
ngrok stampa un URL https://qualcosa.ngrok-free.app. Due cose devono essere vere perché il resto funzioni:
Devi navigare davvero da quell’URL, non da localhost. OrdersController#create costruisce notify_url a partire dall’host da cui è arrivata la richiesta corrente — visita da localhost e Nexi si ritrova con un notify_url che non potrà mai raggiungere, per quanto correttamente sia costruito tutto il resto.
Rails deve essere istruito a fidarsi dell’host ngrok, o ActionDispatch::HostAuthorization rifiuta la richiesta prima ancora che il tuo controller la veda:
# config/environments/development.rb
config.hosts << /.*\.ngrok-free\.(app|dev)/
Senza questa riga, visitare l’URL ngrok mostra la pagina di errore “Blocked hosts” di Rails — un gotcha reale, non ipotetico, e facile da cui perdere dieci minuti se non sai già che config.hosts è il meccanismo.
Una vera carta di test, e un vero pagamento
Il backoffice sandbox di Nexi (la stessa pagina Area test a cui rimandava l’episodio 1 per le credenziali) fornisce numeri di carta di test:
4539 9700 0000 0006— Visa, approvata4539 9700 0000 0014— Visa, rifiutata (utile per esercitare volutamente il ramoKOdi#notify)
Qualunque data di scadenza futura, qualunque CVV a 3 cifre. L’autenticazione 3D Secure in sandbox accetta sempre l’OTP 123456.
Eseguire l’intero flusso — checkout, redirect verso Nexi, inserimento carta, 3D Secure — e poi controllare cosa è davvero arrivato conferma che tutto funziona insieme, non solo ogni pezzo isolato. Ecco una notifica vera ricevuta su /nexi/notify durante proprio questo test, oscurata solo dove davvero non conta:
Started POST "/nexi/notify" for 185.198.117.20
Processing by NexiCallbacksController#notify as HTML
Parameters: {
"codTrans"=>"19JWR3YNVOKC",
"esito"=>"OK",
"importo"=>"1200", # 12,00 €
"divisa"=>"EUR",
"data"=>"20260912", "orario"=>"225428",
"codAut"=>"MP7EL4", # codice di autorizzazione reale
"pan"=>"453997******0006", # mascherato, mai in chiaro
"brand"=>"VISA",
"tipoTransazione"=>"VBV_FULL", # 3D Secure completo
"mac"=>"9fee38d052f87f5ac6548ce0a9ba20c2d9933e8e",
}
Nessun passaggio manuale in nessun punto di quel log — i server di Nexi hanno trovato da soli l’URL ngrok, ci hanno fatto un POST, e l’app ha segnato l’ordine come pagato interamente da sola. Questo è l’asticella vera per dire “funziona”, ed è significativamente più alta di “i test sono verdi”.
Una cosa che vale la pena sapere se vai a guardare il tuo log: Nexi può mandare più campi di quanti ne elenchi qualunque tabella nella sua documentazione — un campo configurato dall’esercente come num_contratto, o campi specifici del terminale come check/aliasEffettivo, a seconda di cosa è stato impostato nel tuo specifico profilo di backoffice. È esattamente per questo che raw_notification_params cattura l’intero payload senza distinzioni invece di scegliere un elenco fisso di chiavi attese — non c’è modo di sapere in anticipo l’insieme completo che un dato terminale manderà davvero.
Dati di seed, per chi vuole provarlo in prima persona
# db/seeds.rb
Product.find_or_create_by!(name: "Sample experience") do |product|
product.price = 12.00
product.purchasable = true
end
bin/rails db:seed
Prezzo basso di proposito — l’ambiente sandbox ha dei limiti, e non c’è motivo di testarli da vicino.
Cosa manca davvero prima della produzione
Questa serie costruisce un flusso di pagamento completo, testato, verificato — volutamente non uno pronto per la produzione. Nello specifico, resta ancora aperto:
- Credenziali
nexi.production— un alias di terminale e una MAC key reali, aggiunti acredentials.yml.encaccanto anexi.sandboxuna volta pronti ad accettare denaro vero. L’endpoint di produzione perde il prefissoint-:https://ecommerce.nexi.it/.... - Un’email di conferma, inviata da
#notifydopomark_paid!— mai da#return, dato che quell’azione è solo UX e il cliente può saltarla del tutto. - Una vista admin su ordini e notifiche — perlopiù in sola lettura, con un’azione esplicita
mark_refundedse servono rimborsi, mai unupdategenerico che potrebbe toccare campi finanziari. - Rivalidare esplicitamente
importo/divisadella notifica controorder.total_amount/order.currencyprima di chiamaremark_paid!. Il MAC già garantisce che Nexi non abbia mandato qualcosa che una notifica manomessa non riuscirebbe a firmare correttamente — questo sarebbe una difesa in profondità sopra quello, non una correzione per un buco noto. - Attivare
purchasable, un prodotto reale alla volta, una volta che tutto quanto sopra è a posto.
Nessuna di queste cambia come funziona il flusso principale — sono ciò che separa “questo elabora correttamente un pagamento” da “questo è ciò che un’azienda vera gestisce senza supervisione”.
Per concludere
Tre episodi, un’app Rails piccola ma completa: un checkout che non si fida mai di un prezzo dal client, un gateway di pagamento che firma ciò che manda e verifica ciò che riceve, un webhook idempotente che non rielabora mai due volte la stessa notifica, e una suite di test più un’esecuzione reale in sandbox a garantire tutto questo. Se stai integrando XPay tu stesso, il repo completo è lì da clonare, leggere e adattare.