e-bon
e-bon.ro
Arhitectură

Validarea conținutului comenzilor

Regulile de validare aplicate de e-bon fiecărei comenzi înainte de a ajunge la AMEF — cote de TVA, tipuri de plată, motive de stornare, echilibrul articole-vs-plăți și răspunsul de eroare primit când o comandă este respinsă.

Fiecare comandă trimisă către API-ul e-bon este validată înainte de a ajunge la dispozitiv. Dacă conținutul nu trece validarea, cererea este respinsă cu HTTP 400 și o listă structurată de erori la nivel de câmp — nicio comandă nu este pusă în așteptare, niciun bon fiscal nu este tipărit.

Această pagină documentează regulile, ca să poți construi cereri care trec din prima.

Validează după constantele fiscale din România

Trei seturi fixe de valori sunt aplicate pe comenzile legate de bonuri.

Cote de TVA

Câmpul vatRate de pe fiecare articol trebuie să fie una dintre cotele valabile în România:

CotăFolosire
0Articole scutite
9Cotă redusă (alimente, cărți, medicamente)
11Cotă redusă (HoReCa, cazare)
21Cotă standard

Orice altă valoare este respinsă cu:

vatRate must be one of: 0, 9, 11, 21

Tipuri de plată

Fiecare intrare din payments[] trebuie să declare un type din acest set:

ValoareSens
cashPlată cu numerar
cardPlată cu cardul
voucherTichet de masă, voucher cadou etc.
otherOrice alt mijloc de plată acceptat

Mesaj de respingere:

type must be one of: cash, card, voucher, other

Motive de stornare

La emiterea unui bon de stornare, reason trebuie să fie unul dintre:

ValoareCând se folosește
operator_errorEroare a casierului pe bonul original
refundClientul a returnat marfa sau a anulat serviciul
tax_base_reductionCorecție a bazei de impozitare (de ex. discount ulterior)

Mesaj de respingere:

reason must be one of: operator_error, refund, tax_base_reduction

Echilibrează articolele cu plățile

Pentru bonurile care includ atât items, cât și payments, totalurile trebuie să coincidă:

sum(items[i].quantity * items[i].price)  ==  sum(payments[i].amount)

Ambele părți sunt rotunjite la 2 zecimale. O diferență de până la ±0,01 RON este tolerată pentru a absorbi rotunjirile de virgulă mobilă — nu este o marjă de discount.

Dacă totalurile nu coincid, cererea este respinsă cu un mesaj de forma:

Payment total (99.99) does not match items total (100.00)
Regula se aplică pe fiecare cerere print_receipt. Se aplică și pe print_reversal_receiptatunci când incluzi payments — vezi nota despre asimetrie de mai jos.

Aplică regulile per tip de comandă

Comenzile de mai jos transportă conținut care este validat câmp cu câmp. Comenzile care nu apar aici fie nu primesc conținut, fie au forma fixată de schema cererii.

Cel mai pretențios conținut. Combină validarea articolelor, validarea plăților și verificarea de echilibru articole-vs-plăți.

CâmpRegulă
itemsTablou obligatoriu, ne-vid.
items[i].nameObligatoriu, șir ne-vid.
items[i].quantityObligatoriu, număr pozitiv (> 0).
items[i].priceNumăr obligatoriu.
items[i].vatRateObligatoriu, trebuie să fie o cotă de TVA din România.
paymentsTablou obligatoriu, ne-vid.
payments[i].typeObligatoriu, trebuie să fie un tip de plată valid.
payments[i].amountObligatoriu, număr pozitiv (> 0).
Articole vs plățiTotalurile trebuie să coincidă în limita de ±0,01 RON.

void_receipt

CâmpRegulă
receiptIdObligatoriu, șir ne-vid.

cash_in / cash_out

Ambele comenzi folosesc același validator.

CâmpRegulă
amountObligatoriu, număr pozitiv (> 0).
descriptionOpțional; dacă este furnizat, trebuie să fie șir.

set_datetime

CâmpRegulă
datetimeOpțional; dacă este furnizat, trebuie să fie un șir ISO-8601.

non_fiscal_receipt

CâmpRegulă
linesTablou obligatoriu cu cel puțin o intrare; fiecare element trebuie să fie șir.
headerOpțional; dacă este furnizat, trebuie să fie șir.
CâmpRegulă
logoObligatoriu, șir ne-vid.

set_vat_rates

CâmpRegulă
ratesTablou obligatoriu cu cel puțin o intrare.
rates[i].nameObligatoriu, șir ne-vid.
rates[i].percentageObligatoriu, număr ne-negativ (>= 0).
Această comandă nu restricționează procentajele la setul de TVA românesc — trimite un set arbitrar de cote către dispozitiv. Firmware-ul AMEF decide ce acceptă.
CâmpRegulă
headerTablou obligatoriu de șiruri.
footerTablou obligatoriu de șiruri.

Tablourile vide sunt acceptate.

set_operator

CâmpRegulă
operatorIdÎntreg pozitiv obligatoriu (>= 1).
nameObligatoriu, șir ne-vid.
passwordOpțional; dacă este furnizat, trebuie să fie șir.
CâmpRegulă
uniqueSaleNumberObligatoriu, șir ne-vid.
originalReceiptNumberObligatoriu, șir ne-vid.
originalReceiptDateTimeObligatoriu, trebuie să fie un șir de dată parsabil ca ISO-8601.
fiscalMemorySerialNumberObligatoriu, șir ne-vid.
originalZReportNumberOpțional; dacă este furnizat, trebuie să fie șir.
reasonObligatoriu, trebuie să fie un motiv de stornare valid.
itemsTablou obligatoriu, ne-vid (aceleași reguli per articol ca la print_receipt).
paymentsOpțional. Dacă este furnizat, fiecare intrare urmează aceleași reguli ca la print_receipt, iar verificarea de echilibru se aplică.
Articolele sunt obligatorii pe ambele. Plățile diferă.
  • La print_receipt, payments este obligatoriu.
  • La print_reversal_receipt, payments este opțional — unele stornări (de exemplu, corectarea unei tipăriri greșite care nu a încasat bani) nu au deloc o componentă de plată.
Regula de echilibru articole-vs-plăți se aplică ambelor, dar pe stornare rulează doar atunci când payments este prezent și ne-vid.

Interpretează răspunsul de eroare la validare

Când conținutul nu trece validarea, API-ul returnează HTTP 400 cu acest corp:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid command payload",
    "details": [
      { "field": "items[0].vatRate", "message": "vatRate must be one of: 0, 9, 11, 21" },
      { "field": "payments", "message": "Payment total (99.99) does not match items total (100.00)" }
    ]
  }
}

details enumeră fiecare câmp care a eșuat, în ordinea verificării. Pentru a localiza eroarea în propria interfață, mapează pe field + code, nu pe textul message — mesajele sunt doar în engleză și pot evolua.

Endpoint-ul dedicat de stornare folosește aceeași formă, dar message-ul de la nivel superior este "Invalid reversal receipt payload" în loc de "Invalid command payload".

Mai departe