Blog ·
Cincisprezece lucruri pe care o migrare de facturare nu le poate lua cu ea
O migrare de facturare nu este o copiere de date. Cincisprezece omisiuni măsurate, banii ca bani întregi și amprenta drept singura cheie folosibilă.
De Provibr
Mutarea unei afaceri de găzduire dintr-un panou de facturare în altul seamănă cu o copiere de date și nu este așa ceva. Două panouri care știu amândouă ce înseamnă un client, un serviciu și o factură rămân totuși în dezacord asupra a ceea ce conțin acele obiecte, iar dezacordul nu este niciodată simetric: sursa știe de obicei mai mult decât acceptă API-ul destinației. O migrare care se preface că nu este așa scrie documente care nu se potrivesc cu cele primite deja de client.
Primul lucru pe care îl produce acest instrument nu sunt, prin urmare, date. Este o listă cu ceea ce nu va ajunge.
Banii sunt bani întregi, iar conversia citește text
Ambele panouri livrează o sumă ca număr JSON (9.95) sau ca text zecimal ("9.95"). Conversia evidentă este înmulțirea cu o sută.
Conversia evidentă este greșită. Ca număr în virgulă mobilă pe 64 de biți, 9,95 înseamnă 9,949999999999999289, iar 9.95 * 100.0 dă 994,9999999999999. Trunchiat, asta înseamnă 994 de bani: un ban mai puțin decât spune factura. Pe un server de joc este o supărare de rotunjire. Într-o contabilitate este un document incorect și iese la iveală când cineva închide exercițiul.
De aceea modelul canonic poartă o sumă ca număr întreg de bani plus un cod de monedă, iar conversia numără cifre în forma textuală în loc să treacă printr-un număr în virgulă mobilă. Mai mult de două zecimale se rotunjesc depărtându-se de zero în loc să fie trunchiate, fiindcă o jumătate de ban trunchiată face să dispară o linie de factură. Notația științifică este respinsă din capul locului: a sprijini 1e2 pe jumătate ar transforma o factură de o sută de euro într-un singur ban, iar refuzul costă mai puțin decât ingeniozitatea. Există o limită superioară de 2^53 de bani, fiindcă celălalt capăt al firului socotește în numere JavaScript, iar o valoare pe care noi o acceptăm și care acolo nu mai poate fi reprezentată exact este o abatere tăcută în loc de o eroare.
Cincisprezece omisiuni, fiecare măsurată
Opt sunt valabile pentru orice migrare de facturare, iar șapte sunt proprii primei rute, de la FOSSBilling la Paymenter. Toate cincisprezece au fost măsurate față de containere în funcțiune și nu preluate din documentație.
Cele opt generale sunt acelea pe care le ghicești de îndată ce le rostești. Parolele clienților sunt amprente calculate cu cheia aplicației sursă și nu ar funcționa nicăieri altundeva, iar punctul de acces pentru utilizatori al destinației cere o parolă nouă de cel puțin opt caractere. Cardurile și mandatele salvate sunt jetoane care aparțin contului panoului sursă la furnizorul de plăți; a le lua ar însemna ca o debitare să plece dintr-un cont pe care clientul nu l-a semnat niciodată. Configurația procesatorilor poartă chei și adrese de webhook ale panoului vechi, deci nu este citită niciodată. Șabloanele de e-mail sunt scrise în limbajul de șabloane al propriului panou, iar un șablon preluat produce mesaje cu marcaje nesubstituite. Atașamentele tichetelor există ca fișier pe discul panoului sursă, fără niciun punct de acces care să le întoarcă conținutul. Contabilitatea afiliaților nu are un corespondent cu același înțeles, iar preluarea ei ar face să renască comisioane deja plătite. Codurile de reducere lipsite de istoricul lor de utilizare sunt de-a dreptul greșite: un cod limitat la zece folosiri, deja folosit de patru ori, ajunge ca un cod cu zece folosiri rămase. Secretele de autentificare în doi pași sunt cifrate pe fiecare panou și nu sunt întoarse nicăieri.
Cele șapte proprii rutei sunt jumătatea interesantă, fiindcă sunt toate limite ale API-ului de administrare al destinației și nu ale destinației în sine.
| Ce rămâne în urmă | De ce, măsurat |
|---|---|
| Firmă, adresă, cod fiscal, telefon, limbă | Cererea de creare validează prenumele, numele, adresa de e-mail, parola, data verificării și rolul, iar controlerul creează exact mulțimea validată. Tot restul cade. |
| Numerele de factură | invoices.number este unic în baza de date, dar nu apare în niciuna dintre cele două reguli de validare și în nicio resursă; nu poate fi scris și nu poate fi recitit. |
| Taxa pe o linie | O linie de factură acceptă descriere, cantitate, preț și o referință. Nu există câmp de taxă. |
| Catalogul de produse | Controlerul de produse are doar index și show, iar fișierul de drepturi acordă numai view pe produse și categorii. |
| Înregistrările de plată | API-ul nu publică niciun punct de acces pentru plăți sau tranzacții. |
| Un serviciu deja scadent | Data de expirare este validată ca fiind astăzi sau mai târziu. |
| Identitățile personalului | Un mesaj de tichet cere un user_id care există ca utilizator, iar personalul sursei nu este utilizator al destinației. |
Două dintre acele rânduri poartă o decizie care merită numită.
Numerele de factură pleacă spre stderr. Fiindcă destinația își atribuie propria serie și nu acceptă și nu întoarce numărul vechi, migratorul scrie câte o linie per factură pe ieșirea de eroare, cu numărul sursei pe ea, acolo unde jurnalul agentului o culege. Nu este elegant. Este cinstit: numărul se află pe un document pe care clientul îl ține în mână, iar singurul loc în care îl mai putem pune este un jurnal pe care administratorul îl controlează.
O factură cu taxă este blocată în loc să fie scrisă. Destinația calculează taxa din propriile setări, deci aceeași factură ar căpăta acolo un total diferit de documentul deja trimis. Scrierea ei totuși produce o a doua înregistrare contradictorie a unei singure operațiuni. Planul așază astfel de facturi sub tax_not_carried și merge mai departe.
Distincția aceasta străbate tot instrumentul. Un produs lipsă blochează serviciul în loc să îl claseze altundeva. Blocarea este un rezultat; ghicitul nu este.
Sursa răspunde HTTP 200 atunci când eșuează
API-ul de administrare al FOSSBilling răspunde la un apel eșuat cu starea 200 și un corp de forma {"result":null,"error":{"message":"...","code":9999}}. Cine se uită numai la codul de stare citește o eroare ca răspuns gol, iar o migrare care nu deosebește gol de eșuat va sincroniza într-o zi o contabilitate până la a o goli. Există exact o funcție care deschide acel plic și se uită la error înainte de result.
Cititorul este în plus doar de citire prin construcție, iar asta este un test în loc de o promisiune. Fiecare metodă pe care o poate apela este o constantă numită care se termină în get sau get_list, șapte cu totul, iar un test citește fișierul sursă și confruntă fiecare apel client.get( cu acea listă. Un cititor care ar căpăta un apel _create ar fi un test roșu și nu o descoperire ulterioară.
Încă o ciudățenie măsurată: un produs FOSSBilling se întoarce cu toate cele șapte perioade de facturare bifate ca active și cu prețul zero, orice ar vinde panoul. Prețul este singurul semnal folosibil despre care ciclu este real.
Idempotență pe o amprentă, fiindcă nu există cheie externă
Modul obișnuit de a face o migrare reluabilă este scrierea unui identificator propriu pe destinație și căutarea lui data următoare. Paymenter nu oferă unde să îl pui: nu există câmp de referință externă care să supraviețuiască unei scrieri și să se întoarcă la citire.
Identitatea este așadar o amprentă calculată din conținut, de ambele părți.
- Un client este adresa lui de e-mail, cu litere mici.
- Un serviciu este e-mailul, numele produsului, prețul în bani, moneda și cantitatea.
- O factură este e-mailul, moneda, data scadenței și liniile ei, cu totalul adunat din acele linii.
- Un tichet este e-mailul plus subiectul.
Două dintre ele poartă un avertisment în cod. Amprenta serviciului exclude în mod deliberat următoarea dată de scadență, fiindcă destinația respinge o dată din trecut și păstrează una goală, iar atunci același serviciu ar avea la reluare o amprentă diferită de cea de la scriere și ar fi creat a doua oară: un câmp pe care destinația are voie să îl schimbe nu poate face parte din cheie. Totalul facturii se adună din linii în loc să fie citit din câmpul de total al sursei, fiindcă pe destinație acea valoare este un calcul și nu o coloană, iar două părți care adună diferit produc o cheie care nu se potrivește niciodată. Liniile sunt sortate înainte de compunere, fiindcă ordinea în care se întorc este cea a identificatorilor lor, care nu trebuie să fie ordinea scrierii dacă una dintre ele a căzut.
Cheia tichetului este cea slabă și așa scrie: două tichete ale aceluiași client cu exact același subiect se contopesc într-unul. Includerea primului mesaj ar rezolva asta și costă un al doilea apel per tichet pe destinație.
Trei feluri de a trata o coliziune
Când un obiect există deja pe destinație, rularea face unul din trei lucruri, iar alegerea se face dinainte: skip lasă în pace rândul existent, overwrite_empty completează numai câmpurile care sunt goale acolo, iar stop oprește întreaga rulare înainte de orice scriere.
Măsurat față de un FOSSBilling 0.8 real și un Paymenter real: o simulare a planificat patru clienți, patru servicii, patru facturi, cinci produse și două tichete; rularea a creat patru clienți, patru servicii, patru facturi și două tichete, cu renumărare ulterioară. Uciderea procesului la jumătatea drumului cu kill -9 și pornirea lui din nou a creat zero obiecte și a lăsat destinația identică octet cu octet. Cu stop a urmat o oprire fără nicio scriere. Cu overwrite_empty au fost completate doar câmpurile de nume goale, iar cele deja completate au rămas neatinse. O factură într-o monedă neacceptată s-a întors ca un 422 care numea câmpul, a fost notată drept currency_unknown, iar rularea a continuat.
De ce ruta nu se numește live
Instrumentul este terminat și măsurat, iar ruta tot nu poate fi pornită din panou. Motivul este o graniță care s-a dovedit importantă: acreditările unei migrări se citesc din tabelul de integrări, iar coloana de tip a acelui tabel este registrul infrastructurii pe care o operăm noi. O intrare live acolo poartă o pagină de documentație, un parcurs de configurare, o schemă de acreditări, un test de conexiune și nouă traduceri, iar ea apare pe site ca ceva cu care se conduc servere. Un panou de facturare nu este așa ceva. Îl citim o dată și nu îl mai atingem niciodată.
A numi ruta live ar pune deasupra ei o insignă care spune altceva decât ecranul de dedesubt, așa că există o a treia stare, adapter_ready, care spune ceea ce este adevărat: adaptorul există și a fost măsurat față de panouri reale, iar locul în care se introduc cele două panouri încă nu există. Acel loc este o rundă de sine stătătoare, fiindcă salvarea unui panou de facturare drept conexiune este o decizie de schemă și nu un produs secundar al unui instrument de migrare.
Din munca de migrare a Provibr. De ce un API care răspunde 200 la o eroare este o clasă de defect și nu un accident se află în de ce un 200 de la Proxmox nu este un rezultat, iar de ce o migrare care a sărit în tăcere este mai rea decât una care a eșuat se află în migrarea care nu a rulat niciodată.