← Înapoi la devlog

30 iunie 2026 · Sebastian

Refacerea plăților unui checkout live fără a scoate magazinul din funcțiune

#php#symfony#payments#architecture

Roviniete.ro vinde roviniete și asigurări auto RCA către mii de șoferi români în fiecare zi. Afacerea era sănătoasă; checkout-ul de dedesubt nu era. Funcționa pe o integrare de plăți îmbătrânită, cu practic o singură metodă de plată, confirmări de plată care uneori eșuau în tăcere și fără nicio modalitate de a reînnoi o rovinietă care expiră fără a o cumpăra din nou de la zero.

Misiunea a fost întreaga experiență de plată: un gateway nou, plată cu cardul, plată directă bancă-la-bancă (open banking), confirmări fiabile, reînnoiri automate, un catalog mai larg și un back office securizat — pe un magazin care nu putea niciodată să cadă. „Nu strica lucrul care deja încasează bani" a fost constrângerea din spatele fiecăruia dintre aceste obiective. Iată cum au intrat piesele la locul lor.

Rulează noul gateway alături de cel vechi

Planul tentant este un cutover: construiești noua integrare PayU v4, schimbi un comutator, ștergi integrarea veche. Planul tentant este și cel care transformă o greșeală de tastare într-un config într-o întrerupere a veniturilor.

Așa că nu am făcut cutover. Am ridicat serviciile v4 alături de integrarea existentă, în spatele aceluiași checkout pe care clientul deja îl cunoștea. Cele două metode de plată cu cardul coexistă literalmente în sistem și sunt marcate ca mutual exclusive, astfel încât o comandă trece exact prin una dintre ele — calea nouă sau calea legacy — niciodată un amestec al ambelor. Asta a permis ca funcționalitățile să aterizeze una câte una: mai întâi cardul, apoi open banking, apoi transferul bancar și reînnoirile. Dacă ceva se comporta greșit, raza de impact era o singură metodă de plată, nu magazinul.

O migrare pe care nimeni nu o poate vedea este o migrare pe care o poți da înapoi. Fă calea nouă live lângă cea veche înainte de a o face singura.

Oferă oamenilor mai mult de un mod de a plăti

Adevărata taxă a vechiului checkout nu era tehnologia, era fundătura: un client fără acea unică opțiune suportată pur și simplu pleca. Așa că scopul noului gateway era amploarea. Peste v4 am adăugat plată cu cardul, plată directă bancă-la-bancă prin open banking cu băncile mari din România și o opțiune de transfer bancar — trei moduri familiare de a plăti acolo unde, efectiv, exista unul singur.

Mai multe metode înseamnă însă mai multă suprafață — iar prima problemă spinoasă a venit direct din una dintre ele.

Semnătura trebuie să se potrivească cu bytes pe care nu i-ai scris tu

Primul bug spinos după lansarea live a mințit despre propria cauză. Confirmările de plată — callback-urile IPN server-to-server ale PayU — erau respinse ca având o semnătură invalidă, dar doar pe comenzile RCA. Rovinietele plătite cu cardul se confirmau fără probleme. Orice instinct spune „merchant-ul RCA este configurat greșit".

Nu era merchant-ul. PayU semnează bytes-ii bruți ai corpului request-ului, iar un verificator naiv face hash pe corpul pe care l-a re-encodat din JSON-ul parsat. Cele două string-uri sunt identice până în momentul în care payload-ul poartă un byte non-ASCII — un nume românesc cu o diacritică, o adresă cu ș sau ț. Re-encodarea îl normalizează diferit, hash-ul diverge, verificarea eșuează. Comenzile RCA pur și simplu purtau acele date mai des, așa că un bug de forma conținutului purta un costum de forma merchant-ului.

$bodyHash = md5($rawRequestBody);          // what PayU actually signed
$bodyHash = md5(json_encode($parsedBody)); // a different string the moment a "ș" appears

Dacă re-serializezi un payload înainte de a-i verifica semnătura, verifici encoder-ul tău, nu expeditorul. Fă hash pe bytes-ii care au sosit.

Confirmări care se finalizează, reînnoiri care nu întreabă de două ori

Două dintre cele mai valoroase câștiguri au fost cele mai puțin spectaculoase. Confirmările se finalizează acum cap-coadă prin handler-ul IPN v4 în loc să eșueze în tăcere, așa că comenzile plătite nu mai rămân blocate și nimeni din echipă nu își mai petrece după-amiaza reconciliind plăți manual. Iar rovinietele se reînnoiesc automat — șoferii rămân acoperiți fără să revină pentru a recumpăra de la zero, ceea ce transformă discret o achiziție unică în venit recurent. Pe lângă asta, catalogul magazinului a crescut, astfel încât poate vinde mai mult decât înainte, totul pe aceleași șine de plată securizate.

Securizează și back office-ul

O platformă de plăți este la fel de sigură ca și conturile din spatele ei. Partea de admin stă acum în spatele autentificării cu doi factori: un cod de unică folosință trimis pe email operatorului la login, cu o cale de retrimitere pentru când email-ul întârzie. Este o suprafață mică în comparație cu checkout-ul, dar este suprafața care poate mișca bani și date, așa că a primit aceeași seriozitate ca tot ce este orientat către client.

Ce a livrat efectiv

Cap-coadă: checkout migrat pe PayU v4 alături de gateway-ul vechi cu zero downtime; plăți cu cardul, bancă-la-bancă și prin transfer acolo unde cândva exista efectiv o singură opțiune; confirmări IPN care se finalizează fiabil în loc să eșueze în tăcere; reînnoiri automate ale rovinietelor; un catalog mai larg; și autentificare cu doi factori pe back office.

Lecția care s-a dovedit profitabilă peste tot: livrează pe sistemul live câte o piesă izolată pe rând și verifică în raport cu bytes-ii care chiar sosesc. Migrarea fără o întrerupere și vânarea unui bug de semnătură care arăta către suspectul greșit au devenit amândouă mai simple în momentul în care nimic nu a intrat live în mare și nimic nu a avut încredere într-o copie re-encodată a ceea ce a semnat expeditorul.