{
  "info": {
    "_postman_id": "6f1d2c3b-8a47-4e2b-9c1d-5e7f0a9b3c21",
    "name": "PosFix — API terminal",
    "description": "API-ul HTTP al casei de marcat PosFix. Prin el, sistemul tău (ERP, POS terț, magazin online) emite bonuri fiscale, încasează și retrage numerar, scoate rapoarte, tipărește și corectează plăți cu cardul pe casa comerciantului.\n\nAcelași API rulează pe **Windows (PC-ECC)** și pe **Android**. Unde o cerere, un câmp sau un cod de eroare diferă, descrierea cererii are secțiunea „Windows și Android\".\n\n## Conectare\n\n| Mod | `baseUrl` | Când îl folosești |\n|---|---|---|\n| Rețea locală | `http://<ip-casă>:4567` | Sistemul tău e în aceeași rețea cu casa. |\n| Remote (relay) | `https://api.posfix.md/api/relay/<publicId>` | De oriunde. Adresa o generează PosFix pentru casa respectivă; o vezi și pe casă, în fereastra modului Provider. |\n\nCererile sunt identice în ambele moduri. Prin relay:\n\n- casa trebuie să fie pornită și în modul Provider;\n- relay-ul așteaptă răspunsul cel mult **75 de secunde**;\n- limita e **300 de cereri pe minut** per adresă IP;\n- se transmit mai departe doar antetele `Content-Type`, `Accept` și `Idempotency-Key`.\n\n**Erorile relay-ului au altă formă** decât cele ale casei — fără `success`/`error`:\n\n| HTTP | Corp | Când |\n|---|---|---|\n| 401 | `{\"errorCode\":\"AUTH_INVALID_ENDPOINT\",\"message\":\"…\"}` | Adresa relay nu există sau a fost revocată. |\n| 401 | `{\"errorCode\":\"AUTH_MISSING_KEY\",…}` / `AUTH_INVALID_KEY` | Cheia lipsește sau e greșită. |\n| 502 | `{\"errorCode\":\"PROVIDER_OFFLINE\",…}` | Casa e oprită sau nu e în modul Provider. |\n| 504 | `{\"errorCode\":\"RELAY_TIMEOUT\",…}` | Casa n-a răspuns în 75 s. Operațiunea poate să fi reușit: reia cu **același** `Idempotency-Key`. |\n| 429 | `{\"error\":\"rate_limited\",\"retry_after_seconds\":60}` | Peste 300 de cereri pe minut. Antet `Retry-After: 60`. |\n\n## Autentificare\n\nAntetul `X-API-Key` pe fiecare cerere. Excepții: `GET /api/health` și `GET /api/catalog/health`. Aceeași cheie merge pe rețeaua locală și prin relay. O găsești pe casă: **Setări → Developer → Arată cheia API**.\n\n## Convenții\n\n- Corpul e JSON, cu `Content-Type: application/json`.\n- Numele câmpurilor se scriu **exact** în camelCase. Pe Windows, în afară de `POST /api/sales`, `Amount` nu e același câmp cu `amount`.\n- Sumele se trimit ca **numere JSON** (`25.5`), nu ca text (`\"25.50\"`). Sunt în lei, cu cel mult 2 zecimale; cantitățile au cel mult 3.\n- Datele de filtrare au forma `YYYY-MM-DD`; momentele din răspuns sunt ISO 8601 UTC.\n- În tabelele de răspuns, `?` după tip înseamnă că valoarea poate fi `null`.\n- Câmpurile necunoscute din cerere se ignoră.\n\n## Formatul răspunsului\n\n```json\n{ \"success\": true, \"data\": { }, \"error\": null, \"timestamp\": \"2026-09-17T09:30:00.000Z\" }\n```\n\n```json\n{\n  \"success\": false,\n  \"data\": null,\n  \"error\": {\n    \"code\": \"VALIDATION_ERROR\",\n    \"message\": \"Request validation failed\",\n    \"details\": [ { \"field\": \"items[0].unitPrice\", \"code\": \"ITEM_PRICE_INVALID\", \"message\": \"Unit price must be > 0 with max 2 decimal places\" } ]\n  },\n  \"timestamp\": \"2026-09-17T09:30:00.000Z\"\n}\n```\n\n`details[]` apare doar la `VALIDATION_ERROR`. **Decide după `error.code`**; `details[].code` diferă între platforme pe câteva câmpuri (tabelul de mai jos). `message` e pentru oameni și se poate schimba.\n\nRăspunsuri **fără** acest înveliș:\n\n- `/api/catalog/*` întorc JSON brut.\n- **Windows:** `POST /api/sales` cu JSON invalid sau cu o valoare de tip greșit → `400` fără corp; alt `Content-Type` decât JSON → `415` fără corp; parametru de interogare numeric invalid → `400` fără corp; rută inexistentă → `404` fără corp; metodă greșită → `405`.\n- **Android:** rută inexistentă sau metodă greșită → `404 NOT_FOUND`, cu înveliș.\n- Erorile relay-ului (mai sus).\n\n## Coduri de eroare\n\n| HTTP | `error.code` | Când | Platformă |\n|---|---|---|---|\n| 400 | `INVALID_JSON` | Corpul nu e un obiect JSON valid. | ambele (Windows: nu la `POST /api/sales`) |\n| 400 | `VALIDATION_ERROR` | Câmpuri greșite; vezi `details[]`. **Android:** structură greșită (lipsesc `items`/`payments`, tip JSON greșit) → fără `details`, mesaj `Invalid request structure`. | ambele |\n| 401 | `MISSING_API_KEY` | Lipsește `X-API-Key`. | ambele |\n| 401 | `INVALID_API_KEY` | Cheia nu corespunde. | ambele |\n| 402 | `CARD_PAYMENT_DECLINED` | Banca a refuzat, clientul a anulat sau terminalul bancar n-a răspuns. | ambele |\n| 402 | `CARD_PAYMENT_ERROR` | Eroare la terminalul bancar la anulare / rambursare. | Windows |\n| 404 | `SALE_NOT_FOUND` | Vânzarea nu există pe casă. | ambele |\n| 404 | `NOT_FOUND` | Rută inexistentă, sau plata originală negăsită la anulare / rambursare. | Android |\n| 404 | `CASHIER_NOT_FOUND` | Casierul cerut nu există. | Windows |\n| 409 | `TERMINAL_NOT_FISCAL` | Casa nu e fiscalizată sau autorizată la SFS. | ambele |\n| 409 | `MEV_CREDENTIALS_MISSING` | Lipsesc credențialele SFS. | ambele |\n| 409 | `FISCAL_DAY_EXPIRED` | Au trecut 24 de ore de la primul bon după ultimul Z. Scoate raportul Z. | ambele |\n| 409 | `INSUFFICIENT_CASH_BALANCE` | Retragerea sau restul depășesc numerarul din sertar. | ambele |\n| 409 | `DUPLICATE_LIMIT_REACHED` | Duplicatul bonului s-a tipărit deja. | ambele |\n| 409 | `IDEMPOTENCY_KEY_REUSE_MISMATCH` | Aceeași `Idempotency-Key` cu alt corp. | ambele |\n| 409 | `NO_CASHIER_CONFIGURED` | Nu există casier activ și casa nu poate crea unul. | Windows |\n| 409 | `NO_OPEN_SHIFT` | Operațiune de numerar fără tură deschisă. | Windows |\n| 429 | `BUSY` | Altă operațiune fiscală ține casa ocupată de peste 30 s. **Windows:** antet `Retry-After: 5`. | ambele |\n| 500 | `INTERNAL_ERROR` | Eroare neașteptată pe casă. **Android:** și un refuz SFS la depunere / retragere. | ambele |\n| 500 | `PRINT_ERROR` | Imprimanta a raportat o eroare. | ambele |\n| 500 | `CARD_PAYMENT_ERROR` | Eroare la terminalul bancar (inclusiv refuzul unei anulări). | Android |\n| 502 | `MEV_ERROR` | SFS a respins operațiunea sau n-a răspuns. Bonul **nu** s-a emis. | ambele |\n| 503 | `MEV_UNAVAILABLE` | **Android:** platforma PosFix nu răspunde (loturi de livrare). | Android |\n| 503 | `PRINTER_UNAVAILABLE` | Imprimanta e deconectată. | ambele |\n| 503 | `CARD_TERMINAL_UNAVAILABLE` | Plată cu card cerută, dar casa n-are terminal bancar disponibil. | ambele |\n| 4xx/5xx | `PLATFORM_ERROR` | Eroarea platformei, transmisă mai departe (loturi de livrare). | Android |\n\n## Coduri de validare — `details[].code`\n\n| Cerere | Câmp | Windows | Android |\n|---|---|---|---|\n| vânzare | `items` / `payments` goale | `ITEMS_REQUIRED` / `PAYMENTS_REQUIRED` | fără `details` |\n| vânzare | `items[].name` | `ITEM_NAME_INVALID` | `ITEM_NAME_INVALID` |\n| vânzare | `items[].unitPrice` | `ITEM_PRICE_INVALID` | `ITEM_PRICE_INVALID` |\n| vânzare | `items[].quantity` | `ITEM_QUANTITY_INVALID` | `ITEM_QUANTITY_INVALID` |\n| vânzare | `items[].vatCode` | `ITEM_VAT_CODE_INVALID`, `UNKNOWN_VAT_CODE` | `ITEM_VAT_CODE_INVALID`, `UNKNOWN_VAT_CODE` |\n| vânzare | reducere / adaos pe poziție | `ITEM_DISCOUNT_PERCENT_INVALID`, `ITEM_DISCOUNT_ABSOLUTE_INVALID`, `ITEM_MARKUP_PERCENT_INVALID`, `ITEM_MARKUP_ABSOLUTE_INVALID` | la fel |\n| vânzare | reducere / adaos pe bon | `CART_DISCOUNT_PERCENT_INVALID`, `CART_DISCOUNT_ABSOLUTE_INVALID`, `CART_MARKUP_PERCENT_INVALID`, `CART_MARKUP_ABSOLUTE_INVALID` | la fel |\n| vânzare | calculul poziției | `ITEM_NET_AMOUNT_NEGATIVE`, `ITEM_VAT_AMOUNT_NEGATIVE` | la fel |\n| vânzare | `payments[]` | `PAYMENT_TYPE_INVALID`, `PAYMENT_AMOUNT_INVALID`, `PAYMENT_RRN_INVALID`, `PAYMENT_SUM_MISMATCH` | la fel |\n| vânzare | rest fără numerar | — | `NON_CASH_CHANGE_NOT_ALLOWED` |\n| vânzare | `amountReceived` / `change` | `AMOUNT_RECEIVED_INVALID`, `INSUFFICIENT_PAYMENT`, `CHANGE_INVALID`, `CHANGE_MISMATCH`, `CALCULATION_TOTAL_ZERO` | la fel |\n| vânzare | `delivery` | `PHONE_FORMAT_INVALID`, `EMAIL_FORMAT_INVALID`, `CUSTOMER_NAME_INVALID`, `LANGUAGE_INVALID` | la fel |\n| vânzare | `cashier` | `CASHIER_EMPTY`, `CASHIER_ID_INVALID`, `CASHIER_NAME_TOO_LONG` | — (câmpul nu există) |\n| numerar | `amount` | `CASH_AMOUNT_INVALID`, `CASH_AMOUNT_DECIMALS` | `AMOUNT_INVALID` |\n| numerar | `reason` | `CASH_REASON_TOO_LONG` | `REASON_TOO_LONG` |\n| raport periodic | filtrul | `REQUEST_BODY_REQUIRED`, `PERIODIC_RANGE_REQUIRED` | fără `details` |\n| raport periodic | date | `DATE_INVALID`, `DATE_RANGE_INVERTED` | `DATE_RANGE_INVALID`, `DATE_TO_FUTURE` |\n| raport periodic | numere de Z | — | `REPORT_NUMBER_INVALID`, `REPORT_RANGE_INVALID` |\n| tipărire | `lines` | `LINES_REQUIRED`, `MAX_COLUMNS`, `MAX_LENGTH` | `LINE_TEXT_TOO_LONG` (structură greșită: fără `details`) |\n| tipărire | `feedLines` | — (se limitează la 0–10) | `FEED_LINES_INVALID` |\n| card | corpul | `REQUEST_BODY_REQUIRED`, `CARD_AMOUNT_INVALID`, `CARD_AMOUNT_DECIMALS`, `CARD_CHEQUE_NUMBER_REQUIRED`, `CARD_CHEQUE_NUMBER_INVALID`, `CARD_RRN_REQUIRED`, `CARD_RRN_INVALID`, `CARD_AUTH_CODE_INVALID` | fără `details` (motivul e în `message`) |\n\n## Operațiunile fiscale merg una câte una\n\nCasa execută o singură operațiune fiscală odată. Cererile care vin între timp așteaptă cel mult **30 de secunde**, apoi primesc `429 BUSY`.\n\n- **Android:** așteaptă vânzarea, depunerea, retragerea și rapoartele X / Z.\n- **Windows:** așteaptă **orice** POST — și tipărirea, duplicatul, raportul periodic, corecțiile de card.\n\nUn răspuns la vânzare poate dura mult: așteptarea casei, clientul la terminalul bancar (**Windows:** până la 180 s) și SFS (**Android:** până la 3 încercări, la 15 s una de alta). Setează în clientul tău un timeout de cel puțin 4 minute pe `POST /api/sales`.\n\n## Reluarea sigură: `Idempotency-Key`\n\nPune o cheie unică (un GUID nou) pe fiecare POST care schimbă ceva. Casa salvează răspunsul sub cheia aceea.\n\n- **N-ai primit niciun răspuns** (rețea căzută, timeout, `504 RELAY_TIMEOUT`) → retrimite **cu aceeași cheie și același corp**. Dacă operațiunea s-a făcut, primești răspunsul ei, cu antetul `X-Idempotency-Replayed: true`, nu un bon dublu.\n- **Ai primit o eroare** → retrimite **cu o cheie nouă**. Casa salvează și răspunsurile de eroare; cu aceeași cheie ai primi aceeași eroare, fără ca operațiunea să mai ruleze. **Windows** salvează inclusiv `429 BUSY`.\n- **Nu refolosi o cheie pe alt endpoint.** Cheia nu ține cont de adresă: aceeași cheie cu același corp trimisă la alt endpoint întoarce răspunsul primului.\n- Două cereri **simultane** cu aceeași cheie nouă rulează amândouă. Nu trimite în paralel aceeași operațiune.\n\nExemplu: trimiți vânzarea cu cheia `8d1f…`; conexiunea cade. Retrimiți cu `8d1f…` și primești `200` cu `X-Idempotency-Replayed: true` — bonul exista deja.\n\nAntetul de răspuns `X-Request-Id` (pe POST) e id-ul intrării din `GET /api/audit/requests`.\n\n## Nu există bon offline prin API\n\nDacă SFS nu răspunde, vânzarea eșuează cu `502 MEV_ERROR` și bonul **nu** se emite. Câmpurile `isOffline` și `offlineFiscalCode` rămân în răspuns pentru compatibilitate, dar sunt mereu `false` / `null`.\n\n## Tura\n\nNu trebuie să deschizi tura: casa o deschide singură la prima vânzare sau operațiune de numerar. Ziua fiscală se închide cu raportul Z. După 24 de ore de la primul bon de după ultimul Z, operațiunile fiscale răspund `409 FISCAL_DAY_EXPIRED` până scoți raportul Z.\n\n## Tabele de referință\n\n**Tipuri de plată (`payments[].type`) — coduri ACPS, trimise ca text:**\n\n| Cod | Plată | Cheie în `paymentTotals` pe Windows |\n|---|---|---|\n| `\"1\"` | Numerar | `CASH` |\n| `\"2\"` | Card bancar | `CARD` |\n| `\"3.1\"` | Voucher | `VOUCHER` |\n| `\"3.2\"` | Cec / certificat valoric | `CHECK` |\n| `\"3.3\"` | Tichet cu valoare prestabilită | `TICKET` |\n| `\"5\"` | Tichet de masă electronic | `MEAL_TICKET` |\n| `\"6\"` | Abonament | `SUBSCRIPTION` |\n| `\"7\"` | Alt instrument de plată | — (nu apare) |\n| `\"8.1\"` | Credit | `CREDIT` |\n| `\"8.2\"` | Leasing | `LEASING` |\n| `\"8.3\"` | Avans | `ADVANCE` |\n| `\"8.4\"` | Arvună | `DEPOSIT` |\n| `\"8.5\"` | Gaj | `PLEDGE` |\n| `\"8.8\"` | Compensare | `COMPENSATION` |\n| `\"8.9\"` | Alt mod | `OTHER` |\n\nPe Android, `paymentTotals` folosește chiar codul ACPS drept cheie. Denumiri ca `\"CASH\"` **nu** sunt acceptate în cerere.\n\n**Coduri TVA (`items[].vatCode`):** `A`, `B`, `C`, `D`, `E` — cotele configurate pe casă (`GET /api/vat-rates`); `_` — fără TVA.\n\n**Operațiuni fiscale (`GET /api/fiscal/operations`):** `operationType` = `receipt`, `refund`, `cashIn`, `cashOut`, `xReport`, `zReport`; `status` = `pending`, `sent`, `success`, `failed`, `offline`.\n\n## Variabilele colecției\n\n- `baseUrl`, `apiKey` — le completezi tu.\n- `saleId` — se completează din `Listă vânzări` (cea mai recentă vânzare).\n- `lotId` — se completează după crearea unui lot.\n- `today`, `tomorrow`, `monthStart` — se calculează înaintea fiecărei cereri.\n\nPentru datele comerciantului din cloud (catalog, stoc, comenzi, rapoarte Z sincronizate) folosește colecția **PosFix — API public de integrare**.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "apikey",
    "apikey": [
      {
        "key": "key",
        "value": "X-API-Key",
        "type": "string"
      },
      {
        "key": "value",
        "value": "{{apiKey}}",
        "type": "string"
      },
      {
        "key": "in",
        "value": "header",
        "type": "string"
      }
    ]
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "const pad = (n) => String(n).padStart(2, '0');",
          "const fmt = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;",
          "const now = new Date();",
          "const next = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);",
          "pm.collectionVariables.set('today', fmt(now));",
          "pm.collectionVariables.set('tomorrow', fmt(next));",
          "pm.collectionVariables.set('monthStart', fmt(now).slice(0, 8) + '01');"
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "http://192.168.1.50:4567",
      "description": "Rețea locală: http://<ip-casă>:4567 · Remote: https://api.posfix.md/api/relay/<publicId>"
    },
    {
      "key": "apiKey",
      "value": "",
      "description": "Pe casă: Setări → Developer → Arată cheia API"
    },
    {
      "key": "saleId",
      "value": "",
      "description": "Se completează din „Listă vânzări”"
    },
    {
      "key": "lotId",
      "value": "",
      "description": "Se completează după crearea unui lot"
    },
    {
      "key": "today",
      "value": "",
      "description": "Calculat automat (YYYY-MM-DD)"
    },
    {
      "key": "tomorrow",
      "value": "",
      "description": "Calculat automat (YYYY-MM-DD)"
    },
    {
      "key": "monthStart",
      "value": "",
      "description": "Calculat automat (prima zi a lunii)"
    }
  ],
  "item": [
    {
      "name": "0. Conectare și configurare",
      "description": "Începe de aici: verifică legătura, apoi citește cotele TVA și casierii de pe casă.",
      "item": [
        {
          "name": "Stare casă",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/health",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "health"
              ]
            },
            "description": "Verifică dacă serverul casei răspunde. Nu cere cheie. Începe de aici.\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `status` | string | **Android:** `ready` / `not_fiscal`. **Windows:** `ready` / `degraded` / `not_fiscal` / `error`. |\n| `fiscalStatus` | string | **Android:** la fel ca `status`. **Windows:** `ready` / `no_shift` / `not_authorized` / `not_fiscal` / `unknown`. `no_shift` e normal înainte de prima vânzare. |\n| `hasOpenShift` | boolean | **Windows:** există tură deschisă. **Android:** ziua fiscală nu a expirat. |\n| `version` | string? | Versiunea aplicației de casă. |\n| `terminalId` | string? | **Android:** numărul de înregistrare SFS al casei. **Windows:** numărul terminalului. |\n| `platform` | string | `android` / `windows`. |\n| `hasCardTerminal` | boolean | Terminalul bancar e conectat și activ. |\n| `cardTerminalStatus` | string | `connected` / `disconnected`. |\n| `deviceIp` | string? | Adresa IP a casei în rețeaua locală. |\n| `serverPort` | integer | Portul serverului (implicit 4567). |\n| `uptime` | integer | Secunde de la pornirea serverului API. |\n| `serverStartedAt` | string? | Momentul pornirii serverului, ISO 8601 UTC. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 500 | `INTERNAL_ERROR` | **Android:** eroare la citirea stării. **Windows:** răspunde tot 200, cu `status: error`. |\n\n### De reținut\n\n- `status: ready` nu garantează că o vânzare trece. **Android:** API-ul acceptă operațiuni fiscale abia după ce casa a avut cel puțin o operațiune fiscală reușită; până atunci răspunde `409 TERMINAL_NOT_FISCAL`.",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Casa răspunde', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 — casă pregătită",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/health",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "health"
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"status\": \"ready\",\n    \"version\": \"1.0.1+5\",\n    \"fiscalStatus\": \"ready\",\n    \"hasOpenShift\": true,\n    \"terminalId\": \"T-12345\",\n    \"serverPort\": 4567,\n    \"uptime\": 3600,\n    \"serverStartedAt\": \"2026-09-17T08:30:00.000Z\",\n    \"deviceIp\": \"192.168.1.50\",\n    \"platform\": \"android\",\n    \"hasCardTerminal\": true,\n    \"cardTerminalStatus\": \"connected\"\n  },\n  \"error\": null,\n  \"timestamp\": \"2026-09-17T09:30:00.000Z\"\n}"
            },
            {
              "name": "502 — relay: casa nu e în modul Provider",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/health",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "health"
                  ]
                }
              },
              "status": "Bad Gateway",
              "code": 502,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"errorCode\": \"PROVIDER_OFFLINE\",\n  \"message\": \"Terminalul nu e în Provider mode / offline\"\n}"
            }
          ]
        },
        {
          "name": "Cote TVA",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/vat-rates",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "vat-rates"
              ]
            },
            "description": "Codurile TVA active pe casă, cu cotele lor. Folosește-le în `items[].vatCode`.\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `merchantIsVatPayer` | boolean | Comerciantul e plătitor de TVA. |\n| `rates[].code` | string | Codul: `A`–`E` sau `_`. |\n| `rates[].percent` | number | Cota, în procente. |\n| `rates[].name` | string? | Denumirea cotei. |\n| `rates[].description` | string? | Descriere. |\n| `rates[].isSystemDefault` | boolean | Cotă implicită SFS. **Android:** mereu `false`. |\n| `rates[].sortOrder` | integer | Ordinea de afișare; lista vine sortată după ea. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 500 | `INTERNAL_ERROR` | Eroare la citire. |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n\n### Windows și Android\n\n- **Windows:** dacă pe casă nu există nicio cotă sincronizată, lista întoarce valori implicite (`A` 20, `B` 8, `C` 0, `_` 0). Vânzarea nu le folosește: fără cotă reală, un cod dă `UNKNOWN_VAT_CODE` dacă nu trimiți `vatPercent`."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/vat-rates",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "vat-rates"
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"merchantIsVatPayer\": true,\n    \"rates\": [\n      {\n        \"code\": \"A\",\n        \"percent\": 20,\n        \"name\": \"Standard\",\n        \"description\": \"Cota standard\",\n        \"isSystemDefault\": true,\n        \"sortOrder\": 1\n      },\n      {\n        \"code\": \"B\",\n        \"percent\": 8,\n        \"name\": \"Redusă\",\n        \"description\": \"Cota redusă\",\n        \"isSystemDefault\": true,\n        \"sortOrder\": 2\n      },\n      {\n        \"code\": \"_\",\n        \"percent\": 0,\n        \"name\": \"Fără TVA\",\n        \"description\": \"Scutit\",\n        \"isSystemDefault\": true,\n        \"sortOrder\": 6\n      }\n    ]\n  },\n  \"error\": null,\n  \"timestamp\": \"2026-09-17T09:30:00.000Z\"\n}"
            }
          ]
        },
        {
          "name": "Casieri",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/cashiers",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "cashiers"
              ]
            },
            "description": "Casierii activi de pe casă. `data` e direct lista.\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `[].id` | string | Id-ul casierului. **Windows:** îl poți trimite în `cashier.id` la vânzare. |\n| `[].fullName` | string | Numele complet. |\n| `[].role` | string | Rolul (de exemplu `Cashier`, `Manager`). |\n| `[].isActive` | boolean | Mereu `true`. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 500 | `INTERNAL_ERROR` | Eroare la citire. |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n\n### Windows și Android\n\n- **Windows:** după prima vânzare prin API pe o casă fără casieri, apare „Casier Provider”, creat automat.\n- **Android:** vânzările prin API se atribuie singure primului manager activ, apoi primului casier activ."
          }
        }
      ]
    },
    {
      "name": "1. Vânzări",
      "description": "Bonuri fiscale: emitere, listă, detaliu, duplicat.",
      "item": [
        {
          "name": "Vânzare simplă — numerar",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Opțional. Aceeași cheie + același corp → răspunsul salvat (antet X-Idempotency-Replayed: true). Aceeași cheie + alt corp → 409 IDEMPOTENCY_KEY_REUSE_MISMATCH."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/sales",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "sales"
              ]
            },
            "description": "Emite un bon fiscal: îl înregistrează la SFS și, opțional, îl tipărește. Totalul îl calculează casa din poziții, reduceri și adaosuri.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `items[]` | array | da | Cel puțin o poziție. |\n| `items[].name` | string | da | 1–100 caractere. `ITEM_NAME_INVALID` |\n| `items[].unitPrice` | number | da | Preț unitar **cu TVA**, lei. > 0, max 2 zecimale. `ITEM_PRICE_INVALID` |\n| `items[].quantity` | number | da | > 0, max 3 zecimale (`1.254` kg). `ITEM_QUANTITY_INVALID` |\n| `items[].vatCode` | string | da | `A`–`E` sau `_`. `ITEM_VAT_CODE_INVALID`. Fără `vatPercent`, codul trebuie să existe activ pe casă: `UNKNOWN_VAT_CODE`. **Windows:** implicit `A`. |\n| `items[].vatPercent` | number | nu | Cota TVA. Dacă lipsește, se ia cota codului de pe casă. Nu se verifică. |\n| `items[].discountPercent` | number | nu | Reducere pe poziție, 0–100. `ITEM_DISCOUNT_PERCENT_INVALID` |\n| `items[].discountAbsolute` | number | nu | Reducere pe poziție, lei, ≥ 0. `ITEM_DISCOUNT_ABSOLUTE_INVALID` |\n| `items[].markupPercent` | number | nu | Adaos pe poziție, %, ≥ 0. `ITEM_MARKUP_PERCENT_INVALID` |\n| `items[].markupAbsolute` | number | nu | Adaos pe poziție, lei, ≥ 0. `ITEM_MARKUP_ABSOLUTE_INVALID` |\n| `items[].id` | string | nu | Id-ul produsului în sistemul tău; ajunge pe vânzarea sincronizată cu platforma. |\n| `items[].barcode`, `items[].sku` | string | nu | **Windows:** dacă lipsește `id`, produsul se caută în catalogul casei după ele. |\n| `items[].cpvCode` | string | nu | Cod CPV. |\n| `items[].allowDecimals` | boolean | nu | **Android:** unitatea bonului, `KG` (`true`) sau `BUC` (implicit). **Windows:** ignorat. |\n| `items[].lineKind` | string | nu | `Goods` (implicit) — marfă; `CustomerAdvance` — avans încasat pe o comandă, nu marfă vândută. |\n| `items[].sourceOrderId` | string (GUID) | nu | Comanda pe care se încasează avansul. **Windows:** un GUID invalid → `400` fără corp. |\n| `cartDiscount.percent` / `.absolute` | number | nu | Reducere pe tot bonul: 0–100 % sau lei ≥ 0. Se aplică **după** reducerile pe poziții. `CART_DISCOUNT_*_INVALID` |\n| `cartMarkup.percent` / `.absolute` | number | nu | Adaos pe tot bonul, ≥ 0. `CART_MARKUP_*_INVALID` |\n| `payments[]` | array | da | Cel puțin o plată. |\n| `payments[].type` | string | da | Cod ACPS exact, **ca text** (`\"1\"`, nu `1`). `PAYMENT_TYPE_INVALID` |\n| `payments[].amount` | number | da | > 0, max 2 zecimale. La numerar trimite suma **dată de client**, cu rest cu tot. `PAYMENT_AMOUNT_INVALID` |\n| `payments[].rrn` | string | nu | Max 12 caractere. La `\"2\"` (card) și `\"5\"` (tichet de masă): **lipsă** → casa încasează pe terminalul bancar; **prezent** → plata e deja încasată în altă parte. `PAYMENT_RRN_INVALID` |\n| `amountReceived` | number | da | Cât a dat clientul. = suma plăților (±0,01): `PAYMENT_SUM_MISMATCH`. ≥ total: `INSUFFICIENT_PAYMENT`. Obligatoriu și la plata doar cu card. |\n| `change` | number | nu | Restul. Dacă îl trimiți, = `amountReceived − total` (±0,01): `CHANGE_MISMATCH`. |\n| `delivery.customerPhone` | string | nu | Format internațional: `+37369123456`. `PHONE_FORMAT_INVALID` |\n| `delivery.customerEmail` | string | nu | `EMAIL_FORMAT_INVALID` |\n| `delivery.customerName` | string | nu | Max 60 caractere. `CUSTOMER_NAME_INVALID` |\n| `delivery.language` | string | nu | `ro` (implicit), `ru`, `en`. `LANGUAGE_INVALID` |\n| `printReceipt` | boolean | nu | Tipărește bonul. Implicit `true`. |\n| `cashier.id` | string (GUID) | nu | **Doar Windows.** Casierul căruia i se atribuie vânzarea pe platformă. `CASHIER_ID_INVALID` |\n| `cashier.name` | string | nu | **Doar Windows.** Numele casierului, max 100. `CASHIER_NAME_TOO_LONG`; obiect fără `id` și `name`: `CASHIER_EMPTY` |\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `saleId` | string | Id-ul vânzării pe casă. **Windows:** nu e găsit de detaliu și de duplicat; pentru ele folosește `sales[].id` din `GET /api/sales`. |\n| `receiptNumber` | integer | Numărul bonului în ziua fiscală. |\n| `reportNumber` | integer | Numărul raportului Z curent. |\n| `fiscalCode` | string? | Codul fiscal al bonului, primit de la SFS. |\n| `mevId` | string? | Id-ul tranzacției la SFS. |\n| `fiscalOperationId` | string? | Id-ul operațiunii fiscale pe casă. **Windows:** egal cu `fiscalCode`. |\n| `qrCodeData` | string? | Adresa de verificare a bonului pe site-ul SFS. |\n| `total` | number | Total de plată, lei, cu TVA, după toate reducerile. |\n| `subtotal` | number | **Android:** înainte de orice reducere. **Windows:** după reducerile pe poziții, înainte de cele pe bon. |\n| `amountReceived` | number | Suma primită. |\n| `change` | number | Restul calculat. |\n| `vatBreakdown.<cod>` | object | Pe fiecare cod TVA: `code`, `percent`, `vatAmount`, `taxableBase` (fără TVA), `totalWithVat`. |\n| `paymentBreakdown[]` | array | `type`, `amount`, `rrn`? |\n| `cardPaymentResult` | object? | Doar când **casa** a încasat cardul: `rrn`, `authCode`, `cardMask`, `chequeNumber`. Păstrează `chequeNumber` pentru anulare și `rrn` pentru rambursare. La mai multe carduri: **Android** — primul, **Windows** — ultimul. |\n| `receiptDelivery` | object? | `smsSent`, `emailSent`: `true` dacă ai trimis telefon / email. Arată cererea, nu confirmarea. |\n| `receiptStructure` | object | Bonul, gata de afișat: `header` (comerciant, IDNO, adresă, nr. bon, casier, dată), `body` (`items[]` cu ajustări, subtotal, reduceri, total, TVA), `footer` (plăți, rest, cod fiscal, QR, numere ECC/SFS, `isDuplicate`). |\n| `mevResponseXml` | string? | Rezumatul răspunsului SFS. Informativ; nu-l parsa. |\n| `createdAt` | string | Momentul bonului, ISO 8601 UTC. |\n| `purchaseId`, `serverSaleId` | string? | Nu le folosi: vânzarea se sincronizează cu platforma după răspuns. **Windows:** `purchaseId` e un id fără legătură cu platforma. |\n| `isOffline`, `offlineFiscalCode` | boolean, string? | Mereu `false` / `null` (nu există bon offline prin API). |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 400 | `VALIDATION_ERROR` | Câmpuri greșite (vezi coloana „Reguli”). **Android:** lipsesc `items` / `payments` sau un câmp are tip JSON greșit → fără `details`. |\n| 400 | `INVALID_JSON` | **Android:** corpul nu e obiect JSON. **Windows:** JSON invalid → `400` fără corp. |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n| 402 | `CARD_PAYMENT_DECLINED` | Cardul a fost refuzat, anulat de client sau terminalul bancar n-a răspuns. |\n| 409 | `TERMINAL_NOT_FISCAL` / `MEV_CREDENTIALS_MISSING` | Casa nu e fiscalizată sau nu are credențiale SFS. |\n| 409 | `FISCAL_DAY_EXPIRED` | Au trecut 24 de ore de la primul bon după ultimul Z. Scoate raportul Z. |\n| 409 | `INSUFFICIENT_CASH_BALANCE` | Restul e mai mare decât numerarul din sertar. |\n| 409 | `NO_CASHIER_CONFIGURED` | **Windows:** niciun casier activ, iar casa nu poate crea unul. |\n| 409 | `IDEMPOTENCY_KEY_REUSE_MISMATCH` | Aceeași cheie de idempotență, alt corp. |\n| 415 | — | **Windows:** `Content-Type` diferit de `application/json`. |\n| 429 | `BUSY` | Altă operațiune fiscală n-a eliberat casa în 30 s. Reîncearcă peste câteva secunde, cu cheie de idempotență nouă. |\n| 500 | `CARD_PAYMENT_ERROR` | **Android:** eroare neașteptată la terminalul bancar. |\n| 500 | `INTERNAL_ERROR` | Eroare neașteptată pe casă. |\n| 502 | `MEV_ERROR` | SFS a respins bonul sau n-a răspuns. Bonul **nu** s-a emis. **Android** reîncearcă singur de 3 ori, la 15 s. |\n| 503 | `CARD_TERMINAL_UNAVAILABLE` | Plată cu card fără `rrn`, dar casa n-are terminal bancar disponibil. |\n\n### Windows și Android\n\n- **Windows:** la o poziție se aplică doar **prima** ajustare nenulă, în ordinea `discountPercent`, `discountAbsolute`, `markupPercent`, `markupAbsolute`. Pe bon, `cartMarkup` se ignoră dacă există `cartDiscount`. Trimite cel mult o ajustare pe poziție și nu combina reducere cu adaos pe bon.\n- **Windows:** `delivery` se validează, dar SMS-ul / emailul **nu** se trimite. **Android:** se trimite după ce vânzarea se sincronizează cu platforma.\n- **Windows:** doar `POST /api/sales` acceptă sume trimise ca text și nume de câmpuri în altă capitalizare. Trimite oricum numere și camelCase.\n- **Android:** restul se dă doar când există plată în numerar (`NON_CASH_CHANGE_NOT_ALLOWED`).\n\n### De reținut\n\n- Restul cere numerar fizic: o vânzare cu rest mai mare decât numerarul din sertar e refuzată (`409 INSUFFICIENT_CASH_BALANCE`) înainte de încasare. Depune fond de rest cu `POST /api/cash/in`.\n- Tipărirea nu întârzie răspunsul și nu îl schimbă: o imprimantă deconectată nu face vânzarea să eșueze.\n- Dacă o plată cu card încasată de casă e urmată de o eroare (`502 MEV_ERROR` sau alt card refuzat), plata cu cardul **nu** se anulează automat. Verifică pe terminalul bancar și anuleaz-o cu `POST /api/card/void`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"items\": [\n    {\n      \"name\": \"Cafea espresso\",\n      \"unitPrice\": 25,\n      \"quantity\": 2,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20\n    }\n  ],\n  \"payments\": [\n    {\n      \"type\": \"1\",\n      \"amount\": 50\n    }\n  ],\n  \"amountReceived\": 50,\n  \"printReceipt\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 — bon emis",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  },
                  {
                    "key": "Idempotency-Key",
                    "value": "{{$guid}}",
                    "description": "Opțional. Aceeași cheie + același corp → răspunsul salvat (antet X-Idempotency-Replayed: true). Aceeași cheie + alt corp → 409 IDEMPOTENCY_KEY_REUSE_MISMATCH."
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/sales",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "sales"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"items\": [\n    {\n      \"name\": \"Cafea espresso\",\n      \"unitPrice\": 25,\n      \"quantity\": 2,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20\n    }\n  ],\n  \"payments\": [\n    {\n      \"type\": \"1\",\n      \"amount\": 50\n    }\n  ],\n  \"amountReceived\": 50,\n  \"printReceipt\": true\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"saleId\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n    \"purchaseId\": null,\n    \"serverSaleId\": null,\n    \"fiscalOperationId\": \"5d6e7f80-9a1b-4c2d-8e3f-4a5b6c7d8e9f\",\n    \"receiptNumber\": 12,\n    \"reportNumber\": 67,\n    \"fiscalCode\": \"7F3A9C21\",\n    \"mevId\": \"7F3A9C21\",\n    \"isOffline\": false,\n    \"offlineFiscalCode\": null,\n    \"total\": 50,\n    \"subtotal\": 50,\n    \"amountReceived\": 50,\n    \"change\": 0,\n    \"vatBreakdown\": {\n      \"B\": {\n        \"code\": \"B\",\n        \"percent\": 20,\n        \"vatAmount\": 8.33,\n        \"taxableBase\": 41.67,\n        \"totalWithVat\": 50\n      }\n    },\n    \"paymentBreakdown\": [\n      {\n        \"type\": \"1\",\n        \"amount\": 50,\n        \"rrn\": null\n      }\n    ],\n    \"qrCodeData\": \"https://mev.sfs.md/receipt-verifier/J403001234/50/1726565400123/2026-09-17\",\n    \"mevResponseXml\": \"<mevResponse><id>7F3A9C21</id><code>0</code><state>1</state></mevResponse>\",\n    \"receiptDelivery\": null,\n    \"cardPaymentResult\": null,\n    \"receiptStructure\": {\n      \"header\": {\n        \"merchantName\": \"SRL Exemplu\",\n        \"merchantIdno\": \"1003600000000\",\n        \"merchantAddress\": \"mun. Chișinău, str. Ștefan cel Mare 1\",\n        \"documentType\": \"BON FISCAL\",\n        \"receiptNumber\": 12,\n        \"cashierName\": \"Ana Rusu\",\n        \"dateTime\": \"2026-09-17T09:30:00.000Z\",\n        \"subdivisionCode\": null\n      },\n      \"body\": {\n        \"items\": [\n          {\n            \"name\": \"Cafea espresso\",\n            \"quantity\": 2,\n            \"unit\": \"BUC\",\n            \"unitPrice\": 25,\n            \"originalCost\": 50,\n            \"lineTotal\": 50,\n            \"vatCode\": \"B\",\n            \"vatPercent\": 20,\n            \"adjustmentType\": null,\n            \"adjustmentPercent\": null,\n            \"adjustmentAmount\": null\n          }\n        ],\n        \"subtotal\": 50,\n        \"cartDiscountAmount\": 0,\n        \"cartMarkupAmount\": 0,\n        \"total\": 50,\n        \"vatBreakdown\": {\n          \"B\": {\n            \"code\": \"B\",\n            \"percent\": 20,\n            \"vatAmount\": 8.33,\n            \"taxableBase\": 41.67,\n            \"totalWithVat\": 50\n          }\n        }\n      },\n      \"footer\": {\n        \"payments\": [\n          {\n            \"type\": \"1\",\n            \"amount\": 50\n          }\n        ],\n        \"amountReceived\": 50,\n        \"change\": 0,\n        \"mevId\": \"7F3A9C21\",\n        \"fiscalCode\": \"7F3A9C21\",\n        \"qrCodeData\": \"https://mev.sfs.md/receipt-verifier/J403001234/50/1726565400123/2026-09-17\",\n        \"eccNumber\": \"0001\",\n        \"factoryNumber\": null,\n        \"serialNumber\": null,\n        \"sfsNumber\": \"J403001234\",\n        \"isOffline\": false,\n        \"isDuplicate\": false\n      }\n    },\n    \"createdAt\": \"2026-09-17T09:30:00.000Z\"\n  },\n  \"error\": null,\n  \"timestamp\": \"2026-09-17T09:30:00.000Z\"\n}"
            },
            {
              "name": "400 — validare",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  },
                  {
                    "key": "Idempotency-Key",
                    "value": "{{$guid}}",
                    "description": "Opțional. Aceeași cheie + același corp → răspunsul salvat (antet X-Idempotency-Replayed: true). Aceeași cheie + alt corp → 409 IDEMPOTENCY_KEY_REUSE_MISMATCH."
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/sales",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "sales"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"items\": [\n    {\n      \"name\": \"Cafea espresso\",\n      \"unitPrice\": 25,\n      \"quantity\": 2,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20\n    }\n  ],\n  \"payments\": [\n    {\n      \"type\": \"1\",\n      \"amount\": 50\n    }\n  ],\n  \"amountReceived\": 50,\n  \"printReceipt\": true\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"data\": null,\n  \"error\": {\n    \"code\": \"VALIDATION_ERROR\",\n    \"message\": \"Request validation failed\",\n    \"details\": [\n      {\n        \"field\": \"items[0].unitPrice\",\n        \"code\": \"ITEM_PRICE_INVALID\",\n        \"message\": \"Unit price must be > 0 with max 2 decimal places\"\n      },\n      {\n        \"field\": \"payments\",\n        \"code\": \"PAYMENT_SUM_MISMATCH\",\n        \"message\": \"Sum of payments (50) does not match amountReceived (10)\"\n      }\n    ]\n  },\n  \"timestamp\": \"2026-09-17T09:30:00.000Z\"\n}"
            },
            {
              "name": "409 — rest mai mare decât numerarul din sertar",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  },
                  {
                    "key": "Idempotency-Key",
                    "value": "{{$guid}}",
                    "description": "Opțional. Aceeași cheie + același corp → răspunsul salvat (antet X-Idempotency-Replayed: true). Aceeași cheie + alt corp → 409 IDEMPOTENCY_KEY_REUSE_MISMATCH."
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/sales",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "sales"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"items\": [\n    {\n      \"name\": \"Cafea espresso\",\n      \"unitPrice\": 25,\n      \"quantity\": 2,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20\n    }\n  ],\n  \"payments\": [\n    {\n      \"type\": \"1\",\n      \"amount\": 50\n    }\n  ],\n  \"amountReceived\": 50,\n  \"printReceipt\": true\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"data\": null,\n  \"error\": {\n    \"code\": \"INSUFFICIENT_CASH_BALANCE\",\n    \"message\": \"Insufficient change in drawer. Available: 0.00 MDL, Required: 7.50 MDL\"\n  },\n  \"timestamp\": \"2026-09-17T09:30:00.000Z\"\n}"
            },
            {
              "name": "502 — SFS n-a confirmat bonul",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  },
                  {
                    "key": "Idempotency-Key",
                    "value": "{{$guid}}",
                    "description": "Opțional. Aceeași cheie + același corp → răspunsul salvat (antet X-Idempotency-Replayed: true). Aceeași cheie + alt corp → 409 IDEMPOTENCY_KEY_REUSE_MISMATCH."
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/sales",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "sales"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"items\": [\n    {\n      \"name\": \"Cafea espresso\",\n      \"unitPrice\": 25,\n      \"quantity\": 2,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20\n    }\n  ],\n  \"payments\": [\n    {\n      \"type\": \"1\",\n      \"amount\": 50\n    }\n  ],\n  \"amountReceived\": 50,\n  \"printReceipt\": true\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Bad Gateway",
              "code": 502,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"data\": null,\n  \"error\": {\n    \"code\": \"MEV_ERROR\",\n    \"message\": \"MEV nu a confirmat bonul. Bonul NU a fost emis și vânzarea nu a fost înregistrată.\"\n  },\n  \"timestamp\": \"2026-09-17T09:30:00.000Z\"\n}"
            }
          ]
        },
        {
          "name": "Vânzare cu rest",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Opțional. Aceeași cheie + același corp → răspunsul salvat (antet X-Idempotency-Replayed: true). Aceeași cheie + alt corp → 409 IDEMPOTENCY_KEY_REUSE_MISMATCH."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/sales",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "sales"
              ]
            },
            "description": "Clientul dă 20 lei pe un bon de 12,50.\n\n- `payments[].amount` și `amountReceived` = 20,00 (suma dată de client).\n- `change` e opțional; dacă îl trimiți, trebuie să fie exact 7,50.\n- În sertar trebuie să fie cel puțin 7,50 lei numerar, altfel răspunsul e `409 INSUFFICIENT_CASH_BALANCE`.\n\nCâmpurile, răspunsul și erorile: vezi **Vânzare simplă — numerar**.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"items\": [\n    {\n      \"name\": \"Pâine albă\",\n      \"unitPrice\": 12.5,\n      \"quantity\": 1,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20\n    }\n  ],\n  \"payments\": [\n    {\n      \"type\": \"1\",\n      \"amount\": 20\n    }\n  ],\n  \"amountReceived\": 20,\n  \"change\": 7.5,\n  \"printReceipt\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Vânzare la kg",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Opțional. Aceeași cheie + același corp → răspunsul salvat (antet X-Idempotency-Replayed: true). Aceeași cheie + alt corp → 409 IDEMPOTENCY_KEY_REUSE_MISMATCH."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/sales",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "sales"
              ]
            },
            "description": "Cantitatea acceptă cel mult 3 zecimale: 1,254 kg × 50,00 = **62,70** lei. Pe Android, `allowDecimals: true` tipărește unitatea `KG`.\n\nCâmpurile, răspunsul și erorile: vezi **Vânzare simplă — numerar**.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"items\": [\n    {\n      \"name\": \"Brânză de vaci\",\n      \"unitPrice\": 50,\n      \"quantity\": 1.254,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20,\n      \"allowDecimals\": true\n    }\n  ],\n  \"payments\": [\n    {\n      \"type\": \"1\",\n      \"amount\": 62.7\n    }\n  ],\n  \"amountReceived\": 62.7,\n  \"printReceipt\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Vânzare complexă — reduceri, plăți mixte, bon pe SMS",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Opțional. Aceeași cheie + același corp → răspunsul salvat (antet X-Idempotency-Replayed: true). Aceeași cheie + alt corp → 409 IDEMPOTENCY_KEY_REUSE_MISMATCH."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/sales",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "sales"
              ]
            },
            "description": "Reducere pe poziție (procent și sumă), reducere pe tot bonul, numerar + card deja încasat (`rrn`) și bonul trimis clientului.\n\n| Poziție | Calcul | Net |\n|---|---|---|\n| Cafea espresso | 2 × 25,00 | 50,00 |\n| Tort Napoleon | 0,5 × 180,00 − 10% | 81,00 |\n| Apă minerală | 3 × 15,00 − 5,00 | 40,00 |\n| **Pozițiile** |  | **171,00** |\n| Reducere pe bon | −10% din 171,00 | −17,10 |\n| **Total** |  | **153,90** |\n\nPlăți: 100,00 numerar + 53,90 card = 153,90 = `amountReceived`.\n\nCâmpurile, răspunsul și erorile: vezi **Vânzare simplă — numerar**.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"items\": [\n    {\n      \"id\": \"prod-001\",\n      \"barcode\": \"4840000000017\",\n      \"name\": \"Cafea espresso\",\n      \"unitPrice\": 25,\n      \"quantity\": 2,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20\n    },\n    {\n      \"id\": \"prod-002\",\n      \"name\": \"Tort Napoleon\",\n      \"unitPrice\": 180,\n      \"quantity\": 0.5,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20,\n      \"discountPercent\": 10,\n      \"allowDecimals\": true\n    },\n    {\n      \"id\": \"prod-003\",\n      \"name\": \"Apă minerală 0,5 L\",\n      \"unitPrice\": 15,\n      \"quantity\": 3,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20,\n      \"discountAbsolute\": 5\n    }\n  ],\n  \"cartDiscount\": {\n    \"percent\": 10\n  },\n  \"payments\": [\n    {\n      \"type\": \"1\",\n      \"amount\": 100\n    },\n    {\n      \"type\": \"2\",\n      \"amount\": 53.9,\n      \"rrn\": \"123456789012\"\n    }\n  ],\n  \"amountReceived\": 153.9,\n  \"delivery\": {\n    \"customerPhone\": \"+37369123456\",\n    \"customerEmail\": \"client@exemplu.md\",\n    \"customerName\": \"Vasile Munteanu\",\n    \"language\": \"ro\"\n  },\n  \"printReceipt\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Vânzare cu card pe terminalul bancar",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Opțional. Aceeași cheie + același corp → răspunsul salvat (antet X-Idempotency-Replayed: true). Aceeași cheie + alt corp → 409 IDEMPOTENCY_KEY_REUSE_MISMATCH."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/sales",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "sales"
              ]
            },
            "description": "Plata `\"2\"` **fără** `rrn`: casa pornește încasarea pe terminalul bancar și așteaptă clientul. Răspunsul vine după ce cardul e acceptat sau refuzat, iar bonul e înregistrat la SFS.\n\nDin răspuns păstrează `cardPaymentResult.chequeNumber` (pentru anulare în aceeași zi) și `cardPaymentResult.rrn` (pentru rambursare după închiderea zilei bancare).\n\n**Windows:** clientul are până la 180 s la terminal. Prin relay, peste 75 s primești `504 RELAY_TIMEOUT` — reia cu aceeași `Idempotency-Key` ca să afli rezultatul.\n\nCardul l-ai încasat deja în aplicația ta sau pe alt terminal? Vezi **Vânzare cu card încasat în altă aplicație (RRN)**.\n\nCâmpurile și erorile: vezi **Vânzare simplă — numerar**.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"items\": [\n    {\n      \"name\": \"Cafea espresso\",\n      \"unitPrice\": 25,\n      \"quantity\": 1,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20\n    }\n  ],\n  \"payments\": [\n    {\n      \"type\": \"2\",\n      \"amount\": 25\n    }\n  ],\n  \"amountReceived\": 25,\n  \"printReceipt\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "402 — card refuzat",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  },
                  {
                    "key": "Idempotency-Key",
                    "value": "{{$guid}}",
                    "description": "Opțional. Aceeași cheie + același corp → răspunsul salvat (antet X-Idempotency-Replayed: true). Aceeași cheie + alt corp → 409 IDEMPOTENCY_KEY_REUSE_MISMATCH."
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/sales",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "sales"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"items\": [\n    {\n      \"name\": \"Cafea espresso\",\n      \"unitPrice\": 25,\n      \"quantity\": 1,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20\n    }\n  ],\n  \"payments\": [\n    {\n      \"type\": \"2\",\n      \"amount\": 25\n    }\n  ],\n  \"amountReceived\": 25,\n  \"printReceipt\": true\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Payment Required",
              "code": 402,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"data\": null,\n  \"error\": {\n    \"code\": \"CARD_PAYMENT_DECLINED\",\n    \"message\": \"Card payment declined: Insufficient funds\"\n  },\n  \"timestamp\": \"2026-09-17T09:30:00.000Z\"\n}"
            },
            {
              "name": "503 — fără terminal bancar",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  },
                  {
                    "key": "Idempotency-Key",
                    "value": "{{$guid}}",
                    "description": "Opțional. Aceeași cheie + același corp → răspunsul salvat (antet X-Idempotency-Replayed: true). Aceeași cheie + alt corp → 409 IDEMPOTENCY_KEY_REUSE_MISMATCH."
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/sales",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "sales"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"items\": [\n    {\n      \"name\": \"Cafea espresso\",\n      \"unitPrice\": 25,\n      \"quantity\": 1,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20\n    }\n  ],\n  \"payments\": [\n    {\n      \"type\": \"2\",\n      \"amount\": 25\n    }\n  ],\n  \"amountReceived\": 25,\n  \"printReceipt\": true\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"data\": null,\n  \"error\": {\n    \"code\": \"CARD_TERMINAL_UNAVAILABLE\",\n    \"message\": \"Card payment requested but card terminal is not available.\"\n  },\n  \"timestamp\": \"2026-09-17T09:30:00.000Z\"\n}"
            }
          ]
        },
        {
          "name": "Vânzare cu card încasat în altă aplicație (RRN)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Opțional. Aceeași cheie + același corp → răspunsul salvat (antet X-Idempotency-Replayed: true). Aceeași cheie + alt corp → 409 IDEMPOTENCY_KEY_REUSE_MISMATCH."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/sales",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "sales"
              ]
            },
            "description": "Cardul a fost deja încasat **în afara casei** — de aplicația ta, de alt terminal bancar sau de POS-ul băncii — iar casa doar emite bonul fiscal. Trimiți plata `\"2\"` (card) sau `\"5\"` (tichet de masă) **cu** `rrn`: casa nu mai pornește terminalul bancar și înregistrează bonul cu plata respectivă.\n\n| Ce se întâmplă |  |\n|---|---|\n| Terminalul bancar | Nu se pornește. Plata se socotește încasată; casa nu verifică RRN-ul la bancă. |\n| Bonul fiscal | Plata apare ca **card** (sau tichet de masă), cu suma trimisă. RRN-ul **nu** se tipărește pe bon și nu pleacă la SFS; nu se tipărește nici chitanța băncii. |\n| Răspunsul | `paymentBreakdown[].rrn` și `receiptStructure.footer.payments[].rrn` întorc RRN-ul trimis. `cardPaymentResult` e `null`: casa n-a încasat nimic. |\n| Anulare și rambursare | Le faci în aplicația sau pe terminalul care a încasat cardul. Nu chema `POST /api/card/void` sau `/refund` pentru această plată: casa caută plata doar printre cele încasate de ea. Pe **Android**, dacă numărul cecului (la anulare) sau RRN-ul (la rambursare) coincide cu al unei plăți a casei, operația se încearcă pe plata aceea: e refuzată (`CARD_PAYMENT_ERROR` sau `CARD_PAYMENT_DECLINED`) sau chiar se execută. Altfel răspunsul e `404 NOT_FOUND`. Pe **Windows**, cererea pleacă la terminalul bancar al casei, care n-a făcut plata. |\n| Platforma | **Windows:** RRN-ul ajunge pe platformă odată cu vânzarea. **Android:** pleacă doar tipul și suma plății. |\n\nPe plată se primesc doar `type`, `amount` și `rrn` (cel mult 12 caractere, altfel `PAYMENT_RRN_INVALID`). Codul de autorizare, cardul mascat și numărul cecului bancar nu se primesc: casa nu le cere și nu le păstrează. Un `rrn` gol (`\"\"`) înseamnă „fără RRN”: casa pornește terminalul bancar.\n\nPoți amesteca plățile: numerar încasat de casă + card încasat de tine (`rrn` doar pe plata cu cardul). Vezi **Vânzare complexă**.\n\nCâmpurile, restul răspunsului și erorile: vezi **Vânzare simplă — numerar**.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"items\": [\n    {\n      \"name\": \"Cafea espresso\",\n      \"unitPrice\": 25,\n      \"quantity\": 1,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20\n    }\n  ],\n  \"payments\": [\n    {\n      \"type\": \"2\",\n      \"amount\": 25,\n      \"rrn\": \"426512345678\"\n    }\n  ],\n  \"amountReceived\": 25,\n  \"printReceipt\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 — bon emis, cardul încasat în altă aplicație",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  },
                  {
                    "key": "Idempotency-Key",
                    "value": "{{$guid}}",
                    "description": "Opțional. Aceeași cheie + același corp → răspunsul salvat (antet X-Idempotency-Replayed: true). Aceeași cheie + alt corp → 409 IDEMPOTENCY_KEY_REUSE_MISMATCH."
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/sales",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "sales"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"items\": [\n    {\n      \"name\": \"Cafea espresso\",\n      \"unitPrice\": 25,\n      \"quantity\": 1,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20\n    }\n  ],\n  \"payments\": [\n    {\n      \"type\": \"2\",\n      \"amount\": 25,\n      \"rrn\": \"426512345678\"\n    }\n  ],\n  \"amountReceived\": 25,\n  \"printReceipt\": true\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"saleId\": \"b2c3d4e5-f6a7-4801-9bcd-ef2345678901\",\n    \"receiptNumber\": 13,\n    \"fiscalCode\": \"8A4B0D32\",\n    \"total\": 25,\n    \"amountReceived\": 25,\n    \"change\": 0,\n    \"paymentBreakdown\": [\n      {\n        \"type\": \"2\",\n        \"amount\": 25,\n        \"rrn\": \"426512345678\"\n      }\n    ],\n    \"cardPaymentResult\": null\n  },\n  \"error\": null,\n  \"timestamp\": \"2026-09-17T09:30:00.000Z\"\n}"
            },
            {
              "name": "400 — RRN prea lung",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  },
                  {
                    "key": "Idempotency-Key",
                    "value": "{{$guid}}",
                    "description": "Opțional. Aceeași cheie + același corp → răspunsul salvat (antet X-Idempotency-Replayed: true). Aceeași cheie + alt corp → 409 IDEMPOTENCY_KEY_REUSE_MISMATCH."
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/sales",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "sales"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"items\": [\n    {\n      \"name\": \"Cafea espresso\",\n      \"unitPrice\": 25,\n      \"quantity\": 1,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20\n    }\n  ],\n  \"payments\": [\n    {\n      \"type\": \"2\",\n      \"amount\": 25,\n      \"rrn\": \"426512345678\"\n    }\n  ],\n  \"amountReceived\": 25,\n  \"printReceipt\": true\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"data\": null,\n  \"error\": {\n    \"code\": \"VALIDATION_ERROR\",\n    \"message\": \"Request validation failed\",\n    \"details\": [\n      {\n        \"field\": \"payments[0].rrn\",\n        \"code\": \"PAYMENT_RRN_INVALID\",\n        \"message\": \"RRN must be max 12 characters\"\n      }\n    ]\n  },\n  \"timestamp\": \"2026-09-17T09:30:00.000Z\"\n}"
            }
          ]
        },
        {
          "name": "Listă vânzări",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/sales?dateFrom={{today}}&dateTo={{today}}&limit=50&offset=0",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "sales"
              ],
              "query": [
                {
                  "key": "dateFrom",
                  "value": "{{today}}",
                  "description": "YYYY-MM-DD. Implicit: azi."
                },
                {
                  "key": "dateTo",
                  "value": "{{today}}",
                  "description": "YYYY-MM-DD, inclusiv. Implicit: mâine."
                },
                {
                  "key": "limit",
                  "value": "50",
                  "description": "1–200. Implicit 50."
                },
                {
                  "key": "offset",
                  "value": "0",
                  "description": "Implicit 0."
                },
                {
                  "key": "status",
                  "value": "",
                  "description": "Starea sincronizării cu platforma (valorile diferă pe platforme).",
                  "disabled": true
                },
                {
                  "key": "cashierId",
                  "value": "",
                  "description": "Android: vânzările unui casier. Windows: nu funcționează (întoarce listă goală).",
                  "disabled": true
                }
              ]
            },
            "description": "Vânzările de pe casă — inclusiv cele făcute din aplicație, nu doar prin API — pe interval de zile, cu paginare. Testul cererii salvează id-ul celei mai recente vânzări în `saleId`.\n\n### Parametri de interogare\n\n| Parametru | Tip | Implicit | Descriere |\n|---|---|---|---|\n| `dateFrom` | string | azi | Prima zi, `YYYY-MM-DD`. Valoare invalidă → implicit, fără eroare. |\n| `dateTo` | string | mâine | Ultima zi, **inclusiv**. |\n| `limit` | integer | 50 | 1–200. **Windows:** valoare nenumerică → `400` fără corp. |\n| `offset` | integer | 0 | Câte rezultate sari. **Android:** negativ → `500`. |\n| `status` | string | — | **Android:** `pending`, `syncedPurchase`, `fiscalSent`, `syncedFiscal`, `failed`. **Windows:** `pending`, `synced`. |\n| `cashierId` | string | — | **Android:** doar vânzările casierului. **Windows:** nu folosi (listă goală). |\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `sales[].id` | string | Id-ul vânzării; îl folosești la detaliu și duplicat. |\n| `sales[].total` | number | Total, lei. **Android:** text (`\"150\"`). |\n| `sales[].subtotal` | number? | **Android:** text. **Windows:** `null`. |\n| `sales[].paymentType` | string | **Android:** denumiri ACPS unite cu „ + ” (`NUMERAR + CARD`). **Windows:** coduri ACPS unite cu virgulă (`1,2`). |\n| `sales[].paymentAmount` | number | Suma plătită. **Android:** text. |\n| `sales[].changeAmount` | number? | Restul. **Android:** text. **Windows:** `null`. |\n| `sales[].status` | string | Starea sincronizării cu platforma (vezi `status` mai sus). **Android:** `failed` = SFS n-a confirmat bonul. |\n| `sales[].cashierName` | string? | Casierul. |\n| `sales[].cashierId` | string? | **Windows:** `null`. |\n| `sales[].fiscalOperationId` | string? | Operațiunea fiscală. |\n| `sales[].purchaseId`, `sales[].serverSaleId` | string? | Id-uri pe platformă. **Windows:** `null`. |\n| `sales[].createdAt` | string | Momentul vânzării. |\n| `total` | integer | Câte vânzări corespund filtrului (înainte de paginare). |\n| `limit`, `offset` | integer | Valorile aplicate. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 500 | `INTERNAL_ERROR` | Eroare la citire. |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n\n### De reținut\n\n- Sumele vin ca **text** pe Android și ca **număr** pe Windows. Citește-le cu un parser care acceptă ambele."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 200) {",
                  "  const rows = (pm.response.json().data || {}).sales || [];",
                  "  const latest = rows.slice().sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)))[0];",
                  "  if (latest) pm.collectionVariables.set('saleId', latest.id);",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Detaliu vânzare",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/sales/{{saleId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "sales",
                "{{saleId}}"
              ]
            },
            "description": "Pozițiile, plățile și datele fiscale ale unei vânzări.\n\n### Parametri de cale\n\n| Parametru | Tip | Descriere |\n|---|---|---|\n| `id` | string | Id-ul din `GET /api/sales` (`sales[].id`). **Android:** merge și `saleId` din răspunsul la vânzare. |\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `id`, `status`, `total`, `subtotal`, `paymentType`, `paymentAmount`, `changeAmount`, `cashierName`, `createdAt` | — | Ca în `GET /api/sales`. **Windows:** `status` e `offline` cât vânzarea nu s-a sincronizat cu platforma, chiar dacă SFS a confirmat-o. |\n| `items[].productName` | string | Denumirea poziției. |\n| `items[].quantity`, `unitPrice`, `originalPrice`, `netAmount`, `discountAmount` | number | Cantitate și sume, lei. **Android:** text. |\n| `items[].vatCode` | string | Codul TVA. |\n| `items[].vatPercent`, `vatAmount`, `markupAmount` | number? | **Android:** text. **Windows:** `null`. |\n| `items[].productId`, `productBarcode`, `productSku` | string? | **Windows:** `null`. |\n| `payments[]` | array | **Windows:** `type`, `amount`, `rrn`?, `chequeNumber`?, `authCode`?, `cardMask`?. **Android:** rânduri brute, cu nume snake_case: `id`, `sale_id`, `payment_type`, `amount` (net: numerarul fără rest), `notes`, `created_at`. |\n| `fiscalOperation` | object? | `mevId`?, `receiptNumber`, `reportNumber`, `status`, `isOffline`, `mevResponseXml`?, `qrCodeData`?. **Windows:** `receiptNumber` e text cu zerouri (`\"0042\"`), `reportNumber` și `mevResponseXml` sunt `null`. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 404 | `SALE_NOT_FOUND` | Id-ul nu există pe casă. |\n| 500 | `INTERNAL_ERROR` | Eroare la citire. |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n\n### De reținut\n\n- **Windows:** `payments[].chequeNumber` și `rrn` de aici sunt datele de care ai nevoie pentru `POST /api/card/void` / `refund`."
          }
        },
        {
          "name": "Duplicat bon",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/sales/{{saleId}}/reprint",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "sales",
                "{{saleId}}",
                "reprint"
              ]
            },
            "description": "Tipărește copia bonului, marcată DUPLICAT, cu datele fiscale ale originalului. Nu trimite nimic la SFS. Se permite **o singură dată** per bon.\n\n### Parametri de cale\n\n| Parametru | Tip | Descriere |\n|---|---|---|\n| `id` | string | Id-ul din `GET /api/sales`. |\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| — | — | nu | Corpul se ignoră. |\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `saleId` | string | Id-ul cerut. |\n| `reprintCount` | integer | Câte duplicate s-au înregistrat (1 după primul). |\n| `lastReprintedAt` | string? | Momentul duplicatului, ISO 8601 UTC. |\n| `lastReprintedBy` | string | Cine l-a cerut (`API`). |\n| `fiscalCode` | string? | Codul fiscal al bonului original. |\n| `receiptNumber` | integer | Numărul bonului original. |\n| `printedCopy` | string | **Doar Android.** `original` dacă originalul nu se tipărise niciodată (atunci nu se consumă duplicatul), altfel `duplicate`. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 400 | `VALIDATION_ERROR` | Id gol. |\n| 404 | `SALE_NOT_FOUND` | Vânzarea nu există pe casă. |\n| 409 | `DUPLICATE_LIMIT_REACHED` | Duplicatul s-a tipărit deja. |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n| 429 | `BUSY` | **Windows:** casa e ocupată cu altă operațiune. |\n| 500 | `INTERNAL_ERROR` | Eroare neașteptată. |\n\n### De reținut\n\n- Duplicatul se consumă **înainte** de tipărire: dacă imprimanta cade, a doua cerere tot dă `DUPLICATE_LIMIT_REACHED`.",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "409 — duplicat deja tipărit",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/sales/{{saleId}}/reprint",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "sales",
                    "{{saleId}}",
                    "reprint"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"data\": null,\n  \"error\": {\n    \"code\": \"DUPLICATE_LIMIT_REACHED\",\n    \"message\": \"A duplicate copy has already been printed for this receipt (SCE Imaginea 39).\"\n  },\n  \"timestamp\": \"2026-09-17T09:30:00.000Z\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "2. Numerar",
      "description": "Depuneri și retrageri. Fiecare emite bon de serviciu la SFS.",
      "item": [
        {
          "name": "Depunere numerar",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Opțional. Aceeași cheie + același corp → răspunsul salvat (antet X-Idempotency-Replayed: true). Aceeași cheie + alt corp → 409 IDEMPOTENCY_KEY_REUSE_MISMATCH."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/cash/in",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "cash",
                "in"
              ]
            },
            "description": "Depune numerar în sertar (de exemplu fondul de rest) și emite bonul de serviciu la SFS.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `amount` | number | da | Suma, lei. > 0, max 2 zecimale. **Windows:** `CASH_AMOUNT_INVALID` / `CASH_AMOUNT_DECIMALS`. **Android:** `AMOUNT_INVALID`. |\n| `reason` | string | nu | Motivul, max 100 caractere. **Windows:** `CASH_REASON_TOO_LONG`. **Android:** `REASON_TOO_LONG`. |\n| `printReceipt` | boolean | nu | Tipărește bonul de serviciu. Implicit `true`. |\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `operationType` | string | `cashIn`. |\n| `amount` | number | Suma, lei. |\n| `oldBalance` / `newBalance` | number | Numerarul din sertar înainte / după, lei. |\n| `operationId` | string | **Android:** id-ul operațiunii pe casă. **Windows:** id-ul cererii către SFS. |\n| `fiscalOperationId` | string? | **Android:** ultima operațiune fiscală de pe casă. **Windows:** codul fiscal al bonului de serviciu. |\n| `receiptNumber`, `reportNumber` | integer | Numărul bonului de serviciu și al raportului Z. **Windows:** `0`. |\n| `mevId`, `mevResponseXml` | string? | Datele SFS. **Windows:** `null`. |\n| `isOffline` | boolean | Mereu `false`. |\n| `createdAt` | string | Momentul operațiunii, ISO 8601 UTC. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 400 | `INVALID_JSON` | Corp lipsă sau invalid. Trimite cel puțin `{\"amount\": …}`. |\n| 400 | `VALIDATION_ERROR` | Sumă sau motiv greșit (vezi mai sus). |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n| 409 | `TERMINAL_NOT_FISCAL` / `FISCAL_DAY_EXPIRED` | Casa nu e fiscalizată sau ziua fiscală a expirat. |\n| 409 | `NO_CASHIER_CONFIGURED` / `NO_OPEN_SHIFT` | **Windows:** nu există casier sau tură. |\n| 409 | `IDEMPOTENCY_KEY_REUSE_MISMATCH` | Aceeași cheie, alt corp. |\n| 429 | `BUSY` | Altă operațiune fiscală n-a eliberat casa în 30 s. Reîncearcă peste câteva secunde, cu cheie de idempotență nouă. |\n| 500 | `INTERNAL_ERROR` | Eroare pe casă. **Android:** și când SFS a respins bonul de serviciu. |\n| 502 | `MEV_ERROR` | **Windows:** SFS a respins sau n-a răspuns. |\n\n### De reținut\n\n- **Nu relua orbește după o eroare 5xx.** Pe Windows, operațiunea poate fi deja înregistrată în sertar. Verifică `cashIn` / `cashOut` în `GET /api/fiscal/daily-summary`, apoi decide.\n- **Windows:** câmpurile se scriu exact `amount`, `reason`, `printReceipt`, iar suma ca număr JSON; `\"100.00\"` dă `INVALID_JSON`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": 500,\n  \"reason\": \"Fond de rest la deschidere\",\n  \"printReceipt\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 — depunere",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  },
                  {
                    "key": "Idempotency-Key",
                    "value": "{{$guid}}",
                    "description": "Opțional. Aceeași cheie + același corp → răspunsul salvat (antet X-Idempotency-Replayed: true). Aceeași cheie + alt corp → 409 IDEMPOTENCY_KEY_REUSE_MISMATCH."
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/cash/in",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "cash",
                    "in"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"amount\": 500,\n  \"reason\": \"Fond de rest la deschidere\",\n  \"printReceipt\": true\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"operationId\": \"0b7c9a3e-1f2d-4c5b-8a9e-3d4f5a6b7c8d\",\n    \"fiscalOperationId\": \"e2f3a4b5-c6d7-4e8f-9a0b-1c2d3e4f5a6b\",\n    \"receiptNumber\": 3,\n    \"reportNumber\": 67,\n    \"mevId\": \"9B21C0DE\",\n    \"isOffline\": false,\n    \"amount\": 500,\n    \"oldBalance\": 0,\n    \"newBalance\": 500,\n    \"operationType\": \"cashIn\",\n    \"mevResponseXml\": \"<mevResponse><id>9B21C0DE</id><code>0</code><state>1</state></mevResponse>\",\n    \"createdAt\": \"2026-09-17T09:30:00.000Z\"\n  },\n  \"error\": null,\n  \"timestamp\": \"2026-09-17T09:30:00.000Z\"\n}"
            }
          ]
        },
        {
          "name": "Retragere numerar",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Opțional. Aceeași cheie + același corp → răspunsul salvat (antet X-Idempotency-Replayed: true). Aceeași cheie + alt corp → 409 IDEMPOTENCY_KEY_REUSE_MISMATCH."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/cash/out",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "cash",
                "out"
              ]
            },
            "description": "Retrage numerar din sertar și emite bonul de serviciu la SFS. Suma nu poate depăși numerarul din sertar.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `amount` | number | da | Suma, lei. > 0, max 2 zecimale. **Windows:** `CASH_AMOUNT_INVALID` / `CASH_AMOUNT_DECIMALS`. **Android:** `AMOUNT_INVALID`. Peste numerarul din sertar: `409 INSUFFICIENT_CASH_BALANCE`. |\n| `reason` | string | nu | Motivul, max 100 caractere. **Windows:** `CASH_REASON_TOO_LONG`. **Android:** `REASON_TOO_LONG`. |\n| `printReceipt` | boolean | nu | Tipărește bonul de serviciu. Implicit `true`. |\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `operationType` | string | `cashOut`. |\n| `amount` | number | Suma, lei. |\n| `oldBalance` / `newBalance` | number | Numerarul din sertar înainte / după, lei. |\n| `operationId` | string | **Android:** id-ul operațiunii pe casă. **Windows:** id-ul cererii către SFS. |\n| `fiscalOperationId` | string? | **Android:** ultima operațiune fiscală de pe casă. **Windows:** codul fiscal al bonului de serviciu. |\n| `receiptNumber`, `reportNumber` | integer | Numărul bonului de serviciu și al raportului Z. **Windows:** `0`. |\n| `mevId`, `mevResponseXml` | string? | Datele SFS. **Windows:** `null`. |\n| `isOffline` | boolean | Mereu `false`. |\n| `createdAt` | string | Momentul operațiunii, ISO 8601 UTC. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 400 | `INVALID_JSON` | Corp lipsă sau invalid. Trimite cel puțin `{\"amount\": …}`. |\n| 400 | `VALIDATION_ERROR` | Sumă sau motiv greșit (vezi mai sus). |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n| 409 | `TERMINAL_NOT_FISCAL` / `FISCAL_DAY_EXPIRED` | Casa nu e fiscalizată sau ziua fiscală a expirat. |\n| 409 | `INSUFFICIENT_CASH_BALANCE` | Suma depășește numerarul din sertar. |\n| 409 | `NO_CASHIER_CONFIGURED` / `NO_OPEN_SHIFT` | **Windows:** nu există casier sau tură. |\n| 409 | `IDEMPOTENCY_KEY_REUSE_MISMATCH` | Aceeași cheie, alt corp. |\n| 429 | `BUSY` | Altă operațiune fiscală n-a eliberat casa în 30 s. Reîncearcă peste câteva secunde, cu cheie de idempotență nouă. |\n| 500 | `INTERNAL_ERROR` | Eroare pe casă. **Android:** și când SFS a respins bonul de serviciu. |\n| 502 | `MEV_ERROR` | **Windows:** SFS a respins sau n-a răspuns. |\n\n### De reținut\n\n- **Nu relua orbește după o eroare 5xx.** Pe Windows, operațiunea poate fi deja înregistrată în sertar. Verifică `cashIn` / `cashOut` în `GET /api/fiscal/daily-summary`, apoi decide.\n- **Windows:** câmpurile se scriu exact `amount`, `reason`, `printReceipt`, iar suma ca număr JSON; `\"100.00\"` dă `INVALID_JSON`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": 200,\n  \"reason\": \"Predare la bancă\",\n  \"printReceipt\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "409 — sold insuficient",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  },
                  {
                    "key": "Idempotency-Key",
                    "value": "{{$guid}}",
                    "description": "Opțional. Aceeași cheie + același corp → răspunsul salvat (antet X-Idempotency-Replayed: true). Aceeași cheie + alt corp → 409 IDEMPOTENCY_KEY_REUSE_MISMATCH."
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/cash/out",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "cash",
                    "out"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"amount\": 200,\n  \"reason\": \"Predare la bancă\",\n  \"printReceipt\": true\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"data\": null,\n  \"error\": {\n    \"code\": \"INSUFFICIENT_CASH_BALANCE\",\n    \"message\": \"Insufficient cash balance. Current: 150.00, requested: 200.00\"\n  },\n  \"timestamp\": \"2026-09-17T09:30:00.000Z\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "3. Rapoarte",
      "description": "X și sumarul zilei nu închid nimic. Z închide ziua fiscală și nu se anulează. Periodicul adună rapoarte Z închise.",
      "item": [
        {
          "name": "Raport X",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/reports/x",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "reports",
                "x"
              ]
            },
            "description": "Situația zilei fiscale curente, trimisă la SFS, fără să închidă ziua și fără să reseteze contoarele.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `printReport` | boolean | nu | Implicit `true`. **Android:** ignorat, raportul se tipărește mereu. **Windows:** respectat doar cu `Content-Type: application/json`. |\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `reportNumber` | integer | **Z:** numărul raportului închis. **X, Android:** numărul raportului X. |\n| `operationId` | string | Id-ul operațiunii fiscale. |\n| `mevId` | string? | Id-ul tranzacției la SFS. |\n| `isOffline` | boolean | SFS n-a confirmat raportul (vezi „Windows și Android”). |\n| `mevResponseXml` | string? | Rezumatul răspunsului SFS. |\n| `generatedAt` | string | ISO 8601 UTC. |\n| `reportData.reportNumber` | integer | Numărul raportului. **Android, la raportul X:** numărul raportului X, nu al lui Z. |\n| `reportData.dailyTotal` | number | Vânzările zilei, lei, cu TVA. |\n| `reportData.dailyTax` | number | TVA-ul zilei, lei. |\n| `reportData.dailyUntaxed` | number | Vânzările pe codul `_` (fără TVA), lei. |\n| `reportData.vatBreakdown.<cod>` | object | Pe fiecare cod TVA: `code`, `percent`, `vatAmount`, `taxableBase` (fără TVA), `gross` (cu TVA). |\n| `reportData.receiptsIssued` | integer | Bonuri emise în zi. |\n| `reportData.lastReceiptNumber` | integer | Numărul ultimului bon. |\n| `reportData.receiptsSentToMev` | integer | Bonuri confirmate de SFS. **Windows:** egal cu `receiptsIssued`. |\n| `reportData.paymentTotals` | object | Încasări pe tip de plată, lei. **Android:** cheia e codul ACPS (`\"1\"`, `\"2\"`…), sume nete (numerar fără rest). **Windows:** chei fixe `CASH`, `CARD`… (vezi tabelul din descrierea colecției). |\n| `reportData.paymentCounts` | object | **Android:** cod ACPS → număr de bonuri. **Windows:** mereu `{}`. |\n| `reportData.cashIn` / `reportData.cashOut` | number | Depuneri / retrageri de numerar, lei. |\n| `reportData.cashBalance` | number | Numerarul din sertar, lei. |\n| `reportData.yearTotal` / `reportData.yearTax` | number? | Vânzări / TVA de la 1 ianuarie, lei. |\n| `reportData.totalSales` | number? | Vânzări brute ale zilei, lei. |\n| `reportData.totalReturns`, `reportData.returnsCount` | number? | Retururi. **Windows:** `null`. |\n| `reportData.cashInCount`, `reportData.cashOutCount` | integer? | Număr de depuneri / retrageri. **Windows:** `null`. |\n| `reportData.generatedAt` | string | Momentul calculului, ISO 8601 UTC. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 400 | `INVALID_JSON` | **Android:** corpul nu e obiect JSON (corpul gol e acceptat). |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n| 409 | `TERMINAL_NOT_FISCAL` / `MEV_CREDENTIALS_MISSING` | Casa nu e fiscalizată sau n-are credențiale SFS. |\n| 429 | `BUSY` | Altă operațiune fiscală n-a eliberat casa în 30 s. Reîncearcă peste câteva secunde, cu cheie de idempotență nouă. |\n| 500 | `INTERNAL_ERROR` | Eroare pe casă. |\n| 502 | `MEV_ERROR` | **Android:** SFS a respins sau n-a răspuns. |\n\n### Windows și Android\n\n- **Windows:** dacă SFS nu răspunde, raportul X vine tot cu `200`, cu `isOffline: true` și `mevId: null`. **Android:** `502 MEV_ERROR`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"printReport\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Sumar zi curentă",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/fiscal/daily-summary",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "fiscal",
                "daily-summary"
              ]
            },
            "description": "Aceleași cifre ca raportul X, fără nimic trimis la SFS și fără tipărire. Folosește-l ca să verifici numerarul din sertar sau după o eroare la depunere / retragere.\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `reportNumber` | integer | Numărul raportului. **Android, la raportul X:** numărul raportului X, nu al lui Z. |\n| `dailyTotal` | number | Vânzările zilei, lei, cu TVA. |\n| `dailyTax` | number | TVA-ul zilei, lei. |\n| `dailyUntaxed` | number | Vânzările pe codul `_` (fără TVA), lei. |\n| `vatBreakdown.<cod>` | object | Pe fiecare cod TVA: `code`, `percent`, `vatAmount`, `taxableBase` (fără TVA), `gross` (cu TVA). |\n| `receiptsIssued` | integer | Bonuri emise în zi. |\n| `lastReceiptNumber` | integer | Numărul ultimului bon. |\n| `receiptsSentToMev` | integer | Bonuri confirmate de SFS. **Windows:** egal cu `receiptsIssued`. |\n| `paymentTotals` | object | Încasări pe tip de plată, lei. **Android:** cheia e codul ACPS (`\"1\"`, `\"2\"`…), sume nete (numerar fără rest). **Windows:** chei fixe `CASH`, `CARD`… (vezi tabelul din descrierea colecției). |\n| `paymentCounts` | object | **Android:** cod ACPS → număr de bonuri. **Windows:** mereu `{}`. |\n| `cashIn` / `cashOut` | number | Depuneri / retrageri de numerar, lei. |\n| `cashBalance` | number | Numerarul din sertar, lei. |\n| `yearTotal` / `yearTax` | number? | Vânzări / TVA de la 1 ianuarie, lei. |\n| `totalSales` | number? | Vânzări brute ale zilei, lei. |\n| `totalReturns`, `returnsCount` | number? | Retururi. **Windows:** `null`. |\n| `cashInCount`, `cashOutCount` | integer? | Număr de depuneri / retrageri. **Windows:** `null`. |\n| `generatedAt` | string | Momentul calculului, ISO 8601 UTC. |\n| `lastZReportDate` | string? | **Doar Windows.** Data ultimului raport Z, ISO 8601 UTC. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 409 | `TERMINAL_NOT_FISCAL` | **Android:** casa nu e fiscalizată. **Windows:** fără această verificare. |\n| 500 | `INTERNAL_ERROR` | Eroare la citire. |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n\n### Windows și Android\n\n- **Windows:** `reportNumber` e numărul **ultimului Z închis**, nu al celui care urmează."
          }
        },
        {
          "name": "Raport periodic — pe interval de date",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/reports/periodic",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "reports",
                "periodic"
              ]
            },
            "description": "Totalizează o perioadă. Nu trimite nimic la SFS. Alege **un** mod: interval de date sau interval de numere de Z.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `dateFrom` / `dateTo` | string | condiționat | `YYYY-MM-DD`, împreună. `dateFrom` ≤ `dateTo`. **Android:** `dateTo` nu poate fi în viitor (`DATE_TO_FUTURE`). |\n| `reportFrom` / `reportTo` | integer | condiționat | Numere de raport Z, număr JSON întreg. Oricare poate lipsi: fără `reportFrom` începe de la primul, fără `reportTo` merge până la ultimul. Dacă trimiți și date, câștigă numerele. |\n| `detailed` | boolean | nu | Adaugă `individualZReports[]`. Implicit `false`. |\n| `printReport` | boolean | nu | Tipărește raportul. Implicit `true`. |\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `filterMode` | string | `dateRange` / `reportRange`. |\n| `dateFrom`, `dateTo` | string | Intervalul aplicat. |\n| `reportFrom`, `reportTo` | integer? | Intervalul de Z aplicat. |\n| `totalSales`, `totalTax`, `totalUntaxed` | number | Vânzări, TVA și vânzări fără TVA, lei. |\n| `receiptsCount` | integer | Bonuri. |\n| `vatBreakdown.<cheie>` | object | `code`, `percent`, `vatAmount`, `taxableBase`, `gross`. **Android:** cheia e `<cod>_<cotă>` (`B_20`). **Windows:** cheia e codul (`B`). |\n| `paymentTotals` | object | Încasări pe tip de plată (chei ca la raportul Z). |\n| `cashIn`, `cashOut` | number | Depuneri / retrageri, lei. |\n| `cashInCount`, `cashOutCount` | integer | Număr de depuneri / retrageri. |\n| `zReportsCount`, `xReportsCount` | integer | Rapoarte Z / X în interval. |\n| `firstZReportNumber`, `lastZReportNumber` | integer | Primul / ultimul Z. |\n| `firstZReportTimestamp`, `lastZReportTimestamp` | string? | Momentul închiderii primului / ultimului Z. |\n| `totalReturns`, `returnsCount` | number? | Retururi. **Windows:** `null`. |\n| `individualZReports[]` | array? | Cu `detailed: true`: `reportNumber`, `closedAt`, `firstReceiptNumber`, `lastReceiptNumber`, `receiptsCount`, `totalSales`, `totalTax`, `vatBreakdown` (fără `percent`), `paymentTotals` (**Windows:** doar `CASH` și `CARD`), `cashIn`, `cashOut`. |\n| `generatedAt` | string | ISO 8601 UTC. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 400 | `INVALID_JSON` | Corp lipsă sau invalid. |\n| 400 | `VALIDATION_ERROR` | Filtru lipsă sau date greșite. **Windows:** `PERIODIC_RANGE_REQUIRED`, `DATE_INVALID`, `DATE_RANGE_INVERTED`. **Android:** `DATE_RANGE_INVALID`, `DATE_TO_FUTURE`, `REPORT_NUMBER_INVALID`, `REPORT_RANGE_INVALID`; filtru lipsă → fără `details`. |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n| 429 | `BUSY` | **Windows:** casa e ocupată cu altă operațiune. |\n| 500 | `INTERNAL_ERROR` | Eroare pe casă. |\n\n### Windows și Android\n\n- **Android:** intră doar zilele închise cu raport Z; ziua curentă nu apare.\n- **Windows:** pe interval de date, `totalSales` și `receiptsCount` includ și vânzările încă neînchise cu Z, iar TVA-ul, plățile și numerarul vin doar din rapoartele Z. `dateTo` fără oră înseamnă începutul zilei.\n- **Windows:** fiecare cerere salvează un raport periodic pe casă și îl tipărește dacă `printReport` nu e `false`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"dateFrom\": \"{{monthStart}}\",\n  \"dateTo\": \"{{today}}\",\n  \"detailed\": false,\n  \"printReport\": false\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Raport periodic — pe numere de Z",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/reports/periodic",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "reports",
                "periodic"
              ]
            },
            "description": "Aceeași cerere, pe numere de raport Z, cu defalcare pe fiecare Z. Câmpurile, răspunsul și erorile: vezi **Raport periodic — pe interval de date**.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reportFrom\": 1,\n  \"reportTo\": 10,\n  \"detailed\": true,\n  \"printReport\": false\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Raport Z — închide ziua",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/reports/z",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "reports",
                "z"
              ]
            },
            "description": "**Închide ziua fiscală.** Nu se poate anula: incrementează numărul raportului și resetează contoarele zilei. Nu-l rula ca test pe o casă în producție.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `printReport` | boolean | nu | Implicit `true`. **Android:** ignorat, raportul se tipărește mereu. **Windows:** respectat doar cu `Content-Type: application/json`. |\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `reportNumber` | integer | **Z:** numărul raportului închis. **X, Android:** numărul raportului X. |\n| `operationId` | string | Id-ul operațiunii fiscale. |\n| `mevId` | string? | Id-ul tranzacției la SFS. |\n| `isOffline` | boolean | SFS n-a confirmat raportul (vezi „Windows și Android”). |\n| `mevResponseXml` | string? | Rezumatul răspunsului SFS. |\n| `generatedAt` | string | ISO 8601 UTC. |\n| `reportData.reportNumber` | integer | Numărul raportului. **Android, la raportul X:** numărul raportului X, nu al lui Z. |\n| `reportData.dailyTotal` | number | Vânzările zilei, lei, cu TVA. |\n| `reportData.dailyTax` | number | TVA-ul zilei, lei. |\n| `reportData.dailyUntaxed` | number | Vânzările pe codul `_` (fără TVA), lei. |\n| `reportData.vatBreakdown.<cod>` | object | Pe fiecare cod TVA: `code`, `percent`, `vatAmount`, `taxableBase` (fără TVA), `gross` (cu TVA). |\n| `reportData.receiptsIssued` | integer | Bonuri emise în zi. |\n| `reportData.lastReceiptNumber` | integer | Numărul ultimului bon. |\n| `reportData.receiptsSentToMev` | integer | Bonuri confirmate de SFS. **Windows:** egal cu `receiptsIssued`. |\n| `reportData.paymentTotals` | object | Încasări pe tip de plată, lei. **Android:** cheia e codul ACPS (`\"1\"`, `\"2\"`…), sume nete (numerar fără rest). **Windows:** chei fixe `CASH`, `CARD`… (vezi tabelul din descrierea colecției). |\n| `reportData.paymentCounts` | object | **Android:** cod ACPS → număr de bonuri. **Windows:** mereu `{}`. |\n| `reportData.cashIn` / `reportData.cashOut` | number | Depuneri / retrageri de numerar, lei. |\n| `reportData.cashBalance` | number | Numerarul din sertar, lei. |\n| `reportData.yearTotal` / `reportData.yearTax` | number? | Vânzări / TVA de la 1 ianuarie, lei. |\n| `reportData.totalSales` | number? | Vânzări brute ale zilei, lei. |\n| `reportData.totalReturns`, `reportData.returnsCount` | number? | Retururi. **Windows:** `null`. |\n| `reportData.cashInCount`, `reportData.cashOutCount` | integer? | Număr de depuneri / retrageri. **Windows:** `null`. |\n| `reportData.generatedAt` | string | Momentul calculului, ISO 8601 UTC. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 400 | `INVALID_JSON` | **Android:** corpul nu e obiect JSON (corpul gol e acceptat). |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n| 409 | `TERMINAL_NOT_FISCAL` / `MEV_CREDENTIALS_MISSING` | Casa nu e fiscalizată sau n-are credențiale SFS. |\n| 409 | `IDEMPOTENCY_KEY_REUSE_MISMATCH` | Aceeași cheie, alt corp. |\n| 429 | `BUSY` | Altă operațiune fiscală n-a eliberat casa în 30 s. Reîncearcă peste câteva secunde, cu cheie de idempotență nouă. |\n| 500 | `INTERNAL_ERROR` | Eroare pe casă. |\n| 502 | `MEV_ERROR` | SFS a respins sau n-a răspuns. Ziua **nu** s-a închis. |\n\n### De reținut\n\n- Z-ul nu cere ca ziua să fie în termen: e chiar ieșirea din `FISCAL_DAY_EXPIRED`.\n- După Z, casa pornește singură închiderea zilei pe terminalul bancar. **Windows:** închide și tura deschisă.\n- După `502` nu repeta imediat: verifică `GET /api/fiscal/operations?type=zReport`. **Windows** păstrează local o înregistrare a încercării.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"printReport\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        }
      ]
    },
    {
      "name": "4. Interogări fiscale",
      "item": [
        {
          "name": "Operațiuni fiscale",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/fiscal/operations?dateFrom={{today}}&dateTo={{tomorrow}}&limit=50&offset=0",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "fiscal",
                "operations"
              ],
              "query": [
                {
                  "key": "dateFrom",
                  "value": "{{today}}",
                  "description": "YYYY-MM-DD. Implicit: azi."
                },
                {
                  "key": "dateTo",
                  "value": "{{tomorrow}}",
                  "description": "Folosește ziua următoare: pe Windows o dată fără oră înseamnă începutul zilei."
                },
                {
                  "key": "type",
                  "value": "receipt",
                  "description": "receipt · refund · cashIn · cashOut · xReport · zReport",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "success",
                  "description": "pending · sent · success · failed · offline",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "50",
                  "description": "1–200"
                },
                {
                  "key": "offset",
                  "value": "0"
                }
              ]
            },
            "description": "Jurnalul operațiunilor fiscale de pe casă — bonuri, retururi, numerar, rapoarte — cu starea lor la SFS.\n\n### Parametri de interogare\n\n| Parametru | Tip | Implicit | Descriere |\n|---|---|---|---|\n| `dateFrom` | string | azi | `YYYY-MM-DD`. |\n| `dateTo` | string | mâine / sfârșitul zilei | **Android:** ziua inclusiv. **Windows:** fără oră = începutul zilei; pune ziua următoare. |\n| `type` | string | — | `receipt`, `refund`, `cashIn`, `cashOut`, `xReport`, `zReport` (se acceptă și `cash_in`, `z_report`…). Valoare necunoscută: **Android** ignoră filtrul, **Windows** întoarce listă goală. |\n| `status` | string | — | `pending`, `sent`, `success`, `failed`, `offline`. |\n| `limit` | integer | 50 | 1–200. |\n| `offset` | integer | 0 | Câte rezultate sari. |\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `operations[].id` | string | Id-ul operațiunii. |\n| `operations[].operationType` | string | Tipul (vezi `type`). |\n| `operations[].status` | string | Starea la SFS. **Windows:** `sent` nu apare. |\n| `operations[].mevId` | string? | Id-ul tranzacției la SFS. |\n| `operations[].receiptNumber` | integer? | Numărul bonului. **Windows:** text, `null` la vânzările prin API și la numerar / rapoarte. |\n| `operations[].reportNumber` | integer? | Numărul raportului Z. |\n| `operations[].isOffline` | boolean | Operațiune neconfirmată de SFS. |\n| `operations[].totalAmount` | number? | Suma, lei (la rapoarte: vânzările zilei). |\n| `operations[].vatBreakdown` | object? | **Android:** cod TVA → sumă TVA. **Windows:** doar la rapoarte, obiect pe cod. |\n| `operations[].vatDetails` | object? | **Android:** pe cod: `percent`, `rate`, `cost`, `gross`. **Windows:** `null`. |\n| `operations[].paymentsBreakdown` | object? | Plăți pe tip. **Android:** cod ACPS → sumă netă, la bonuri. **Windows:** la numerar `{\"CASH\": sumă}`. |\n| `operations[].errorMessage` | string? | Motivul, dacă a eșuat. |\n| `operations[].mevResponseXml` | string? | Rezumatul răspunsului SFS. |\n| `operations[].createdAt`, `completedAt` | string, string? | Creare / finalizare, ISO 8601 UTC. |\n| `total`, `limit`, `offset` | integer | Total înainte de paginare; valorile aplicate. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 500 | `INTERNAL_ERROR` | Eroare la citire. **Android:** și la `offset` negativ. |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n\n### Windows și Android\n\n- **Windows:** la vânzările făcute prin API, `receiptNumber`, `mevId` și defalcările vin goale, iar după sincronizarea cu platforma starea apare `offline`. Pentru bonul emis, sursa sigură e răspunsul la `POST /api/sales`."
          }
        }
      ]
    },
    {
      "name": "5. Tipărire nefiscală",
      "item": [
        {
          "name": "Tipărire text",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/print",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "print"
              ]
            },
            "description": "Tipărește text pe imprimanta casei, fără document fiscal. Răspunsul vine după ce imprimanta a primit textul.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `lines[]` | array | da | Rânduri. Fiecare rând e un **tablou** de 1–3 texte: `[\"text\"]` pe toată lățimea, `[\"stânga\", \"dreapta\"]`, `[\"stânga\", \"centru\", \"dreapta\"]`. `[\"\"]` = rând gol. Un rând scris ca text simplu (`\"text\"`) e respins. |\n| `lines[][]` | string | da | Max 100 caractere. **Windows:** `MAX_LENGTH`. **Android:** `LINE_TEXT_TOO_LONG`. |\n| `feedLines` | integer | nu | Rânduri goale la final, 0–10, implicit 3. **Android:** în afara intervalului → `FEED_LINES_INVALID`. **Windows:** se limitează la interval. |\n| `cut` | boolean | nu | Taie hârtia. Implicit `true`. |\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `linesCount` | integer | Câte rânduri s-au tipărit. |\n| `printedAt` | string | ISO 8601 UTC. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 400 | `INVALID_JSON` | Corpul nu e JSON valid. |\n| 400 | `VALIDATION_ERROR` | Rânduri lipsă sau greșite. **Windows:** `LINES_REQUIRED`, `MAX_COLUMNS`, `MAX_LENGTH`. **Android:** structură greșită → fără `details`. |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n| 429 | `BUSY` | **Windows:** casa e ocupată cu o operațiune fiscală. |\n| 500 | `PRINT_ERROR` | Imprimanta a raportat o eroare. |\n| 503 | `PRINTER_UNAVAILABLE` | Imprimanta e deconectată. |\n\n### Windows și Android\n\n- Lățimea rândului e a imprimantei (pe Windows, 32 de caractere). Rândul pe o coloană se taie la lățime; cele pe 2–3 coloane nu se taie și pot trece pe rândul următor.\n- **Windows:** textul se trimite în codificarea CP866. Verifică diacriticele (ă, ș, ț) pe imprimanta ta.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"lines\": [\n    [\n      \"RAPORT INVENTAR\"\n    ],\n    [\n      \"Data:\",\n      \"17.09.2026\"\n    ],\n    [\n      \"\"\n    ],\n    [\n      \"Cod\",\n      \"Denumire\",\n      \"Cant.\"\n    ],\n    [\n      \"001\",\n      \"Cafea espresso\",\n      \"150 buc\"\n    ],\n    [\n      \"002\",\n      \"Tort Napoleon\",\n      \"12,5 kg\"\n    ],\n    [\n      \"\"\n    ],\n    [\n      \"Responsabil:\",\n      \"\",\n      \"Ion Popescu\"\n    ]\n  ],\n  \"feedLines\": 3,\n  \"cut\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/print",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "print"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"lines\": [\n    [\n      \"RAPORT INVENTAR\"\n    ],\n    [\n      \"Data:\",\n      \"17.09.2026\"\n    ],\n    [\n      \"\"\n    ],\n    [\n      \"Cod\",\n      \"Denumire\",\n      \"Cant.\"\n    ],\n    [\n      \"001\",\n      \"Cafea espresso\",\n      \"150 buc\"\n    ],\n    [\n      \"002\",\n      \"Tort Napoleon\",\n      \"12,5 kg\"\n    ],\n    [\n      \"\"\n    ],\n    [\n      \"Responsabil:\",\n      \"\",\n      \"Ion Popescu\"\n    ]\n  ],\n  \"feedLines\": 3,\n  \"cut\": true\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"linesCount\": 8,\n    \"printedAt\": \"2026-09-17T09:30:00.000Z\"\n  },\n  \"error\": null,\n  \"timestamp\": \"2026-09-17T09:30:00.000Z\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "6. Card bancar",
      "description": "Corecții pe terminalul bancar. Nu emit document fiscal.",
      "item": [
        {
          "name": "Anulare plată card (aceeași zi bancară)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/card/void",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "card",
                "void"
              ]
            },
            "description": "Anulează o plată cu cardul **înainte** de închiderea zilei bancare (înainte de raportul Z). Plata se identifică după **numărul cecului bancar**, nu după RRN — terminalele PAX refuză anularea pe RRN.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `amount` | number | da | Suma de anulat, lei, > 0, max 2 zecimale. Poate fi parțială. **Windows:** `CARD_AMOUNT_INVALID`, `CARD_AMOUNT_DECIMALS`. |\n| `chequeNumber` | string | da | Din `cardPaymentResult.chequeNumber` (răspunsul la vânzare) sau `payments[].chequeNumber` (detaliul vânzării, Windows). **Windows:** max 20, `CARD_CHEQUE_NUMBER_REQUIRED` / `CARD_CHEQUE_NUMBER_INVALID`. **Android:** trebuie să fie numeric. |\n| `reason` | string | nu | Motivul; rămâne în jurnal. |\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `rrn` | string? | RRN. **Android, la anulare:** al plății originale. |\n| `authCode` | string? | Codul de autorizare. |\n| `chequeNumber` | string? | Numărul cecului bancar. |\n| `cardMask` | string? | Numărul cardului mascat. |\n| `responseCode` | string? | Răspunsul băncii. **Android:** `Approved` la succes. |\n| `receiptText` | string? | **Doar Windows.** Textul slipului bancar. |\n| `transactionDateTime` | string? | Momentul operațiunii. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 400 | `INVALID_JSON` / `VALIDATION_ERROR` | Corp invalid sau câmpuri greșite. |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n| 402 | `CARD_PAYMENT_DECLINED` | **Windows:** terminalul bancar a refuzat. |\n| 402 | `CARD_PAYMENT_ERROR` | **Windows:** eroare la terminalul bancar. |\n| 404 | `NOT_FOUND` | **Android:** nu există plată cu acest număr de cec pe casă. |\n| 429 | `BUSY` | **Windows:** casa e ocupată. |\n| 500 | `CARD_PAYMENT_ERROR` | **Android:** anularea a eșuat, inclusiv refuzul terminalului. |\n| 503 | `CARD_TERMINAL_UNAVAILABLE` | **Windows:** terminalul bancar nu e disponibil. |\n\n### Windows și Android\n\n- **Windows (versiunea curentă):** răspunsul vine cu `200` și **fără corp**, oricare ar fi rezultatul. Operațiunea se execută pe terminalul bancar; confirmă rezultatul acolo sau pe slip.\n- **Android:** plata originală trebuie să fi trecut prin această casă, altfel `404 NOT_FOUND`. Erorile de câmp vin ca `VALIDATION_ERROR` fără `details`, cu motivul în `message`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": 25,\n  \"chequeNumber\": \"0001\",\n  \"reason\": \"Clientul a renunțat\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Rambursare plată card (după închiderea zilei bancare)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/card/refund",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "card",
                "refund"
              ]
            },
            "description": "Rambursează o plată cu cardul **după** închiderea zilei bancare. Înainte de ea folosește anularea.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `amount` | number | da | Suma de rambursat, lei, > 0, max 2 zecimale. Poate fi parțială. |\n| `originalRrn` | string | da | RRN-ul plății originale, max 12 caractere. **Windows:** `CARD_RRN_REQUIRED` / `CARD_RRN_INVALID`. |\n| `authCode` | string | nu | Codul de autorizare, max 12. Multe bănci din Moldova îl cer (MAIB, Victoriabank). **Windows:** `CARD_AUTH_CODE_INVALID`. |\n| `reason` | string | nu | Motivul; rămâne în jurnal. |\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `rrn` | string? | RRN. **Android, la anulare:** al plății originale. |\n| `authCode` | string? | Codul de autorizare. |\n| `chequeNumber` | string? | Numărul cecului bancar. |\n| `cardMask` | string? | Numărul cardului mascat. |\n| `responseCode` | string? | Răspunsul băncii. **Android:** `Approved` la succes. |\n| `receiptText` | string? | **Doar Windows.** Textul slipului bancar. |\n| `transactionDateTime` | string? | Momentul operațiunii. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 400 | `INVALID_JSON` / `VALIDATION_ERROR` | Corp invalid sau câmpuri greșite. |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n| 402 | `CARD_PAYMENT_DECLINED` | Banca a refuzat rambursarea. |\n| 402 | `CARD_PAYMENT_ERROR` | **Windows:** eroare la terminalul bancar. |\n| 404 | `NOT_FOUND` | **Android:** nu există plată cu acest RRN pe casă. |\n| 429 | `BUSY` | **Windows:** casa e ocupată. |\n| 500 | `CARD_PAYMENT_ERROR` | **Android:** eroare la terminalul bancar. |\n| 503 | `CARD_TERMINAL_UNAVAILABLE` | **Windows:** terminalul bancar nu e disponibil. |\n\n### Windows și Android\n\n- **Windows (versiunea curentă):** răspunsul vine cu `200` și **fără corp**, oricare ar fi rezultatul. Operațiunea se execută pe terminalul bancar; confirmă rezultatul acolo sau pe slip.\n- **Android:** plata originală trebuie să fi trecut prin această casă, altfel `404 NOT_FOUND`. Erorile de câmp vin ca `VALIDATION_ERROR` fără `details`, cu motivul în `message`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": 25,\n  \"originalRrn\": \"123456789012\",\n  \"authCode\": \"A1B2C3\",\n  \"reason\": \"Retur marfă\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 — rambursare acceptată (Android)",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/card/refund",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "card",
                    "refund"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"amount\": 25,\n  \"originalRrn\": \"123456789012\",\n  \"authCode\": \"A1B2C3\",\n  \"reason\": \"Retur marfă\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"rrn\": \"123456789012\",\n    \"authCode\": \"A1B2C3\",\n    \"chequeNumber\": \"1\",\n    \"cardMask\": \"4111********1111\",\n    \"responseCode\": \"Approved\",\n    \"transactionDateTime\": \"2026-09-17T09:30:00.000Z\"\n  },\n  \"error\": null,\n  \"timestamp\": \"2026-09-17T09:30:00.000Z\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "7. Catalog de pe casă",
      "description": "Produsele și categoriile sincronizate pe casă. Răspund cu **JSON brut, fără** `success`/`data`.",
      "item": [
        {
          "name": "Stare catalog",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/catalog/health",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "catalog",
                "health"
              ]
            },
            "description": "Fără cheie. Cât de complet e catalogul de pe casă și când s-a sincronizat.\n\n### Răspuns 200 — JSON brut\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `terminalLabel` | string | Numele casei în rețea. |\n| `serverTime` | string | Ora casei, ISO 8601 UTC. |\n| `productCount` | integer | Produse pe casă. |\n| `categoryCount` | integer | Categorii pe casă. |\n| `lastCatalogSync` | string? | Ultima sincronizare, ISO 8601 UTC. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 503 | — | Catalogul nu poate fi citit. |",
            "auth": {
              "type": "noauth"
            }
          }
        },
        {
          "name": "Produse",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/catalog/products",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "catalog",
                "products"
              ]
            },
            "description": "Toate produsele de pe casă, fără paginare și fără filtre.\n\n### Răspuns 200 — JSON brut (listă)\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `[].id` | string | Id-ul produsului. |\n| `[].sku` | string? | Cod intern. |\n| `[].name` | string | Denumire. |\n| `[].priceBase` | number | Preț, lei. |\n| `[].categoryId` | string? | Categoria. |\n| `[].vatCode` | string? | Codul TVA. |\n| `[].isActiveInHoreca` | boolean | Activ la vânzare; lista include și produsele inactive. |\n| `[].kdsStationOverrideId` | null | Rezervat. |\n| `[].updatedAt` | string | Ultima modificare. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 500 | — | Eroare la citire. |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |"
          }
        },
        {
          "name": "Categorii",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/catalog/categories",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "catalog",
                "categories"
              ]
            },
            "description": "Categoriile de pe casă.\n\n### Răspuns 200 — JSON brut (listă)\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `[].id` | string | Id-ul categoriei. |\n| `[].name` | string | Denumire. |\n| `[].parentId` | string? | Categoria-părinte. |\n| `[].displayOrder` | integer | Ordinea de afișare. |\n| `[].color` | string? | Culoarea butonului. |\n| `[].isActive` | boolean | Activă. |\n| `[].updatedAt` | string | Ultima modificare. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 500 | — | Eroare la citire. |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |"
          }
        }
      ]
    },
    {
      "name": "8. Loturi de livrare (doar Android)",
      "description": "Casa trimite lotul la platforma PosFix și întoarce codul QR. Curierul scanează QR-ul pe casă, iar coșul se încarcă singur.",
      "item": [
        {
          "name": "Creează lot de livrare",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/delivery-lots",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "delivery-lots"
              ]
            },
            "description": "Creează lotul pe platformă și întoarce QR-ul. Comerciantul și depozitul se iau din casa care trimite cererea. Cere internet pe casă.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `clientName`, `clientPhone`, `deliveryAddress` | string | nu | Datele de livrare. |\n| `lines[].name` | string | da | Denumirea. |\n| `lines[].quantityOrdered` | number | da | Cantitatea comandată. |\n| `lines[].unitPrice` | number | da | Preț unitar cu TVA, lei. |\n| `lines[].vatCode`, `lines[].vatPercent` | string, number | da | Codul și cota TVA. |\n| `lines[].sku`, `barcode`, `productId`, `inventoryProductId` | string | nu | Identificarea produsului. |\n| `lines[].discountPercent`, `discountAbsolute`, `markupPercent`, `markupAbsolute` | number | nu | Ajustări pe linie, ca la vânzare. |\n| `cartDiscountPercent`, `cartDiscountAbsolute`, `cartMarkupPercent`, `cartMarkupAbsolute` | number | nu | Ajustări pe tot lotul. |\n| `webhookUrl` | string | nu | Adresa ta, anunțată când vânzarea din lot se fiscalizează — fie că lotul e scanat prin QR, fie livrat din „Livrări curier”. |\n| `generateQrImage` | boolean | nu | Întoarce și imaginea QR (`qrImageDataUri`). |\n| `assignToTerminal` | boolean | nu | Lotul apare imediat în „Livrări curier” pe casa care l-a creat. |\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `lotId` | string | Id-ul lotului; testul îl salvează în `lotId`. |\n| `qrToken` | string | Codul lotului. |\n| `qrPayload` | string | Textul din QR. |\n| `qrImageDataUri` | string? | Imaginea QR, PNG base64 (cu `generateQrImage`). |\n| `assigned` | boolean | Lotul a fost atribuit casei. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 400 | `INVALID_JSON` | Corpul nu e obiect JSON. |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n| 503 | `MEV_UNAVAILABLE` | Platforma PosFix nu răspunde. |\n| 4xx/5xx | codul platformei / `PLATFORM_ERROR` | Platforma a refuzat lotul; statusul și mesajul vin de la ea. |\n\n### De reținut\n\n- **Webhook-ul** îl trimite casa, după ce bonul s-a fiscalizat: un POST simplu, fără semnătură și fără reîncercări. Răspunde cu orice 2xx. Reconciliază după `lotLineId`, nu după poziția în coș. `saleId` e id-ul tranzacției bonului la SFS. Fiecare trimitere, cu răspunsul primit de la tine, apare în panou pe lot → „Jurnal webhook”.\n- `lines[].status`: `sold` vândută integral · `modified` altă cantitate · `removed` refuzată de client sau scoasă din coș. `lines[].orderedQty` e cantitatea din lot. `extraLines[].status`: `added` — adăugată de casier.\n\n```json\n{\n  \"lotId\": \"f1c776f1-6a03-4190-837c-9bf9b01f41f7\",\n  \"saleId\": \"3c9a8b7d-6e5f-4a3b-2c1d-0e9f8a7b6c5d\",\n  \"soldAt\": \"2026-09-17T09:30:00.000Z\",\n  \"lines\": [\n    {\n      \"lotLineId\": \"a1b2c3\",\n      \"name\": \"Apă plată 0,5 L\",\n      \"orderedQty\": 2,\n      \"soldQty\": 2,\n      \"status\": \"sold\"\n    },\n    {\n      \"lotLineId\": \"d4e5f6\",\n      \"name\": \"Pâine albă\",\n      \"orderedQty\": 1,\n      \"soldQty\": 0,\n      \"status\": \"removed\"\n    }\n  ],\n  \"extraLines\": [\n    {\n      \"productId\": \"prod-42\",\n      \"name\": \"Pungă\",\n      \"soldQty\": 1,\n      \"status\": \"added\"\n    }\n  ]\n}\n```",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"clientName\": \"Vasile Munteanu\",\n  \"clientPhone\": \"+37369123456\",\n  \"deliveryAddress\": \"str. Ștefan cel Mare 1, Chișinău\",\n  \"lines\": [\n    {\n      \"name\": \"Apă plată 0,5 L\",\n      \"quantityOrdered\": 2,\n      \"unitPrice\": 10,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20\n    },\n    {\n      \"name\": \"Pâine albă\",\n      \"quantityOrdered\": 1,\n      \"unitPrice\": 12.5,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20,\n      \"discountPercent\": 10\n    }\n  ],\n  \"webhookUrl\": \"https://exemplu.md/webhooks/posfix-lot\",\n  \"generateQrImage\": true,\n  \"assignToTerminal\": false\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 200) {",
                  "  const d = pm.response.json().data;",
                  "  if (d && d.lotId) pm.collectionVariables.set('lotId', d.lotId);",
                  "}"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 — lot creat",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/delivery-lots",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "delivery-lots"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"clientName\": \"Vasile Munteanu\",\n  \"clientPhone\": \"+37369123456\",\n  \"deliveryAddress\": \"str. Ștefan cel Mare 1, Chișinău\",\n  \"lines\": [\n    {\n      \"name\": \"Apă plată 0,5 L\",\n      \"quantityOrdered\": 2,\n      \"unitPrice\": 10,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20\n    },\n    {\n      \"name\": \"Pâine albă\",\n      \"quantityOrdered\": 1,\n      \"unitPrice\": 12.5,\n      \"vatCode\": \"B\",\n      \"vatPercent\": 20,\n      \"discountPercent\": 10\n    }\n  ],\n  \"webhookUrl\": \"https://exemplu.md/webhooks/posfix-lot\",\n  \"generateQrImage\": true,\n  \"assignToTerminal\": false\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"lotId\": \"f1c776f1-6a03-4190-837c-9bf9b01f41f7\",\n    \"qrToken\": \"LOT-7K3M9Q\",\n    \"qrPayload\": \"posfix://delivery-lot/LOT-7K3M9Q\",\n    \"qrImageDataUri\": \"data:image/png;base64,iVBORw0KGgo…\",\n    \"assigned\": false\n  },\n  \"error\": null,\n  \"timestamp\": \"2026-09-17T09:30:00.000Z\"\n}"
            }
          ]
        },
        {
          "name": "Anulează lot de livrare",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/delivery-lots/{{lotId}}/cancel",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "delivery-lots",
                "{{lotId}}",
                "cancel"
              ]
            },
            "description": "Anulează lotul și eliberează stocul rezervat. Dacă lotul era atribuit unei case, platforma i-l retrage.\n\n### Parametri de cale\n\n| Parametru | Tip | Descriere |\n|---|---|---|\n| `id` | string (GUID) | Id-ul lotului. |\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `lotId` | string | Id-ul lotului. |\n| `cancelled` | boolean | `true`. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n| 503 | `MEV_UNAVAILABLE` | Platforma PosFix nu răspunde. |\n| 4xx/5xx | codul platformei / `PLATFORM_ERROR` | Platforma a refuzat anularea. |",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        }
      ]
    },
    {
      "name": "9. Jurnalul cererilor",
      "item": [
        {
          "name": "Cereri primite",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/audit/requests?source=http&limit=20&offset=0",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "audit",
                "requests"
              ],
              "query": [
                {
                  "key": "source",
                  "value": "http",
                  "description": "http (rețea locală) · signalr (relay)"
                },
                {
                  "key": "endpoint",
                  "value": "/api/sales",
                  "description": "Adresa exactă.",
                  "disabled": true
                },
                {
                  "key": "code",
                  "value": "VALIDATION_ERROR",
                  "description": "OK sau un error.code.",
                  "disabled": true
                },
                {
                  "key": "dateFrom",
                  "value": "{{today}}",
                  "disabled": true
                },
                {
                  "key": "dateTo",
                  "value": "{{tomorrow}}",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Obligatoriu pe Windows. Max 200."
                },
                {
                  "key": "offset",
                  "value": "0",
                  "description": "Obligatoriu pe Windows."
                }
              ]
            },
            "description": "Cererile POST primite de casă și cum a răspuns la fiecare. Cererile GET și cele respinse cu 401 nu se jurnalizează.\n\n### Parametri de interogare\n\n| Parametru | Tip | Implicit | Descriere |\n|---|---|---|---|\n| `source` | string | — | `http` sau `signalr` (relay). Altă valoare → `400 VALIDATION_ERROR`. |\n| `endpoint` | string | — | Adresa exactă, de exemplu `/api/sales`. |\n| `code` | string | — | `OK` sau un `error.code`. |\n| `dateFrom` / `dateTo` | string | — | Momente ISO. O dată fără oră înseamnă începutul zilei; pentru ziua de azi pune `dateTo` = mâine. `dateFrom` > `dateTo` → `400`. |\n| `limit` | integer | 50 | Max 200. **Windows:** obligatoriu; fără el → `400` fără corp. |\n| `offset` | integer | 0 | **Windows:** obligatoriu. |\n\n### Răspuns 200 — `data`\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `requests[].id` | string | Id-ul intrării = antetul `X-Request-Id` primit la cerere. |\n| `requests[].source` | string | `http` / `signalr`. |\n| `requests[].endpoint` | string | Adresa cererii. |\n| `requests[].idempotencyKey` | string? | Cheia trimisă. |\n| `requests[].payloadHash` | string | SHA-256 al corpului (fără spații). |\n| `requests[].httpStatus` | integer? | Statusul răspunsului. |\n| `requests[].responseCode` | string? | `OK` sau `error.code`. |\n| `requests[].latencyMs` | integer? | Durata, ms. |\n| `requests[].clientIp` | string? | IP-ul clientului. |\n| `requests[].relatedSaleId` | string? | Rezervat (`null`). |\n| `requests[].createdAt` | string | ISO 8601 UTC. |\n| `total`, `limit`, `offset` | integer | Total înainte de paginare; valorile aplicate. |\n\n### Erori\n\n| HTTP | `error.code` | Când |\n|---|---|---|\n| 400 | `VALIDATION_ERROR` | `source` necunoscut sau interval inversat. |\n| 400 | — | **Windows:** lipsește `limit` sau `offset`. |\n| 500 | `INTERNAL_ERROR` | Eroare la citire. |\n| 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Cheia lipsește sau e greșită. |\n\n### De reținut\n\n- Corpurile cererilor și răspunsurilor nu se expun. Telefonul și emailul clientului se salvează mascate. Intrările se păstrează 90 de zile."
          }
        }
      ]
    }
  ]
}
