{
  "info": {
    "_postman_id": "3b9d7c52-1e4f-4a86-9c20-7f5e1d3a8b64",
    "name": "PosFix — API public de integrare",
    "description": "API-ul prin care sistemele externe ale unui comerciant (magazin online, ERP, contabilitate) îi citesc și îi scriu datele: catalog, stoc, prețuri, clienți, comenzi, vânzările (rapoartele Z și toate vânzările pe zile și canale), recunoașterea facturilor, programul de fidelitate și evenimente prin webhook-uri.\n\nPentru API-ul de pe casa de marcat (bonuri fiscale, numerar, rapoarte X/Z tipărite) există colecția separată **PosFix — API terminal**.\n\n## Cheia\n\nModulul **„API Integrare”** e un serviciu contra cost: PosFix îl activează pentru comerciant și emite cheia. Cheia (`rk_…`) se arată **o singură dată**; PosFix păstrează doar amprenta ei, deci o cheie pierdută se înlocuiește, nu se recuperează. După înlocuire, cheia veche mai merge 24 de ore.\n\nSe trimite pe fiecare cerere:\n\n```\nX-Posfix-Api-Key: rk_…\n```\n\nComerciantul se ia **din cheie**. Nu există parametru de comerciant, deci o cheie nu poate citi datele altcuiva. O cheie poate fi limitată la anumite case de marcat: atunci vede doar vânzările lor.\n\nÎncepe cu `GET /api/public/v1/ping`: confirmă cheia și îți arată scope-urile ei.\n\n## Scope-uri\n\nFiecare cheie primește doar permisiunile alese la emitere. Scope-ul cerut de o cerere e scris la începutul descrierii ei.\n\n| Scope | Ce permite |\n|---|---|\n| `products:read` | Catalog, categorii, atribute, blocuri de conținut, liste de prețuri, prețuri. |\n| `inventory:read` | Stocul unui produs. |\n| `customers:read` / `customers:write` | Citește / creează clienți. |\n| `orders:read` / `orders:write` | Citește / creează comenzi, pune datele de livrare. |\n| `sales:read` | Case de marcat, rapoarte Z, bonuri. |\n| `docai:catalog` | Nomenclatorul propriu pentru recunoașterea facturilor. |\n| `docai:recognize` | Trimite facturi (PDF, fotografii) la recunoaștere — contra cost. |\n| `docai:read` | Citește facturile recunoscute și confirmă preluarea. |\n| `loyalty:read` | Programele de fidelitate, membrii cu soldul lor, istoricul cardului. Cere și modulul „Programe de fidelitate”. |\n| `loyalty:write` | Înscrie membri, le schimbă numele sau emailul. Cere și modulul „Programe de fidelitate”. |\n| `loyalty:earn` | Acordă pe comenzile plătite și anulează la retur. Cere și modulul „Programe de fidelitate”. |\n| `webhooks:manage` | Abonamente la evenimente. |\n\nFără scope-ul cerut, sau cu modulul „API Integrare” inactiv, răspunsul e `403`, **fără corp**. Scope-urile `loyalty:*` cer în plus modulul „Programe de fidelitate” activ la comerciant. „Ping” nu cere nici scope, nici modul: dacă el merge și o cerere dă `403`, cauza e scope-ul lipsă sau un modul inactiv.\n\n## Limite\n\n- Pe cheie: o rezervă de **120 de cereri**, care se reface cu **2 pe secundă** (~120 pe minut, în ritm constant).\n- Peste limită: `429` cu `errorCode: RATE_LIMITED`, **fără** antet `Retry-After`. Reîncearcă după câteva secunde, cu pauze crescătoare.\n- O a doua limită, de **300 de cereri pe minut**, se numără pe adresa IP. Serviciul vede azi doar adresa gateway-ului, deci limita aceasta e comună tuturor integratorilor: un `429` poate veni și când cheia ta e sub limită.\n- Limita se verifică înaintea cheii: un `429` poate veni și pentru o cerere cu cheie greșită.\n\n## Erori\n\nRăspunsurile de eroare au mai multe forme. **Decide după statusul HTTP**; `errorCode` și `errors` ajută acolo unde există, iar `detail` e text pentru oameni și se poate schimba.\n\n| HTTP | Corp | Când |\n|---|---|---|\n| `401`, `403` | **fără corp** | Cheie lipsă sau invalidă; scope lipsă sau modul inactiv. Cele două `403` arată la fel: verifică-le cu „Ping”. |\n| `404` | fără corp, sau format simplu | Resursa nu există (sau e a altui comerciant). |\n| `400` | `{\"statusCode\":400,\"message\":\"…\",\"errors\":{\"câmp\":[\"…\"]},\"errorCodes\":{\"câmp\":[{\"code\":\"COD\",\"params\":{…}}]}}` | Parametru sau JSON de tip greșit, GUID invalid, validare pe parametri (formatul de legare). |\n| `400`, `404`, `409` | `{\"status\":…,\"title\":\"…\",\"detail\":\"…\",\"errorCode\":\"COD\",\"params\":{…}}` | Refuz de domeniu cu mesaj în `detail` (format simplu). |\n| `400`, `422`, `5xx` | `{\"type\":…,\"title\":\"COD\",\"status\":…,\"detail\":\"…\",\"errorCode\":\"COD\",\"params\":{…},\"errors\":{…},\"errorCodes\":{…}}` | `VALIDATION` (cu `errors` și `errorCodes` pe câmp), refuzul unei reguli de business cu codul lui (`422`, de exemplu stoc insuficient sau preț schimbat), `SERVICE_UNAVAILABLE` (503), `INTERNAL_ERROR` (500). |\n| `429` | `{\"status\":429,\"title\":\"Too Many Requests\",\"detail\":\"…\",\"errorCode\":\"RATE_LIMITED\"}` | Peste limită. |\n\nUn `503` înseamnă că un serviciu intern nu răspunde: reîncearcă. Un `401`/`403` poate apărea și când serviciul care verifică cheia sau modulul e căzut, fără să fie ceva greșit la tine.\n\n## Convenții\n\n- JSON cu nume de câmpuri în **camelCase**. Câmpurile fără valoare vin ca `null`, nu lipsesc — în afara celor despre care descrierea spune că lipsesc (stocul `reserved` / `available`, câmpurile noi din webhook-uri).\n- Datele calendaristice au forma `yyyy-MM-dd`; momentele sunt ISO 8601 UTC, cu `Z`.\n- Sumele sunt numere JSON, în lei dacă nu se spune altfel.\n- **TVA:** `price` din catalog e prețul de la casă, **cu TVA**. La comandă, `lines[].unitPrice` se tratează **fără TVA** și TVA-ul se adaugă peste. Nu copia prețul din catalog direct în comandă: trimite `price / (1 + vatRate/100)` cu 4 zecimale, sau nu trimite prețul deloc și PosFix îl ia de la casă.\n- Versiunea stă în cale (`/api/public/v1/`). În v1 se adaugă doar câmpuri și evenimente noi, nu se schimbă cele existente: **ignoră câmpurile pe care nu le cunoști**.\n- Un parametru-listă se repetă (`?categoryCode=A&categoryCode=B`); valorile booleene se scriu `true` / `false`.\n\n## Paginare\n\n- **Cu cursor** — produse, clienți, membrii și istoricul fidelității: ceri `limit`, primești `nextCursor`; pagina următoare o ceri cu `cursor=<nextCursor>`, până când vine `null`.\n- **Cu pagini** — comenzi, rapoarte Z, bonuri: `page` (de la 1) și `pageSize`; răspunsul are totalul.\n\n## Webhook-uri: cum primești evenimentele\n\nPosFix trimite un `POST` JSON la adresa ta când se întâmplă ceva la comerciant. Te abonezi cu „Creează webhook” și alegi evenimentele.\n\n| Eveniment | Când | `data` |\n|---|---|---|\n| `product.created` | Produs nou în nomenclator. | `posfixId`, `code`, `name`, `description`, `barcode`, `price` (prețul implicit din nomenclator, nu cel de la casă), `currency`, `vatRate`, `isService` |\n| `product.updated` | Produs modificat. | `posfixId`, `code`, `name`, `description`, `currency`, `vatRate`, `isActive` (fără preț și cod de bare), `occurredAt` (momentul schimbării); `nameTranslations`, `descriptionTranslations` — traducerile pe limbile active, cheia = codul limbii, fără limba principală (aceea e `name` / `description`); limba netradusă lipsește |\n| `inventory.updated` | S-a schimbat stocul sau rezervatul unui produs într-un depozit; poate veni cu `quantity` neschimbată. `available` lipsește la preparatele cu rețetă și la pozițiile-serviciu; o poziție-serviciu fără rețetă nu trimite `inventory.updated`. | `posfixId`, `warehouseId`, `quantity` (marfa din depozit), `value` și `averageCost` (valoarea și costul mediu ale poziției la momentul `changedAt`; o recalculare de cost trimite și ea `inventory.updated`, cu `quantity` neschimbată), `changedAt`, `reserved` (promisă pe comenzi confirmate), `available` (`quantity − reserved`, ce se mai poate vinde; negativ când s-a rezervat peste stoc) |\n| `order.created` | Comandă nouă (inclusiv din panou sau de la casă). | `id`, `number`, `status`, `changeType`, `total`, `paidSum`, `currency`, `occurredAt` (momentul schimbării); pe comanda venită prin API și `externalOrderId`, `externalNumber`, `externalSourceChannel`; după pregătire `readyForPickupAt`, după predarea la transportator `shippedAt` și `trackingNumber` |\n| `order.confirmed` | Comerciantul a confirmat comanda. | la fel |\n| `order.ready_for_pickup` | Comanda cu ridicare e pregătită: cumpărătorul poate veni după ea. Starea rămâne `Confirmed`. | la fel; `readyForPickupAt` = momentul pregătirii |\n| `order.shipped` | Marfa a fost predată transportatorului (Nova Post sau alt curier); vine din nou dacă se corectează numărul de expediție. Starea rămâne `Confirmed` până la livrare. Dacă predarea se retrage, vine `order.confirmed` (sau `order.fulfilling`) fără `shippedAt`. | la fel; `shippedAt` = momentul predării, `trackingNumber` = numărul de expediție (TTN, AWB), când există |\n| `order.preparation_started` | Cineva a luat comanda în lucru („Iau în lucru”): marfa se pregătește. Starea rămâne `Confirmed`. | la fel |\n| `order.fulfilling` | Livrată parțial. | la fel |\n| `order.fulfilled` | Livrată integral. | la fel |\n| `order.closed` | Închisă manual. | la fel |\n| `order.cancelled` | Anulată. | la fel, plus `reason` când a anulat-o sistemul: `payment_deadline` = neplătită deloc până la termenul de plată; `stock_refused` = comanda din API refuzată la primire pentru stoc insuficient (cererea ei a primit `422`); `confirmation_failed` = confirmarea comenzii a căzut din alt motiv decât stocul (din API, sau de la casă, de pildă când casierul renunță); fără `reason` = anulată de comerciant |\n| `order.reopened` | O comandă anulată a fost readusă în ciornă. | la fel |\n| `order.partially_paid` | A crescut suma plătită, dar nu acoperă totalul. | la fel; `paidSum` în lei |\n| `order.paid` | Comanda e plătită integral. | la fel; `paidSum` în lei |\n| `docai.recognition.completed` | O factură a fost recunoscută (trimisă prin API sau încărcată în panou). | `id`, `status`, `source`, `externalRef`, `fileName`, `supplierName`, `supplierIdno`, `documentSeries`, `documentNumber`, `issueDate`, `total`, `currency`; datele complete — `GET /api/public/v1/docai/recognitions/{id}` |\n| `docai.recognition.failed` | Recunoașterea n-a reușit (fișier ilizibil, factură prea lungă, întrerupere). | la fel, plus `errorCode` |\n| `docai.recognition.reviewed` | Un om a verificat și a corectat factura în panou. | la fel; datele verificate — pe id |\n| `sale.created` | O vânzare nouă: bon, retur la casă, factură, retur de la client sau raport de amănunt. | `id`, `kind` (`receipt`, `refund`, `invoice`, `customer_return`, `retail_report`), `number`, `date`, `soldAt`, `channel` (`id`, `name`, `type`), `customer` (`kind`: `individual`/`company`, `partnerId`, `name`), `terminalId`, `netAmount`, `vatAmount`, `totalAmount`, `currency`, `lines[]` (`productId`, `productCode`, `name`, `quantity`, `netAmount`, `vatAmount`); sumele în lei, fără TVA, retururile cu minus |\n| `sale.updated` | S-a schimbat ce vede omul pe vânzare: suma, canalul, clientul sau liniile. | la fel, cu valorile noi |\n| `sale.deleted` | Vânzarea a ieșit din evidență (factura anulată, de exemplu). | la fel, cu ultimele valori |\n| `loyalty.member.enrolled` | Un membru nou în programul de fidelitate, înscris pe oricare cale: prin API, în panou, la casă sau pe pagina de înscriere. | membrul la zi, cu câmpurile din `GET /api/public/v1/loyalty/members/{cardNumber}`, fără `wallet`; dacă membrul a fost șters între timp, doar `cardNumber: null` |\n| `loyalty.balance.changed` | S-a schimbat soldul membrului: puncte, ștampile, bonus în lei sau treaptă (o acumulare, o cheltuire, un retur, o expirare). | la fel; `balance` și `bonus` sunt cele de la momentul livrării, nu de la momentul schimbării |\n\nCorpul are același plic la orice eveniment:\n\n```json\n{ \"id\": \"7c1e9a52-3b4d-4f6e-8a90-1b2c3d4e5f60\", \"type\": \"order.paid\", \"createdAt\": \"2026-09-25T08:12:03.4567891Z\", \"version\": \"1\",\n  \"data\": { \"id\": \"3c9a1f52-7d3e-4b8a-a0c4-6e2b91f0d7a8\", \"number\": \"0000000042\", \"status\": \"Confirmed\", \"changeType\": \"paid\", \"total\": 745.8, \"paidSum\": 745.8, \"currency\": \"MDL\", \"occurredAt\": \"2026-09-25T08:12:02.9180344Z\" } }\n```\n\n- `id` e id-ul livrării: același la fiecare reîncercare. **Deduplică după el** — o livrare poate sosi de două ori.\n- `createdAt` e momentul punerii în coadă, nu al evenimentului. Ordinea sosirii nu e garantată.\n- Livrările pot sosi în altă ordine și se pot reîncerca ore în șir (vezi „Reîncercări”): aplică o schimbare doar dacă momentul ei — `changedAt` la stoc, `occurredAt` la produse și comenzi — e mai nou decât cel pe care îl ai deja.\n- O recalculare de cost trimite `inventory.updated` pentru fiecare poziție reevaluată, iar într-o cascadă aceeași poziție poate primi două evenimente, unul intermediar și apoi starea finală: pentru aceeași poziție, evenimentul cu `changedAt` cel mai nou e cel bun.\n- La comenzi, `paidSum` vine pe orice `order.*`, în lei, cum îl ține PosFix pe comandă în momentul evenimentului (se recalculează după fiecare plată, deci poate întârzia puțin): aplică-l doar când `occurredAt` e mai nou decât al ultimului eveniment aplicat; suma la zi o dă `GET /api/public/v1/orders/{id}`.\n- Comanda venită prin API poartă `externalOrderId` (cheia ta), `externalNumber` și `externalSourceChannel`. **Potrivește comenzile după `id`**; `externalOrderId` e doar pentru un `id` pe care încă nu-l știi. O comandă refuzată la primire vine ca pereche `order.created` → `order.cancelled` (`reason` `stock_refused` sau `confirmation_failed`), cu același `id`, deși cererea ta a primit `422` fără `id`. Dacă retrimiți apoi același `externalOrderId`, comanda nouă are alt `id` (cel din răspunsul `200`/`201`): un `order.cancelled` întârziat al încercării refuzate, cu alt `id` decât cel primit, se ignoră — altfel ți-ai anula comanda vie.\n- O plată anulată scade suma plătită fără eveniment: suma curentă se citește din `GET /api/public/v1/orders/{id}`.\n- `reserved`, `available`, `nameTranslations`, `descriptionTranslations`, `reason` și câmpurile `external*` **lipsesc** din `data` când evenimentul nu le are, în loc să vină `null`: verifică dacă există, nu doar dacă sunt `null`.\n\n### Antete\n\n| Antet | Valoare |\n|---|---|\n| `X-Posfix-Signature` | Semnătura, base64 |\n| `X-Posfix-Webhook-Id` | Id-ul livrării (= `id` din corp) |\n| `X-Posfix-Timestamp` | Momentul trimiterii, secunde Unix |\n| `X-Posfix-Event` | Tipul evenimentului |\n\n### Verificarea semnăturii\n\n```\nsignedString       = \"{X-Posfix-Webhook-Id}.{X-Posfix-Timestamp}.{corpul brut}\"\nX-Posfix-Signature = base64( HMAC_SHA256(signingSecret, signedString) )\n```\n\n- Cheia HMAC e **tot** secretul, cu prefixul `whsec_` cu tot, ca octeți UTF-8 — nu se decodează.\n- Semnează **corpul brut**, exact cum a sosit. Un JSON parsat și serializat din nou nu mai dă aceeași semnătură.\n- Compară în timp constant și respinge livrările cu `X-Posfix-Timestamp` mai vechi de ~5 minute.\n\n```js\nconst crypto = require(\"crypto\");\n// rawBody: Buffer, citit cu express.raw({ type: \"application/json\" })\nconst signed = Buffer.concat([Buffer.from(`${webhookId}.${timestamp}.`, \"utf8\"), rawBody]);\nconst expected = crypto.createHmac(\"sha256\", signingSecret).update(signed).digest(\"base64\");\nconst valid = expected.length === signature.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));\n```\n\n### Reîncercări\n\n- Orice răspuns **2xx** în cel mult 15 secunde înseamnă primit.\n- `5xx`, `408`, `429`, redirecționările și căderile de rețea se reîncearcă după 2, 4, 8, 16, 32, 60, 60 de minute. După 8 încercări (~3 ore) livrarea se abandonează.\n- Orice alt `4xx` oprește livrarea **imediat**, fără reîncercare. La fel un nume de domeniu care nu se rezolvă sau care duce la o adresă privată.\n- Redirecționările nu se urmează.\n- Cât modulul „API Integrare” e inactiv, evenimentele nu se trimit și nici nu se păstrează.\n\n## Variabilele colecției\n\n- `baseUrl` — `https://api.posfix.md` (producție) sau `https://dev.posfix.md` (teste).\n- `apiKey` — cheia primită de la PosFix.\n- `today`, `monthStart` — se calculează înaintea fiecărei cereri.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "apikey",
    "apikey": [
      {
        "key": "key",
        "value": "X-Posfix-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();",
          "pm.collectionVariables.set('today', fmt(now));",
          "pm.collectionVariables.set('monthStart', fmt(now).slice(0, 8) + '01');"
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://api.posfix.md",
      "description": "Producție. Pentru teste: https://dev.posfix.md"
    },
    {
      "key": "apiKey",
      "value": "",
      "description": "Cheia rk_… primită de la PosFix odată cu modulul „API Integrare”"
    },
    {
      "key": "today",
      "value": "",
      "description": "Calculat automat (YYYY-MM-DD)"
    },
    {
      "key": "monthStart",
      "value": "",
      "description": "Calculat automat (prima zi a lunii)"
    }
  ],
  "item": [
    {
      "name": "0. Verificare",
      "description": "Începe de aici: confirmă că cheia merge și ce scope-uri are.",
      "item": [
        {
          "name": "Ping",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/ping",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "ping"
              ]
            },
            "description": "Verifică cheia. Întoarce comerciantul cheii și scope-urile ei. Începe de aici.\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `organizationId` | string (uuid) | Comerciantul căruia îi aparține cheia. |\n| `scopes` | string[] | Scope-urile cheii. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 401 | — | Cheia lipsește sau nu e validă. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n\n### De reținut\n\n- Ping nu cere scope și nici modulul „API Integrare”. Dacă ping merge și altă cerere dă `403`, cheii îi lipsește scope-ul acelei cereri sau modulul nu e activ."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/ping",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "ping"
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"organizationId\": \"3f6c2a1e-8b4d-4c7a-9e21-5d0f7b8a9c12\",\n  \"scopes\": [\n    \"products:read\",\n    \"inventory:read\",\n    \"orders:write\",\n    \"sales:read\"\n  ]\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "1. Catalog și stoc",
      "description": "Sortimentul de vânzare, fișele produselor, stocul, categoriile și atributele.",
      "item": [
        {
          "name": "Listă produse",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/products?limit=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "products"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "50",
                  "description": "1–200, implicit 50."
                },
                {
                  "key": "search",
                  "value": "apa",
                  "description": "Cuvinte din cod sau denumire.",
                  "disabled": true
                },
                {
                  "key": "categoryCode",
                  "value": "BAUTURI",
                  "description": "Codul categoriei; repetabil.",
                  "disabled": true
                },
                {
                  "key": "categoryId",
                  "value": "",
                  "description": "Id-ul categoriei; repetabil.",
                  "disabled": true
                },
                {
                  "key": "includeSubcategories",
                  "value": "true",
                  "description": "Implicit true.",
                  "disabled": true
                },
                {
                  "key": "attribute",
                  "value": "tara:Moldova",
                  "description": "cod:valoare; repetabil.",
                  "disabled": true
                },
                {
                  "key": "includeInactive",
                  "value": "false",
                  "description": "Implicit false.",
                  "disabled": true
                },
                {
                  "key": "includeUnlisted",
                  "value": "false",
                  "description": "Implicit false.",
                  "disabled": true
                },
                {
                  "key": "warehouseId",
                  "value": "",
                  "description": "Stocul unui singur depozit; implicit toate.",
                  "disabled": true
                },
                {
                  "key": "cursor",
                  "value": "",
                  "description": "nextCursor din pagina precedentă.",
                  "disabled": true
                }
              ]
            },
            "description": "**Scope necesar:** `products:read`\n\nSortimentul de vânzare al comerciantului, pagină cu pagină: produsele care au card la casă, cu prețul, categoria și stocul.\n\n### Parametri de interogare\n\n| Parametru | Tip | Implicit | Descriere |\n|---|---|---|---|\n| `limit` | integer | 50 | Peste 200 se reduce la 200. |\n| `search` | string | — | Cuvinte din **cod sau denumire**, în orice ordine: fiecare cuvânt trebuie să apară. Fără diferență de majuscule și de diacritice. Nu caută în codul de bare. |\n| `categoryId`, `categoryCode` | uuid / string | — | Categoriile de la casă; repetabile, legate prin SAU: `?categoryCode=APA&categoryCode=SUCURI`. |\n| `includeSubcategories` | boolean | `true` | Include subcategoriile celor cerute. |\n| `attribute` | string | — | `cod:valoare`, repetabil. Același cod = SAU, coduri diferite = ȘI: `?attribute=brand:Samyang&attribute=tara:Moldova`. Codul e cel din „Atribute”, cu litere mici. |\n| `includeInactive` | boolean | `false` | Include produsele și cardurile inactive. |\n| `includeUnlisted` | boolean | `false` | Include pozițiile **fără** card la casă (materiale, servicii interne). Se ignoră când filtrezi pe categorie. |\n| `warehouseId` | string (uuid) | toate | Stocul (`onHand`, `onHandByWarehouse`, `inStock`) se ia doar din acest depozit. Id-ul vine din „Depozite”. Nu restrânge lista de produse. |\n| `cursor` | string | — | Valoarea `nextCursor` din pagina precedentă. |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `items[]` | array | Produsele; câmpurile sunt în tabelul de mai jos. `composition` și `contentBlocks` vin goale în listă. |\n| `nextCursor` | string? | Cursorul paginii următoare; `null` pe ultima. |\n\n### Produsul\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `posfixId` | string (uuid) | Id-ul produsului în PosFix. Cu el ceri prețuri. |\n| `code` | string | Codul produsului (de obicei 10 cifre). Cu el comanzi și citești stocul. |\n| `name` | string | Denumirea din nomenclator. |\n| `names` | object | Denumirea pe limbi; regula hărților de limbi e în notele cererii. |\n| `description` | string? | Descrierea în română. |\n| `barcode` | string | Codul de bare; `\"\"` dacă lipsește. |\n| `price`, `currency` | number, string | Prețul de vânzare de la casă (cu TVA); fără card la casă, prețul implicit din nomenclator. |\n| `vatRate` | number? | Cota TVA, %. `null` = cotă nestabilită, diferit de `0`. |\n| `unit` | string | Unitatea de bază (`buc`, `kg`). |\n| `isActive` | boolean | Produsul e activ în nomenclator. |\n| `category`, `categoryPath[]` | object?, array | Categoria de la casă (`id`, `name`, `code`, `names`) și drumul ei de la rădăcină. |\n| `imageUrl` | string? | Adresa **relativă** a imaginii; o citești cu aceeași cheie (cererea „Imaginea produsului”). |\n| `attributes[]` | array | `code` și `value` — valorile atributelor (marcă, țară…); `valueNames` = valoarea pe limbi. |\n| `onHand[]` | array | Stocul total, un singur element cu `warehouseId: null`: `quantity`, `reserved`, `available` (sensul lor e în notele cererii). Cu `?warehouseId=` la listă, stocul acelui depozit, cu id-ul lui. |\n| `onHandByWarehouse[]` | array | Stocul pe fiecare depozit care are marfa: `warehouseId`, `code`, `name`, `quantity`, `reserved`, `available`. Depozitul care lipsește are 0. |\n| `inStock` | boolean | Stocul fizic total e peste zero, chiar dacă e tot rezervat. Dacă se mai poate cumpăra îți spune `onHand[].available`. |\n| `descriptions` | object | Descrierea pe limbi; regula hărților de limbi e în notele cererii. |\n| `composition[]` | array | Compoziția: `ingredient`, `percent`, `allergen`. Doar la „Produs după cod”. |\n| `contentBlocks[]` | array | Blocurile de conținut (HTML curățat): `labels` și `contents` pe limbi, plus câmpurile vechi `labelRo`… `contentEn`. Doar la „Produs după cod”. |\n| `sku` | string? | Codul extern al produsului, altfel SKU-ul de la casă. |\n| `shelfLifeDays` | integer? | Termenul de valabilitate, zile. |\n| `packaging[]` | array | `unit` și `factor`: „1 `unit` = `factor` unități de bază”. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | — | Filtru `attribute` fără forma `cod:valoare`, valoare de tip greșit (`tree=1`, GUID invalid). |\n| 400 | `VALIDATION` | „cursor must be a valid product id.” |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Hărțile de limbi (`names`, `descriptions`, `valueNames`, `labels`, `contents`) au cheia = codul limbii: `{ \"ro\": …, \"ru\": … }`. Limbile vin din lista platformei și pot crește oricând — citește cheile primite, nu presupune exact `ro`/`ru`/`en`. O limbă fără traducere lipsește din hartă: folosești textul de bază (`name`, `description`).\n- Stocul are trei cifre: `quantity` e marfa din depozit; `reserved` e partea promisă pe comenzile confirmate care n-au plecat încă; `available` = `quantity − reserved` e ce se mai poate vinde. În magazin arată `available`.\n- `available` lipsește când nu există un disponibil de publicat (preparatele cu rețetă, pozițiile-serviciu) sau când nu se știe; un `available` absent nu înseamnă epuizat. Nu-l calcula din `quantity` și nu-l înlocui cu `0`. Un produs fără stoc — și, cu `?warehouseId=`, fără stoc în depozitul cerut — are `quantity`, `reserved` și `available` 0. Un `reserved` absent înseamnă că valoarea nu se știe.\n- Ordinea e după id, nu după nume. Parcurge paginile până la `nextCursor: null`.\n- Lista se păstrează în cache **45 de secunde**, deci stocul din ea poate întârzia. Pentru stocul la zi folosește „Stoc produs”.\n- Un produs fără card la casă nu apare decât cu `includeUnlisted=true`.\n- Listele se trimit repetând parametrul (`?a=x&a=y`), nu cu virgulă. Un boolean se scrie `true`/`false`, nu `1`."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/products?limit=50",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "products"
                  ],
                  "query": [
                    {
                      "key": "limit",
                      "value": "50",
                      "description": "1–200, implicit 50."
                    },
                    {
                      "key": "search",
                      "value": "apa",
                      "description": "Cuvinte din cod sau denumire.",
                      "disabled": true
                    },
                    {
                      "key": "categoryCode",
                      "value": "BAUTURI",
                      "description": "Codul categoriei; repetabil.",
                      "disabled": true
                    },
                    {
                      "key": "categoryId",
                      "value": "",
                      "description": "Id-ul categoriei; repetabil.",
                      "disabled": true
                    },
                    {
                      "key": "includeSubcategories",
                      "value": "true",
                      "description": "Implicit true.",
                      "disabled": true
                    },
                    {
                      "key": "attribute",
                      "value": "tara:Moldova",
                      "description": "cod:valoare; repetabil.",
                      "disabled": true
                    },
                    {
                      "key": "includeInactive",
                      "value": "false",
                      "description": "Implicit false.",
                      "disabled": true
                    },
                    {
                      "key": "includeUnlisted",
                      "value": "false",
                      "description": "Implicit false.",
                      "disabled": true
                    },
                    {
                      "key": "warehouseId",
                      "value": "",
                      "description": "Stocul unui singur depozit; implicit toate.",
                      "disabled": true
                    },
                    {
                      "key": "cursor",
                      "value": "",
                      "description": "nextCursor din pagina precedentă.",
                      "disabled": true
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items\": [\n    {\n      \"posfixId\": \"0b8f5e2a-6c1d-4e7f-9a3b-2d4c6e8f1a01\",\n      \"code\": \"0000000123\",\n      \"name\": \"Apă minerală Borjomi 0,5 L\",\n      \"description\": \"Apă minerală naturală carbogazoasă.\",\n      \"barcode\": \"4860019001339\",\n      \"price\": 18.5,\n      \"currency\": \"MDL\",\n      \"vatRate\": 20,\n      \"unit\": \"buc\",\n      \"isActive\": true,\n      \"category\": {\n        \"id\": \"a1b2c3d4-0002-4a5b-8c6d-7e8f9a0b1c02\",\n        \"name\": \"Apă minerală\",\n        \"code\": \"APA\",\n        \"names\": {\n          \"ro\": \"Apă minerală\",\n          \"ru\": \"Минеральная вода\"\n        }\n      },\n      \"imageUrl\": \"/api/public/v1/products/0000000123/image\",\n      \"attributes\": [\n        {\n          \"code\": \"tara\",\n          \"value\": \"Georgia\",\n          \"valueNames\": {\n            \"ro\": \"Georgia\",\n            \"ru\": \"Грузия\"\n          }\n        },\n        {\n          \"code\": \"brand\",\n          \"value\": \"Borjomi\",\n          \"valueNames\": {\n            \"ro\": \"Borjomi\"\n          }\n        }\n      ],\n      \"composition\": [],\n      \"onHand\": [\n        {\n          \"warehouseId\": null,\n          \"quantity\": 144,\n          \"reserved\": 6,\n          \"available\": 138\n        }\n      ],\n      \"onHandByWarehouse\": [\n        {\n          \"warehouseId\": \"e4d3c2b1-0001-4f5e-8d7c-6b5a49382701\",\n          \"code\": \"DEP-1\",\n          \"name\": \"Depozit central\",\n          \"quantity\": 120,\n          \"reserved\": 6,\n          \"available\": 114\n        },\n        {\n          \"warehouseId\": \"e4d3c2b1-0002-4f5e-8d7c-6b5a49382702\",\n          \"code\": \"MAG-2\",\n          \"name\": \"Magazin Botanica\",\n          \"quantity\": 24,\n          \"reserved\": 0,\n          \"available\": 24\n        }\n      ],\n      \"names\": {\n        \"ro\": \"Apă minerală Borjomi 0,5 L\",\n        \"ru\": \"Минеральная вода Borjomi 0,5 л\"\n      },\n      \"descriptions\": {\n        \"ro\": \"Apă minerală naturală carbogazoasă.\",\n        \"ru\": \"Натуральная газированная минеральная вода.\"\n      },\n      \"contentBlocks\": [],\n      \"categoryPath\": [\n        {\n          \"id\": \"a1b2c3d4-0001-4a5b-8c6d-7e8f9a0b1c01\",\n          \"name\": \"Băuturi\",\n          \"code\": \"BAUTURI\",\n          \"names\": {\n            \"ro\": \"Băuturi\",\n            \"ru\": \"Напитки\"\n          }\n        },\n        {\n          \"id\": \"a1b2c3d4-0002-4a5b-8c6d-7e8f9a0b1c02\",\n          \"name\": \"Apă minerală\",\n          \"code\": \"APA\",\n          \"names\": {\n            \"ro\": \"Apă minerală\",\n            \"ru\": \"Минеральная вода\"\n          }\n        }\n      ],\n      \"sku\": \"BRJ-05\",\n      \"shelfLifeDays\": 730,\n      \"packaging\": [\n        {\n          \"unit\": \"bax\",\n          \"factor\": 12\n        }\n      ],\n      \"inStock\": true\n    }\n  ],\n  \"nextCursor\": \"0b8f5e2a-6c1d-4e7f-9a3b-2d4c6e8f1a01\"\n}"
            },
            {
              "name": "400 — filtru de atribut greșit",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/products?limit=50",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "products"
                  ],
                  "query": [
                    {
                      "key": "limit",
                      "value": "50",
                      "description": "1–200, implicit 50."
                    },
                    {
                      "key": "search",
                      "value": "apa",
                      "description": "Cuvinte din cod sau denumire.",
                      "disabled": true
                    },
                    {
                      "key": "categoryCode",
                      "value": "BAUTURI",
                      "description": "Codul categoriei; repetabil.",
                      "disabled": true
                    },
                    {
                      "key": "categoryId",
                      "value": "",
                      "description": "Id-ul categoriei; repetabil.",
                      "disabled": true
                    },
                    {
                      "key": "includeSubcategories",
                      "value": "true",
                      "description": "Implicit true.",
                      "disabled": true
                    },
                    {
                      "key": "attribute",
                      "value": "tara:Moldova",
                      "description": "cod:valoare; repetabil.",
                      "disabled": true
                    },
                    {
                      "key": "includeInactive",
                      "value": "false",
                      "description": "Implicit false.",
                      "disabled": true
                    },
                    {
                      "key": "includeUnlisted",
                      "value": "false",
                      "description": "Implicit false.",
                      "disabled": true
                    },
                    {
                      "key": "warehouseId",
                      "value": "",
                      "description": "Stocul unui singur depozit; implicit toate.",
                      "disabled": true
                    },
                    {
                      "key": "cursor",
                      "value": "",
                      "description": "nextCursor din pagina precedentă.",
                      "disabled": true
                    }
                  ]
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/problem+json"
                }
              ],
              "body": "{\n  \"statusCode\": 400,\n  \"message\": \"One or more errors occurred!\",\n  \"errors\": {\n    \"attribute\": [\n      \"Filtrul de atribut „brand” trebuie să aibă forma cod:valoare (de ex. brand:Samyang).\"\n    ]\n  },\n  \"errorCodes\": {\n    \"attribute\": [\n      {\n        \"code\": \"INTEGRATIONS_ATTRIBUTE_FILTER_FORMAT\",\n        \"params\": {\n          \"filter\": \"brand\"\n        }\n      }\n    ]\n  }\n}"
            }
          ]
        },
        {
          "name": "Produs după cod",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/products/:code",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "products",
                ":code"
              ],
              "variable": [
                {
                  "key": "code",
                  "value": "0000000123",
                  "description": "Codul produsului"
                }
              ]
            },
            "description": "**Scope necesar:** `products:read`\n\nFișa completă a unui produs: tot ce dă lista, plus compoziția și blocurile de conținut pe limbi. Găsește și produsele inactive sau fără card la casă.\n\n### Parametri de cale\n\n| Parametru | Tip | Descriere |\n|---|---|---|\n| `code` | string | Codul exact al produsului (cu zerourile din față). |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `posfixId` | string (uuid) | Id-ul produsului în PosFix. Cu el ceri prețuri. |\n| `code` | string | Codul produsului (de obicei 10 cifre). Cu el comanzi și citești stocul. |\n| `name` | string | Denumirea din nomenclator. |\n| `names` | object | Denumirea pe limbi; regula hărților de limbi e în notele cererii. |\n| `description` | string? | Descrierea în română. |\n| `barcode` | string | Codul de bare; `\"\"` dacă lipsește. |\n| `price`, `currency` | number, string | Prețul de vânzare de la casă (cu TVA); fără card la casă, prețul implicit din nomenclator. |\n| `vatRate` | number? | Cota TVA, %. `null` = cotă nestabilită, diferit de `0`. |\n| `unit` | string | Unitatea de bază (`buc`, `kg`). |\n| `isActive` | boolean | Produsul e activ în nomenclator. |\n| `category`, `categoryPath[]` | object?, array | Categoria de la casă (`id`, `name`, `code`, `names`) și drumul ei de la rădăcină. |\n| `imageUrl` | string? | Adresa **relativă** a imaginii; o citești cu aceeași cheie (cererea „Imaginea produsului”). |\n| `attributes[]` | array | `code` și `value` — valorile atributelor (marcă, țară…); `valueNames` = valoarea pe limbi. |\n| `onHand[]` | array | Stocul total, un singur element cu `warehouseId: null`: `quantity`, `reserved`, `available` (sensul lor e în notele cererii). Cu `?warehouseId=` la listă, stocul acelui depozit, cu id-ul lui. |\n| `onHandByWarehouse[]` | array | Stocul pe fiecare depozit care are marfa: `warehouseId`, `code`, `name`, `quantity`, `reserved`, `available`. Depozitul care lipsește are 0. |\n| `inStock` | boolean | Stocul fizic total e peste zero, chiar dacă e tot rezervat. Dacă se mai poate cumpăra îți spune `onHand[].available`. |\n| `descriptions` | object | Descrierea pe limbi; regula hărților de limbi e în notele cererii. |\n| `composition[]` | array | Compoziția: `ingredient`, `percent`, `allergen`. Doar la „Produs după cod”. |\n| `contentBlocks[]` | array | Blocurile de conținut (HTML curățat): `labels` și `contents` pe limbi, plus câmpurile vechi `labelRo`… `contentEn`. Doar la „Produs după cod”. |\n| `sku` | string? | Codul extern al produsului, altfel SKU-ul de la casă. |\n| `shelfLifeDays` | integer? | Termenul de valabilitate, zile. |\n| `packaging[]` | array | `unit` și `factor`: „1 `unit` = `factor` unități de bază”. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 404 | — | Codul nu există. Corp gol. |\n| 400 | `INTEGRATIONS_PRODUCT_CODE_REQUIRED` | „Codul produsului (productCode) este obligatoriu.” |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Hărțile de limbi (`names`, `descriptions`, `valueNames`, `labels`, `contents`) au cheia = codul limbii: `{ \"ro\": …, \"ru\": … }`. Limbile vin din lista platformei și pot crește oricând — citește cheile primite, nu presupune exact `ro`/`ru`/`en`. O limbă fără traducere lipsește din hartă: folosești textul de bază (`name`, `description`).\n- Stocul are trei cifre: `quantity` e marfa din depozit; `reserved` e partea promisă pe comenzile confirmate care n-au plecat încă; `available` = `quantity − reserved` e ce se mai poate vinde. În magazin arată `available`.\n- `available` lipsește când nu există un disponibil de publicat (preparatele cu rețetă, pozițiile-serviciu) sau când nu se știe; un `available` absent nu înseamnă epuizat. Nu-l calcula din `quantity` și nu-l înlocui cu `0`. Un produs fără stoc — și, cu `?warehouseId=`, fără stoc în depozitul cerut — are `quantity`, `reserved` și `available` 0. Un `reserved` absent înseamnă că valoarea nu se știe.\n- Blocurile de conținut vin doar pentru produsele active și doar pentru definițiile active (vezi „Blocuri de conținut”).\n- Câmpurile vechi ale blocului (`labelRu`, `contentEn`…) rămân pentru integrările existente și nu vor purta niciodată o limbă adăugată ulterior; folosește `labels` și `contents`.\n- Răspunsul stă în cache 45 de secunde."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/products/:code",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "products",
                    ":code"
                  ],
                  "variable": [
                    {
                      "key": "code",
                      "value": "0000000123",
                      "description": "Codul produsului"
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"posfixId\": \"0b8f5e2a-6c1d-4e7f-9a3b-2d4c6e8f1a01\",\n  \"code\": \"0000000123\",\n  \"name\": \"Apă minerală Borjomi 0,5 L\",\n  \"description\": \"Apă minerală naturală carbogazoasă.\",\n  \"barcode\": \"4860019001339\",\n  \"price\": 18.5,\n  \"currency\": \"MDL\",\n  \"vatRate\": 20,\n  \"unit\": \"buc\",\n  \"isActive\": true,\n  \"category\": {\n    \"id\": \"a1b2c3d4-0002-4a5b-8c6d-7e8f9a0b1c02\",\n    \"name\": \"Apă minerală\",\n    \"code\": \"APA\",\n    \"names\": {\n      \"ro\": \"Apă minerală\",\n      \"ru\": \"Минеральная вода\"\n    }\n  },\n  \"imageUrl\": \"/api/public/v1/products/0000000123/image\",\n  \"attributes\": [\n    {\n      \"code\": \"tara\",\n      \"value\": \"Georgia\",\n      \"valueNames\": {\n        \"ro\": \"Georgia\",\n        \"ru\": \"Грузия\"\n      }\n    },\n    {\n      \"code\": \"brand\",\n      \"value\": \"Borjomi\",\n      \"valueNames\": {\n        \"ro\": \"Borjomi\"\n      }\n    }\n  ],\n  \"composition\": [\n    {\n      \"ingredient\": \"Apă minerală naturală\",\n      \"percent\": null,\n      \"allergen\": false\n    },\n    {\n      \"ingredient\": \"Dioxid de carbon\",\n      \"percent\": null,\n      \"allergen\": false\n    }\n  ],\n  \"onHand\": [\n    {\n      \"warehouseId\": null,\n      \"quantity\": 144,\n      \"reserved\": 6,\n      \"available\": 138\n    }\n  ],\n  \"onHandByWarehouse\": [\n    {\n      \"warehouseId\": \"e4d3c2b1-0001-4f5e-8d7c-6b5a49382701\",\n      \"code\": \"DEP-1\",\n      \"name\": \"Depozit central\",\n      \"quantity\": 120,\n      \"reserved\": 6,\n      \"available\": 114\n    },\n    {\n      \"warehouseId\": \"e4d3c2b1-0002-4f5e-8d7c-6b5a49382702\",\n      \"code\": \"MAG-2\",\n      \"name\": \"Magazin Botanica\",\n      \"quantity\": 24,\n      \"reserved\": 0,\n      \"available\": 24\n    }\n  ],\n  \"names\": {\n    \"ro\": \"Apă minerală Borjomi 0,5 L\",\n    \"ru\": \"Минеральная вода Borjomi 0,5 л\"\n  },\n  \"descriptions\": {\n    \"ro\": \"Apă minerală naturală carbogazoasă.\",\n    \"ru\": \"Натуральная газированная минеральная вода.\"\n  },\n  \"contentBlocks\": [\n    {\n      \"key\": \"pastrare\",\n      \"labelRo\": \"Condiții de păstrare\",\n      \"labelRu\": \"Условия хранения\",\n      \"labelEn\": null,\n      \"contentRo\": \"<p>A se păstra la loc uscat, ferit de soare.</p>\",\n      \"contentRu\": null,\n      \"contentEn\": null,\n      \"sortOrder\": 1,\n      \"labels\": {\n        \"ro\": \"Condiții de păstrare\",\n        \"ru\": \"Условия хранения\"\n      },\n      \"contents\": {\n        \"ro\": \"<p>A se păstra la loc uscat, ferit de soare.</p>\"\n      }\n    }\n  ],\n  \"categoryPath\": [\n    {\n      \"id\": \"a1b2c3d4-0001-4a5b-8c6d-7e8f9a0b1c01\",\n      \"name\": \"Băuturi\",\n      \"code\": \"BAUTURI\",\n      \"names\": {\n        \"ro\": \"Băuturi\",\n        \"ru\": \"Напитки\"\n      }\n    },\n    {\n      \"id\": \"a1b2c3d4-0002-4a5b-8c6d-7e8f9a0b1c02\",\n      \"name\": \"Apă minerală\",\n      \"code\": \"APA\",\n      \"names\": {\n        \"ro\": \"Apă minerală\",\n        \"ru\": \"Минеральная вода\"\n      }\n    }\n  ],\n  \"sku\": \"BRJ-05\",\n  \"shelfLifeDays\": 730,\n  \"packaging\": [\n    {\n      \"unit\": \"bax\",\n      \"factor\": 12\n    }\n  ],\n  \"inStock\": true\n}"
            }
          ]
        },
        {
          "name": "Imaginea produsului",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/products/:code/image",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "products",
                ":code",
                "image"
              ],
              "variable": [
                {
                  "key": "code",
                  "value": "0000000123",
                  "description": "Codul produsului"
                }
              ]
            },
            "description": "**Scope necesar:** `products:read`\n\nImaginea produsului din nomenclator, ca fișier. De obicei o miniatură JPEG de cel mult 320×320.\n\n### Parametri de cale\n\n| Parametru | Tip | Descriere |\n|---|---|---|\n| `code` | string | Codul produsului. |\n\n### Răspuns\n\n**200** cu octeții imaginii. `Content-Type` e tipul imaginii (`image/jpeg` pentru miniatură); `Cache-Control: private, max-age=300`.\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 404 | — | Produs inexistent, fără imagine sau imagine indisponibilă. Corp gol. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Cererea cere cheia, deci `imageUrl` nu merge direct într-un `<img src>` din browserul clientului tău. Descarc-o pe server și servește-o tu."
          },
          "response": [
            {
              "name": "404 — produs fără imagine",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/products/:code/image",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "products",
                    ":code",
                    "image"
                  ],
                  "variable": [
                    {
                      "key": "code",
                      "value": "0000000123",
                      "description": "Codul produsului"
                    }
                  ]
                }
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "text",
              "header": [],
              "body": ""
            }
          ]
        },
        {
          "name": "Stoc produs",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/inventory?productCode=0000000123",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "inventory"
              ],
              "query": [
                {
                  "key": "productCode",
                  "value": "0000000123",
                  "description": "Codul produsului, obligatoriu."
                },
                {
                  "key": "warehouseId",
                  "value": "",
                  "description": "Un depozit; fără el, totalul.",
                  "disabled": true
                }
              ]
            },
            "description": "**Scope necesar:** `inventory:read`\n\nStocul la zi al unui produs, pe un depozit sau pe toate. Nu trece prin cache.\n\n### Parametri de interogare\n\n| Parametru | Tip | Implicit | Descriere |\n|---|---|---|---|\n| `productCode` | string | — | Obligatoriu. Codul exact al produsului. |\n| `warehouseId` | string (uuid) | toate | Depozitul. Un depozit necunoscut dă cantitatea 0, nu eroare. |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `posfixId`, `code`, `name` | string | Produsul. |\n| `onHand[]` | array | Un singur element: `warehouseId` (cel cerut sau `null` = total), `quantity`, `reserved` și `available`, în unitatea de bază. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `INTEGRATIONS_PRODUCT_CODE_REQUIRED` | „Codul produsului (productCode) este obligatoriu.” |\n| 404 | — | Codul nu există. Corp gol. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Stocul are trei cifre: `quantity` e marfa din depozit; `reserved` e partea promisă pe comenzile confirmate care n-au plecat încă; `available` = `quantity − reserved` e ce se mai poate vinde. În magazin arată `available`.\n- `available` lipsește când nu există un disponibil de publicat (preparatele cu rețetă, pozițiile-serviciu) sau când nu se știe; un `available` absent nu înseamnă epuizat. Nu-l calcula din `quantity` și nu-l înlocui cu `0`. Un produs fără stoc — și, cu `?warehouseId=`, fără stoc în depozitul cerut — are `quantity`, `reserved` și `available` 0. Un `reserved` absent înseamnă că valoarea nu se știe.\n- Cantitatea poate fi **negativă**: s-a vândut marfă care nu fusese încă recepționată.\n- `available` e negativ și când s-a rezervat mai mult decât e în depozit."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/inventory?productCode=0000000123",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "inventory"
                  ],
                  "query": [
                    {
                      "key": "productCode",
                      "value": "0000000123",
                      "description": "Codul produsului, obligatoriu."
                    },
                    {
                      "key": "warehouseId",
                      "value": "",
                      "description": "Un depozit; fără el, totalul.",
                      "disabled": true
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"posfixId\": \"0b8f5e2a-6c1d-4e7f-9a3b-2d4c6e8f1a01\",\n  \"code\": \"0000000123\",\n  \"name\": \"Apă minerală Borjomi 0,5 L\",\n  \"onHand\": [\n    {\n      \"warehouseId\": null,\n      \"quantity\": 144,\n      \"reserved\": 6,\n      \"available\": 138\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Depozite",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/warehouses",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "warehouses"
              ]
            },
            "description": "**Scope necesar:** `inventory:read`\n\nDepozitele active ale comerciantului, cel implicit primul. Id-ul merge în `warehouseId` la „Listă produse” și la „Stoc produs”.\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `items[].id` | string (uuid) | Id-ul depozitului. |\n| `items[].code` | string | Codul depozitului. |\n| `items[].name` | string | Denumirea depozitului. |\n| `items[].isDefault` | boolean | Depozitul implicit al comerciantului. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Lista stă în cache 45 de secunde.\n- Un depozit dezactivat lipsește din listă, dar stocul lui rămâne în `onHand` și `onHandByWarehouse` până se mută marfa."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/warehouses",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "warehouses"
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items\": [\n    {\n      \"id\": \"e4d3c2b1-0001-4f5e-8d7c-6b5a49382701\",\n      \"code\": \"DEP-1\",\n      \"name\": \"Depozit central\",\n      \"isDefault\": true\n    },\n    {\n      \"id\": \"e4d3c2b1-0002-4f5e-8d7c-6b5a49382702\",\n      \"code\": \"MAG-2\",\n      \"name\": \"Magazin Botanica\",\n      \"isDefault\": false\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Atribute",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/attributes",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "attributes"
              ]
            },
            "description": "**Scope necesar:** `products:read`\n\nAtributele produselor (marcă, țară, gust…) cu valorile folosite. Construiește filtrele magazinului din ele; codul merge în `attribute=cod:valoare` la „Listă produse”.\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `items[].code` | string | Codul atributului, cu litere mici. |\n| `items[].name` | string | Denumirea dată de comerciant. |\n| `items[].names` | object | Denumirea pe limbi (hartă de limbi, ca la produs). |\n| `items[].dataType` | string | `string`, `number`, `boolean`, `date` sau `enum`. |\n| `items[].values[]` | string[] | Valorile predefinite și cele folosite pe produse, fără dubluri. |\n| `items[].valueNames[]` | array | Fiecare valoare din `values`, în aceeași ordine: `value` și `names` (valoarea pe limbi). |\n| `items[].sortOrder` | integer | Ordinea de afișare. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- O valoare folosită doar pe produse inactive apare în listă, dar filtrul pe ea poate întoarce zero produse.\n- Filtrul `attribute=cod:valoare` primește valoarea din `values`, niciodată o traducere din `valueNames`."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/attributes",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "attributes"
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items\": [\n    {\n      \"code\": \"brand\",\n      \"name\": \"Marcă\",\n      \"dataType\": \"string\",\n      \"values\": [\n        \"Borjomi\",\n        \"Franzeluța\",\n        \"Nongshim\"\n      ],\n      \"sortOrder\": 1\n    },\n    {\n      \"code\": \"tara\",\n      \"name\": \"Țara de origine\",\n      \"dataType\": \"enum\",\n      \"values\": [\n        \"Georgia\",\n        \"Moldova\"\n      ],\n      \"sortOrder\": 2,\n      \"names\": {\n        \"ro\": \"Țara de origine\",\n        \"ru\": \"Страна происхождения\"\n      },\n      \"valueNames\": [\n        {\n          \"value\": \"Georgia\",\n          \"names\": {\n            \"ro\": \"Georgia\",\n            \"ru\": \"Грузия\"\n          }\n        },\n        {\n          \"value\": \"Moldova\",\n          \"names\": {\n            \"ro\": \"Moldova\",\n            \"ru\": \"Молдова\"\n          }\n        }\n      ]\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Categorii",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/categories?tree=true",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "categories"
              ],
              "query": [
                {
                  "key": "tree",
                  "value": "true",
                  "description": "true = arbore; implicit listă plată."
                }
              ]
            },
            "description": "**Scope necesar:** `products:read`\n\nCategoriile de la casă (cele după care se grupează produsele la vânzare), active. Categoriile contabile din nomenclator nu apar aici.\n\n### Parametri de interogare\n\n| Parametru | Tip | Implicit | Descriere |\n|---|---|---|---|\n| `tree` | boolean | `false` | `true` = rădăcinile, cu `children` completat; altfel toate categoriile într-o listă, cu `children: null`. |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `items[].id`, `name`, `code` | string | Categoria; codul poate fi gol. |\n| `items[].names` | object | Denumirea pe limbi (hartă de limbi, ca la produs). |\n| `items[].parentId` | string (uuid)? | Categoria părinte. |\n| `items[].sortOrder` | integer | Ordinea de afișare. |\n| `items[].children` | array? | Subcategoriile, doar cu `tree=true`. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | — | `tree` altceva decât `true`/`false`. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- În lista plată, `parentId` poate indica o categorie inactivă, care lipsește din listă: tratează acea categorie ca rădăcină."
          },
          "response": [
            {
              "name": "200 — arbore",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/categories?tree=true",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "categories"
                  ],
                  "query": [
                    {
                      "key": "tree",
                      "value": "true",
                      "description": "true = arbore; implicit listă plată."
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items\": [\n    {\n      \"id\": \"a1b2c3d4-0001-4a5b-8c6d-7e8f9a0b1c01\",\n      \"name\": \"Băuturi\",\n      \"code\": \"BAUTURI\",\n      \"parentId\": null,\n      \"sortOrder\": 1,\n      \"names\": {\n        \"ro\": \"Băuturi\",\n        \"ru\": \"Напитки\"\n      },\n      \"children\": [\n        {\n          \"id\": \"a1b2c3d4-0002-4a5b-8c6d-7e8f9a0b1c02\",\n          \"name\": \"Apă minerală\",\n          \"code\": \"APA\",\n          \"parentId\": \"a1b2c3d4-0001-4a5b-8c6d-7e8f9a0b1c01\",\n          \"sortOrder\": 1,\n          \"names\": {\n            \"ro\": \"Apă minerală\",\n            \"ru\": \"Минеральная вода\"\n          },\n          \"children\": []\n        }\n      ]\n    },\n    {\n      \"id\": \"a1b2c3d4-0004-4a5b-8c6d-7e8f9a0b1c04\",\n      \"name\": \"Panificație\",\n      \"code\": \"PANIFICATIE\",\n      \"parentId\": null,\n      \"sortOrder\": 2,\n      \"names\": {\n        \"ro\": \"Panificație\"\n      },\n      \"children\": []\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Blocuri de conținut",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/content-blocks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "content-blocks"
              ]
            },
            "description": "**Scope necesar:** `products:read`\n\nDefinițiile blocurilor de conținut ale comerciantului (ingrediente, păstrare…). Fac locurile din șablonul paginii de produs; conținutul fiecărui produs vine la „Produs după cod”.\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `items[].key` | string | Cheia; aceeași ca în `contentBlocks[].key` al produsului. |\n| `items[].labels` | object | Eticheta pe limbi (hartă de limbi, ca la produs). |\n| `items[].labelRo` | string | Eticheta în română. |\n| `items[].labelRu`, `labelEn` | string? | Etichetele în rusă și engleză — câmpuri vechi; o limbă adăugată ulterior apare doar în `labels`. |\n| `items[].sortOrder` | integer | Ordinea de afișare. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |"
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/content-blocks",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "content-blocks"
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items\": [\n    {\n      \"key\": \"ingrediente\",\n      \"labelRo\": \"Ingrediente\",\n      \"labelRu\": \"Состав\",\n      \"labelEn\": null,\n      \"sortOrder\": 1,\n      \"labels\": {\n        \"ro\": \"Ingrediente\",\n        \"ru\": \"Состав\"\n      }\n    },\n    {\n      \"key\": \"pastrare\",\n      \"labelRo\": \"Condiții de păstrare\",\n      \"labelRu\": \"Условия хранения\",\n      \"labelEn\": \"Storage\",\n      \"sortOrder\": 2,\n      \"labels\": {\n        \"ro\": \"Condiții de păstrare\",\n        \"ru\": \"Условия хранения\",\n        \"en\": \"Storage\"\n      }\n    }\n  ]\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "2. Prețuri",
      "description": "Listele de prețuri și prețul efectiv al produselor pe o listă.",
      "item": [
        {
          "name": "Liste de prețuri",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/price-lists",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "price-lists"
              ]
            },
            "description": "**Scope necesar:** `products:read`\n\nListele de prețuri ale comerciantului, cu lista implicită prima. Id-ul lor merge în „Prețuri pe produse”.\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `items[].id`, `code`, `name` | string | Lista. |\n| `items[].currencyCode` | string | Moneda listei. |\n| `items[].isDefault` | boolean | Lista implicită a comerciantului. |\n| `items[].isActive` | boolean | Lista e activă. Apar și cele inactive. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- O listă cu condiții (public, volum) sau în afara perioadei ei de valabilitate apare aici, dar „Prețuri pe produse” o ignoră și dă prețul implicit.\n- Listele florăriilor nu apar aici: prețul lor se aplică doar în florăriile alese, nu prin API-ul public."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/price-lists",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "price-lists"
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items\": [\n    {\n      \"id\": \"5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a01\",\n      \"code\": \"PL-RETAIL\",\n      \"name\": \"Preț cu amănuntul\",\n      \"currencyCode\": \"MDL\",\n      \"isDefault\": true,\n      \"isActive\": true\n    },\n    {\n      \"id\": \"5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a02\",\n      \"code\": \"ANGRO\",\n      \"name\": \"Angro HoReCa\",\n      \"currencyCode\": \"MDL\",\n      \"isDefault\": false,\n      \"isActive\": true\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Prețuri pe produse",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/products/prices",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "products",
                "prices"
              ]
            },
            "description": "**Scope necesar:** `products:read`\n\nPrețul efectiv al unor produse pe o listă de prețuri — de exemplu, prețurile de angro pentru un client HoReCa.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `productIds[]` | string (uuid)[] | da | `posfixId`-urile produselor, 1–1000. Goală → `400` „Lista de produse (productIds) este obligatorie.”; peste 1000 → `400` „Cel mult 1.000 produse pe un apel.” |\n| `priceListId` | string (uuid) | nu | Lista. Lipsă → lista implicită a comerciantului. |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `items[].posfixId` | string (uuid) | Produsul. |\n| `items[].price` | number | Prețul efectiv. |\n| `items[].currency` | string | Moneda produsului. |\n\n### Cum se alege prețul\n\n1. Prețul explicit al produsului din listă, dacă există și e valabil azi.\n2. La o listă de tip reducere: prețul implicit minus procentul listei, rotunjit la 2 zecimale.\n3. Altfel, prețul implicit al produsului din nomenclator.\n\nLista se ignoră (toate produsele primesc prețul implicit) dacă e inactivă, a altui comerciant, are condiții de public sau de volum, e lista unei florării sau e în afara perioadei de valabilitate. Nu primești eroare în aceste cazuri.\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `INTEGRATIONS_PRICE_PRODUCTS_REQUIRED`, `INTEGRATIONS_PRICE_PRODUCTS_TOO_MANY` | Lista lipsește sau are peste 1000 de id-uri (format simplu); un id nu e GUID (formatul de legare). |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Produsele necunoscute se **omit** din răspuns, fără eroare; id-urile repetate dau un singur rând. Leagă rândurile după `posfixId`, nu după ordine.\n- Prețurile nu se convertesc: o listă în altă monedă se aplică doar produselor în moneda ei.\n- Lista clientului (`defaultPriceListId` de la „Clienți”) nu se aplică singură: trimite-o tu în `priceListId`.\n- Corpul se citește doar cu `Content-Type: application/json`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"productIds\": [\n    \"0b8f5e2a-6c1d-4e7f-9a3b-2d4c6e8f1a01\",\n    \"1c9a6f3b-7d2e-4f80-8b4c-3e5d7f9a2b02\"\n  ],\n  \"priceListId\": \"5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a02\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/products/prices",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "products",
                    "prices"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"productIds\": [\n    \"0b8f5e2a-6c1d-4e7f-9a3b-2d4c6e8f1a01\",\n    \"1c9a6f3b-7d2e-4f80-8b4c-3e5d7f9a2b02\"\n  ],\n  \"priceListId\": \"5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a02\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items\": [\n    {\n      \"posfixId\": \"0b8f5e2a-6c1d-4e7f-9a3b-2d4c6e8f1a01\",\n      \"price\": 15.9,\n      \"currency\": \"MDL\"\n    },\n    {\n      \"posfixId\": \"1c9a6f3b-7d2e-4f80-8b4c-3e5d7f9a2b02\",\n      \"price\": 7.11,\n      \"currency\": \"MDL\"\n    }\n  ]\n}"
            },
            {
              "name": "400 — listă goală",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/products/prices",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "products",
                    "prices"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"productIds\": [\n    \"0b8f5e2a-6c1d-4e7f-9a3b-2d4c6e8f1a01\",\n    \"1c9a6f3b-7d2e-4f80-8b4c-3e5d7f9a2b02\"\n  ],\n  \"priceListId\": \"5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a02\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"type\": \"https://httpstatuses.io/400\",\n  \"title\": \"Validation Failed\",\n  \"status\": 400,\n  \"detail\": \"Lista de produse (productIds) este obligatorie.\",\n  \"errorCode\": \"INTEGRATIONS_PRICE_PRODUCTS_REQUIRED\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "3. Clienți",
      "description": "Clienții comerciantului. Un client se identifică după codul fiscal.",
      "item": [
        {
          "name": "Creează client",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/customers",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "customers"
              ]
            },
            "description": "**Scope necesar:** `customers:write`\n\nCreează clientul după codul fiscal. Dacă există deja un partener cu același cod, îl întoarce **neschimbat** — cererea nu actualizează nimic.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `name` | string | da | Denumirea. Goală → `400` „Numele clientului este obligatoriu.” |\n| `fiscalCode` | string | da | IDNO sau IDNP, exact 13 cifre. Altfel → `400` „Codul fiscal (IDNO/IDNP, 13 cifre) este obligatoriu pentru un client distinct.” |\n| `email` | string | nu | Nu se validează. |\n| `phone` | string | nu | Se salvează așa cum vine. |\n| `address` | string | nu | Se salvează ca adresă juridică și reală. |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `posfixId` | string (uuid) | Id-ul partenerului în PosFix. Pe comenzi apare ca `partnerId`. |\n| `code` | string | Codul partenerului, 10 cifre (`0000000318`). |\n| `name` | string | Denumirea, normalizată: „Societate cu Răspundere Limitată „FLOAREA”” devine `SRL FLOAREA`. |\n| `fiscalCode` | string? | IDNO sau IDNP. |\n| `email`, `phone` | string? | Contactele. |\n| `address` | string? | Adresa reală; dacă lipsește, cea juridică. |\n| `isActive` | boolean | Partenerul e activ. |\n| `defaultPriceListId` | string (uuid)? | Lista de prețuri a clientului, dacă are una. Lista unei florării nu se arată: câmpul e gol. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `INTEGRATIONS_CUSTOMER_NAME_REQUIRED`, `INTEGRATIONS_CUSTOMER_FISCAL_CODE_REQUIRED` | Nume gol sau cod fiscal greșit (format simplu). |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Răspunsul e `200` și la creare, și când clientul exista. Nu există un câmp care să spună care dintre ele.\n- Un partener care era doar furnizor devine și client.\n- Codul care începe cu `2` (IDNP) creează o persoană fizică; restul, persoane juridice.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"SRL Floarea Soarelui\",\n  \"fiscalCode\": \"1012600034567\",\n  \"email\": \"contabil@floarea.md\",\n  \"phone\": \"+37322123456\",\n  \"address\": \"mun. Chișinău, str. Ismail 45\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 — client creat sau găsit",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/customers",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "customers"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"name\": \"SRL Floarea Soarelui\",\n  \"fiscalCode\": \"1012600034567\",\n  \"email\": \"contabil@floarea.md\",\n  \"phone\": \"+37322123456\",\n  \"address\": \"mun. Chișinău, str. Ismail 45\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"posfixId\": \"b4e7c1a2-9d3f-4e61-8a0b-2f5c7d9e1a34\",\n  \"code\": \"0000000318\",\n  \"name\": \"SRL Floarea Soarelui\",\n  \"fiscalCode\": \"1012600034567\",\n  \"email\": \"contabil@floarea.md\",\n  \"phone\": \"+37322123456\",\n  \"address\": \"mun. Chișinău, str. Ismail 45\",\n  \"isActive\": true,\n  \"defaultPriceListId\": null\n}"
            },
            {
              "name": "400 — cod fiscal greșit",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/customers",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "customers"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"name\": \"SRL Floarea Soarelui\",\n  \"fiscalCode\": \"1012600034567\",\n  \"email\": \"contabil@floarea.md\",\n  \"phone\": \"+37322123456\",\n  \"address\": \"mun. Chișinău, str. Ismail 45\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"type\": \"https://httpstatuses.io/400\",\n  \"title\": \"Validation Failed\",\n  \"status\": 400,\n  \"detail\": \"Codul fiscal (IDNO/IDNP, 13 cifre) este obligatoriu pentru un client distinct.\",\n  \"errorCode\": \"INTEGRATIONS_CUSTOMER_FISCAL_CODE_REQUIRED\"\n}"
            }
          ]
        },
        {
          "name": "Client după id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/customers/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "customers",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "posfixId-ul clientului"
                }
              ]
            },
            "description": "**Scope necesar:** `customers:read`\n\nUn client al comerciantului, după `posfixId`.\n\n### Parametri de cale\n\n| Parametru | Tip | Descriere |\n|---|---|---|\n| `id` | string (uuid) | Id-ul clientului. Altceva decât un GUID → `400`. |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `posfixId` | string (uuid) | Id-ul partenerului în PosFix. Pe comenzi apare ca `partnerId`. |\n| `code` | string | Codul partenerului, 10 cifre (`0000000318`). |\n| `name` | string | Denumirea, normalizată: „Societate cu Răspundere Limitată „FLOAREA”” devine `SRL FLOAREA`. |\n| `fiscalCode` | string? | IDNO sau IDNP. |\n| `email`, `phone` | string? | Contactele. |\n| `address` | string? | Adresa reală; dacă lipsește, cea juridică. |\n| `isActive` | boolean | Partenerul e activ. |\n| `defaultPriceListId` | string (uuid)? | Lista de prețuri a clientului, dacă are una. Lista unei florării nu se arată: câmpul e gol. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 404 | — | Nu există, e șters sau e doar furnizor. Corp gol. |\n| 400 | — | `id` nu e GUID (formatul de legare). |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |"
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/customers/:id",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "customers",
                    ":id"
                  ],
                  "variable": [
                    {
                      "key": "id",
                      "value": "",
                      "description": "posfixId-ul clientului"
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"posfixId\": \"b4e7c1a2-9d3f-4e61-8a0b-2f5c7d9e1a34\",\n  \"code\": \"0000000318\",\n  \"name\": \"SRL Floarea Soarelui\",\n  \"fiscalCode\": \"1012600034567\",\n  \"email\": \"contabil@floarea.md\",\n  \"phone\": \"+37322123456\",\n  \"address\": \"mun. Chișinău, str. Ismail 45\",\n  \"isActive\": true,\n  \"defaultPriceListId\": null\n}"
            }
          ]
        },
        {
          "name": "Listă clienți",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/customers?limit=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "customers"
              ],
              "query": [
                {
                  "key": "search",
                  "value": "floarea",
                  "description": "Caută în denumire, cod și cod fiscal.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "50",
                  "description": "1–200, implicit 50."
                },
                {
                  "key": "cursor",
                  "value": "",
                  "description": "nextCursor din pagina precedentă.",
                  "disabled": true
                }
              ]
            },
            "description": "**Scope necesar:** `customers:read`\n\nClienții comerciantului, pagină cu pagină. Include clienții inactivi și partenerul colectiv „Comenzi online”.\n\n### Parametri de interogare\n\n| Parametru | Tip | Implicit | Descriere |\n|---|---|---|---|\n| `search` | string | — | Cuvinte din denumire, cod sau cod fiscal, în orice ordine: fiecare cuvânt trebuie să apară. Fără diferență de majuscule și de diacritice („faina” găsește „Făină”). |\n| `limit` | integer | 50 | Peste 200 se reduce la 200; 0 sau negativ înseamnă 50. |\n| `cursor` | string (uuid) | — | Valoarea `nextCursor` din pagina precedentă. Altceva decât un GUID → `400`. |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `items[]` | array | Clienții, cu câmpurile de la „Client după id”. |\n| `nextCursor` | string? | Cursorul paginii următoare; `null` pe ultima pagină. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `VALIDATION` | Cursor invalid. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Ordinea e după id, nu după nume. Parcurge toate paginile până la `nextCursor: null`; nu te baza pe ordine."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/customers?limit=50",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "customers"
                  ],
                  "query": [
                    {
                      "key": "search",
                      "value": "floarea",
                      "description": "Caută în denumire, cod și cod fiscal.",
                      "disabled": true
                    },
                    {
                      "key": "limit",
                      "value": "50",
                      "description": "1–200, implicit 50."
                    },
                    {
                      "key": "cursor",
                      "value": "",
                      "description": "nextCursor din pagina precedentă.",
                      "disabled": true
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items\": [\n    {\n      \"posfixId\": \"0a1f2b3c-4d5e-4f60-8a71-92b3c4d5e6f7\",\n      \"code\": \"0000000011\",\n      \"name\": \"Comenzi online\",\n      \"fiscalCode\": null,\n      \"email\": null,\n      \"phone\": null,\n      \"address\": null,\n      \"isActive\": true,\n      \"defaultPriceListId\": null\n    },\n    {\n      \"posfixId\": \"b4e7c1a2-9d3f-4e61-8a0b-2f5c7d9e1a34\",\n      \"code\": \"0000000318\",\n      \"name\": \"SRL Floarea Soarelui\",\n      \"fiscalCode\": \"1012600034567\",\n      \"email\": \"contabil@floarea.md\",\n      \"phone\": \"+37322123456\",\n      \"address\": \"mun. Chișinău, str. Ismail 45\",\n      \"isActive\": true,\n      \"defaultPriceListId\": null\n    }\n  ],\n  \"nextCursor\": null\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "4. Comenzi",
      "description": "Comenzile din magazinul tău. Comanda nu face contabilitate: nota o poartă livrarea. Cu cardul de fidelitate al cumpărătorului (`loyalty`), PosFix face singur și fidelitatea comenzii: bonusul folosit, punctele și cashback-ul la încheiere, bonusul întors la anulare.",
      "item": [
        {
          "name": "Creează comandă",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/orders",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "orders"
              ]
            },
            "description": "**Scope necesar:** `orders:write`\n\nComanda din magazinul tău devine comandă de client în PosFix. Cu depozit, se confirmă pe loc și marfa se rezervă; fără depozit, rămâne `Draft`. Comanda e **informativă**: nu emite bon fiscal, factură sau notă contabilă. Acestea apar abia când comerciantul livrează marfa.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `externalOrderId` | string | da | Id-ul comenzii la tine, max 200. E **cheia de idempotență**: aceeași valoare nu creează a doua comandă. |\n| `externalNumber` | string | nu | Numărul comenzii la tine (`#1001`), max 100. Doar copie: numărul PosFix se alocă separat. |\n| `orderDate` | string | nu | `yyyy-MM-dd`. Ziua e cea din Moldova; un instant ISO se ia în ziua lui de acolo. Lipsă **sau ilizibilă** → azi, în ora Moldovei, fără eroare. Anul alege seria de numerotare. |\n| `currency` | string | nu | Doar `MDL` deocamdată (și implicit). Altă monedă e refuzată cu `TRADE_ORDER_CURRENCY_NOT_SUPPORTED`. |\n| `customer.verified` | boolean | nu | Implicit `false`. `true` = magazinul și-a verificat clientul: cont logat, cu telefonul sau IDNO-ul confirmat. Verificat → comanda merge pe clientul existent, cu prețurile lui. Neverificat → comanda nu se lipește de o firmă după IDNO și nu primește prețul clientului; se lipește cel mult de clientul cu amănuntul găsit după telefon, cu prețul public. |\n| `customer.idno` | string | nu | IDNO/IDNP de 13 cifre, max 20 de caractere. Verificat → comanda merge pe firma cu acest cod, creată la nevoie. Neverificat → IDNO-ul nu leagă comanda de firmă; rămâne pe comandă, cu numele, telefonul și emailul, ca cumpărător declarat, pe care comerciantul îl vede și îl verifică. |\n| `customer.phone` | string | nu | Telefonul cumpărătorului, max 50. Un număr din Moldova (`+373 69 123 456`, `069123456`) duce comanda pe clientul cu acest telefon, creat la nevoie: verificat → clientul existent, cu prețurile lui; neverificat → doar clientul cu amănuntul (fără cod fiscal), cu prețul public. Fără telefon din Moldova și fără IDNO verificat, comanda merge pe partenerul colectiv „Comenzi online”. |\n| `customer.name`, `.email`, `.address` | string | nu | Max 500 / 200 / 500. Se folosesc când se creează un partener nou (din IDNO verificat sau din telefon). Pe un cumpărător neverificat, numele și emailul rămân și pe comandă, ca cumpărător declarat. Adresa clientului nu e adresa de livrare: pentru livrare folosește `delivery` sau `deliveryAddress`. |\n| `warehouseId` | string (uuid) | nu | Depozitul din care se rezervă marfa. Lipsă → depozitul implicit al cheii, dacă are unul. Cu depozit, comanda se confirmă după setarea de stoc a cheii (mai jos). |\n| `delivery.method` | string | nu* | `pickup` (ridicare), `own_courier` (curierul comerciantului) sau `nova_post`. *Obligatoriu când trimiți `delivery`. Altă valoare → `400`. |\n| `delivery.recipientName`, `.recipientPhone` | string | nu | Destinatarul, max 200 / 50. La `nova_post` sunt obligatorii, iar telefonul trebuie să fie din Moldova. |\n| `delivery.novaPostDivisionId`, `.novaPostSettlementId`, `.novaPostDivisionLabel` | number, number, string | nu | Punctul Nova Post (id-urile lor, > 0) și numele lui, max 300. Cu punct, adresa de mai jos nu se mai folosește. |\n| `delivery.city`, `.street`, `.building`, `.flat`, `.postCode` | string | nu | Adresa, max 100 / 200 / 20 / 20 / 10. La `nova_post` fără punct sunt obligatorii toate, în afară de `flat`. |\n| `deliveryAddress`, `deliveryContact` | string | nu | Adresa și contactul ca text liber, pentru integrările mai vechi. Cu `delivery`, PosFix compune textul singur din câmpurile ei. |\n| `paymentMethod` | string | nu | `online`, `on_pickup` (la ridicare), `cash_on_delivery` (ramburs) sau `bank_transfer`. Altă valoare → `400`. |\n| `loyalty.cardNumber` | string | nu* | Cardul de fidelitate al cumpărătorului, 13 cifre (îl ai de la „Înscrie un membru”). Comanda se leagă de membru, iar la încheierea ei membrul primește punctele sau cashback-ul pe marfa livrată. Membrul deja legat de un client din PosFix aduce comanda pe acel client, oricare ar fi telefonul trimis (un IDNO verificat câștigă: comanda rămâne pe firmă); fără `customer.phone`, se folosește telefonul membrului. *Obligatoriu când trimiți `loyalty`. Cere scope-ul `loyalty:earn`. |\n| `loyalty.bonusAmount` | number | nu | Bonusul în lei folosit pe comandă: mai mare sau egal cu zero, cel mult două zecimale, cel mult `maxRedeemable` din „Cât bonus se poate folosi” (cu `orderAmount` = totalul comenzii cu TVA). Se scade din prețul liniilor, deci clientul plătește totalul minus bonusul. Cere în plus scope-ul `loyalty:redeem`. |\n| `lines[]` | array | da | Cel puțin o linie. |\n| `lines[].productCode` | string | da | **Codul** produsului din catalog (`code`, 10 cifre), nu SKU-ul. Cod necunoscut → `400`. |\n| `lines[].quantity` | number | da | > 0. |\n| `lines[].unitPrice` | number | nu | Prețul unitar **fără TVA**; TVA-ul se adaugă după cota produsului. Lipsă → prețul de la casă, adus fără TVA. Cum se verifică, depinde de setarea de preț a cheii (mai jos); pe prețul din PosFix e mai simplu să-l omiți. La un client verificat, prețul judecat e prețul lui (lista de pe fișă sau de pe dosar): un magazin care afișează prețul catalogului public poate fi refuzat la acești clienți. |\n| `lines[].discountPct` | number | nu | Reducere pe linie, 0–100, cu cel mult 2 zecimale (altfel `400` `COMMON_PRECISION` pe câmp; nu se rotunjește). Pe cheile cu prețul din PosFix, o reducere diferită de 0 → `400`. |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `orderId` | string (uuid) | Id-ul comenzii în PosFix. Numărul ei îl dă „Comandă după id”. |\n| `idempotentReplay` | boolean | `true` dacă `externalOrderId` exista deja. |\n| `status` | string | `created` sau `replayed`. E rezultatul cererii, nu starea comenzii. |\n| `orderStatus` | string? | Starea comenzii: `Draft` sau `Confirmed` (la repetare, starea de acum). |\n| `paymentDueAt` | string? | Termenul de plată, ISO 8601 UTC, dacă cheia are termen, comanda e confirmată și plata nu e la ridicare sau la livrare; altfel `null`. |\n\n### Setările cheii\n\nLe pune PosFix pe cheie, la cererea comerciantului; nu se trimit în cerere.\n\n| Setare | Ce face |\n|---|---|\n| Prețul liniilor | **Prețul din PosFix** (implicit la cheile noi): prețul îl pune PosFix, deci `unitPrice` se poate omite. Trimis, trece dacă e prețul de la casă fără TVA, rotunjit la ban sau la 4 zecimale, ori dacă `unitPrice × (1 + cota/100)`, rotunjit la ban, e exact prețul de la casă; altfel comanda se refuză (`422`) și primești prețurile curente. Prețul salvat e oricum cel din PosFix: pentru un client verificat, prețul lui (lista de pe fișă sau de pe dosar), altfel prețul public. **Prețul integratorului** (cheile emise înaintea acestor setări): prețul trimis rămâne cum e. În ambele, o linie fără `unitPrice` primește prețul de la casă, adus fără TVA. Pe prețul din PosFix, reducerea pe linie (`discountPct`) se refuză: reducerile vin din listele de prețuri ale comerciantului. |\n| Stoc insuficient | **Refuză comanda** (implicit la cheile noi): comanda se confirmă doar dacă tot stocul e disponibil; altfel `422` și comanda nu rămâne. **Primește comanda** (cheile emise înaintea acestor setări): se confirmă și se rezervă cât se poate. |\n| Depozitul implicit | Depozitul comenzilor care nu trimit `warehouseId`. Fără niciun depozit, comanda rămâne `Draft`, fără rezervare. |\n| Termenul de plată | Oprit sau 1–10 080 de minute de la primire. O comandă confirmată și neplătită deloc la termen se anulează singură (webhook `order.cancelled`); cea plătită parțial rămâne la comerciant. Termenul nu se aplică plății la ridicare sau la livrare (`on_pickup`, `cash_on_delivery`): o comandă care nu se poate plăti înainte nu se anulează pentru neplată. |\n\n### Fidelitatea pe comandă (`loyalty`)\n\nAșa lucrezi când comenzile trec prin PosFix: platforma face singură tot ce ține de fidelitate, tu trimiți doar cardul și bonusul. Comparația cu varianta în care magazinul cheamă direct API-ul de fidelitate e la „8. Fidelitate”.\n\n| Când | Ce face PosFix |\n|---|---|\n| La creare | Verifică cardul, leagă comanda de membru și, cu `bonusAmount`, rezervă bonusul după regulile programului (aceleași ca la casă) și îl scade din prețul liniilor. Bonusul rezervat nu se mai poate cheltui în altă parte, dar nici nu e încă luat de pe card. O rezervare refuzată refuză comanda. |\n| Comanda livrată integral sau închisă | Ia de pe card bonusul de pe marfa livrată, iar restul (de pe marfa nelivrată) îl întoarce pe card. Acordă punctele sau cashback-ul pe valoarea livrată, după reduceri. O singură dată, oricâte evenimente ar veni. |\n| Comanda anulată | Întoarce tot bonusul pe card. Dacă o parte se livrase, decontează ca la închidere: bonusul și acordarea doar pe ce s-a livrat. |\n| Comanda livrată, apoi întoarsă (coletul refuzat, livrarea anulată de comerciant) | Pune la loc bonusul luat și îl rezervă din nou pe comandă; retrage punctele sau cashback-ul. La următorul final decontează pe ce s-a livrat atunci. |\n| Comanda anulată readusă în ciornă | Comanda pierde bonusul (s-a întors pe card la anulare) și revine la prețul întreg; cardul rămâne, deci acordarea se face la încheiere. |\n| Membrul înscris prin API, nelegat de un client | Se leagă de clientul comenzii la prima comandă cu card (nu de partenerul colectiv „Comenzi online”). De atunci, comenzile lui merg pe același client, chiar dacă își schimbă telefonul. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `VALIDATION` | Câmp obligatoriu lipsă, greșit sau prea lung; vezi `errors`. Valoare necunoscută la `delivery.method` sau `paymentMethod`: „Valoarea „card” nu e acceptată. Valori posibile: online, on_pickup, cash_on_delivery, bank_transfer.” |\n| 400 | `INTEGRATIONS_ORDER_PRODUCT_UNKNOWN`, `INTEGRATIONS_ORDER_VAT_UNRESOLVED` | „Produs necunoscut: codul „X” nu există în catalogul comerciantului.” sau „Cota TVA a produsului „X” e nerezolvată, iar comanda nu se înregistrează cu 0%. Verificați cota produsului în catalog.” (format simplu; codul produsului în `params.code`). |\n| 400 | `TRADE_ORDER_DELIVERY_RECIPIENT_REQUIRED`, `TRADE_ORDER_DELIVERY_DESTINATION_REQUIRED`, `TRADE_ORDER_DELIVERY_PHONE_INVALID` | Livrarea Nova Post fără destinatar sau fără destinație, ori telefonul destinatarului nu e din Moldova. |\n| 400 | `INTEGRATIONS_ORDER_DISCOUNT_NOT_ALLOWED` | Prețul din PosFix: o linie are `discountPct`. „Linia cu produsul „X” are reducere, dar cheia folosește prețul de la casă: reducerile vin din listele de prețuri ale comerciantului. Trimiteți linia fără discountPct.” |\n| 409 | `INTEGRATIONS_ORDER_UNRESOLVED` | O cerere mai veche cu aceeași cheie s-a întrerupt înainte ca PosFix să țină cheia pe comandă, deci comanda poate exista deja. Nu retrimite automat: comerciantul verifică; dacă n-o are, trimite comanda cu alt `externalOrderId`. |\n| 422 | `INTEGRATIONS_ORDER_PRICE_CHANGED` | Prețul din PosFix: un preț trimis nu dă prețul de la casă. Comanda nu s-a creat; prețurile curente sunt în `priceMismatches` (`productCode`, `sentUnitPrice`, `currentUnitPrice`, toate fără TVA). |\n| 422 | `INSUFFICIENT_STOCK` | Refuză comanda la stoc insuficient: „Stoc insuficient pentru „X” în „Y”: sunt necesare 5, disponibile 2, lipsesc 3. Înregistrați întâi intrarea mărfii sau micșorați cantitatea.” Comanda nu s-a creat; poți primi totuși webhook-urile `order.created` și `order.cancelled` (cu `reason: stock_refused`) pentru ea — răspunsul API-ului rămâne adevărul. |\n| 400 | `INTEGRATIONS_LOYALTY_CARD_NUMBER_FORMAT`, `INTEGRATIONS_LOYALTY_AMOUNT_INVALID` | Cardul nu are 13 cifre sau bonusul nu e o sumă în lei (negativ, peste două zecimale). |\n| 400 | `TRADE_ORDER_LOYALTY_CARD_UNKNOWN` | „Cardul de fidelitate 2000000012345 nu e al acestei firme. Verificați numărul sau înscrieți clientul în program.” Comanda nu s-a creat. |\n| 400 | `TRADE_ORDER_LOYALTY_BONUS_EXCEEDS_TOTAL` | „Bonusul de 300,00 lei e mai mare decât comanda, de 243,38 lei. Trimiteți un bonus cel mult cât comanda.” |\n| 403 | — | Comanda are `loyalty`, dar cheii îi lipsește `loyalty:earn` (sau `loyalty:redeem`, cu bonus), ori comerciantul nu are activ modulul „Programe de fidelitate”. Corp gol. |\n| 422 | `LOYALTY_BONUS_HOLD_EXCEEDS_MAX`, `LOYALTY_QUOTE_NOTHING_AVAILABLE` și celelalte motive ale cotării | Bonusul cerut nu se poate folosi pe comandă (peste plafonul programului, sold insuficient, card fără portofel). „Pe bonul acesta se pot folosi cel mult 120,50 MDL.” Comanda nu s-a creat: retrimite cu același `externalOrderId` și un bonus mai mic, sau fără bonus. |\n| 422 | `TRADE_ORDER_PARTNER_INACTIVE` | Partenerul pe care ar merge comanda e dezactivat în PosFix: de regulă firma găsită după IDNO-ul unui client verificat (`customer.verified: true`). Comanda nu s-a creat; comerciantul reactivează partenerul, apoi retrimiți cu același `externalOrderId`. Pe telefon nu apare: partenerul dezactivat se sare, iar comanda merge pe un client cu amănuntul nou. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Răspunsul e `200`, nu `201`, și la creare, și la repetare.\n- `replayed` poate veni chiar la prima ta cerere: dacă o încercare internă a ajuns la PosFix, dar răspunsul ei s-a pierdut, reîncercarea primește comanda deja creată. Tratează `created` și `replayed` la fel: comanda există, iar `orderId` e al ei.\n- La o repetare corpul **nu se compară**: primești comanda existentă chiar dacă ai schimbat liniile. O comandă nouă cere `externalOrderId` nou.\n- La repetare, `orderStatus` și `paymentDueAt` sunt cele de acum ale comenzii. Dacă între timp comerciantul a șters comanda, aceeași cerere creează una nouă.\n- Repetarea e sigură: dacă o cerere a căzut pe drum (timeout, `503`), retrimite-o cu același `externalOrderId` și nu se creează a doua comandă. Excepția sunt cheile rămase nelămurite de la o cerere întreruptă înainte ca PosFix să țină cheia pe comandă: ele primesc `409` `INTEGRATIONS_ORDER_UNRESOLVED`, iar comerciantul verifică întâi dacă are deja comanda; dacă n-o are, comanda se trimite cu alt `externalOrderId`.\n- După un refuz de preț sau de stoc comanda nu există: retrimite cu același `externalOrderId`, cu prețurile din `priceMismatches` sau după ce se completează stocul.\n- Prețul fără TVA din prețul de la casă: `price / (1 + vatRate/100)` (`7,90 / 1,08 = 7,3148`). Pe prețul din PosFix trece la 4 zecimale (`7,3148`), la ban (`7,31`) sau orice preț care, cu TVA și rotunjit la ban, dă prețul de la casă. Rotunjirea la ban nu se acceptă când prețul fără TVA la ban ar fi 0.\n- Linia: net = cantitate × preț × (1 − reducere/100); TVA = net × cota produsului; total = net + TVA. Netul și TVA-ul se rotunjesc la 2 zecimale, la depărtare de zero (2,345 → 2,35).\n- Comanda nouă declanșează webhook-ul `order.created`, iar confirmarea `order.confirmed`.\n- Cu `loyalty`, nu mai chema pentru aceeași comandă „Folosește bonus pe o comandă”, „Acordă pe o comandă” sau „Anulează o comandă (retur)” din API-ul de fidelitate: le face PosFix, iar a doua oară membrul ar primi de două ori.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"externalOrderId\": \"shop-100245\",\n  \"orderDate\": \"{{today}}\",\n  \"currency\": \"MDL\",\n  \"customer\": {\n    \"name\": \"Ana Rusu\",\n    \"email\": \"ana.rusu@example.md\",\n    \"phone\": \"+373 69 123 456\"\n  },\n  \"delivery\": {\n    \"method\": \"own_courier\",\n    \"recipientName\": \"Ana Rusu\",\n    \"recipientPhone\": \"+373 69 123 456\",\n    \"city\": \"mun. Chișinău\",\n    \"street\": \"bd. Dacia\",\n    \"building\": \"27\",\n    \"flat\": \"4\",\n    \"postCode\": \"MD-2005\"\n  },\n  \"paymentMethod\": \"online\",\n  \"lines\": [\n    {\n      \"productCode\": \"0000000123\",\n      \"quantity\": 12,\n      \"unitPrice\": 15.42\n    },\n    {\n      \"productCode\": \"0000000124\",\n      \"quantity\": 3,\n      \"unitPrice\": 7.3148,\n      \"discountPct\": 10\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 — comandă creată și confirmată",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/orders",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "orders"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"externalOrderId\": \"shop-100245\",\n  \"orderDate\": \"{{today}}\",\n  \"currency\": \"MDL\",\n  \"customer\": {\n    \"name\": \"Ana Rusu\",\n    \"email\": \"ana.rusu@example.md\",\n    \"phone\": \"+373 69 123 456\"\n  },\n  \"delivery\": {\n    \"method\": \"own_courier\",\n    \"recipientName\": \"Ana Rusu\",\n    \"recipientPhone\": \"+373 69 123 456\",\n    \"city\": \"mun. Chișinău\",\n    \"street\": \"bd. Dacia\",\n    \"building\": \"27\",\n    \"flat\": \"4\",\n    \"postCode\": \"MD-2005\"\n  },\n  \"paymentMethod\": \"online\",\n  \"lines\": [\n    {\n      \"productCode\": \"0000000123\",\n      \"quantity\": 12,\n      \"unitPrice\": 15.42\n    },\n    {\n      \"productCode\": \"0000000124\",\n      \"quantity\": 3,\n      \"unitPrice\": 7.3148,\n      \"discountPct\": 10\n    }\n  ]\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"orderId\": \"3c9a1f52-7d3e-4b8a-a0c4-6e2b91f0d7a8\",\n  \"idempotentReplay\": false,\n  \"status\": \"created\",\n  \"orderStatus\": \"Confirmed\",\n  \"paymentDueAt\": \"2026-10-16T10:15:00Z\"\n}"
            },
            {
              "name": "400 — card de fidelitate străin",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/orders",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "orders"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"externalOrderId\": \"shop-100245\",\n  \"orderDate\": \"{{today}}\",\n  \"currency\": \"MDL\",\n  \"customer\": {\n    \"name\": \"Ana Rusu\",\n    \"email\": \"ana.rusu@example.md\",\n    \"phone\": \"+373 69 123 456\"\n  },\n  \"delivery\": {\n    \"method\": \"own_courier\",\n    \"recipientName\": \"Ana Rusu\",\n    \"recipientPhone\": \"+373 69 123 456\",\n    \"city\": \"mun. Chișinău\",\n    \"street\": \"bd. Dacia\",\n    \"building\": \"27\",\n    \"flat\": \"4\",\n    \"postCode\": \"MD-2005\"\n  },\n  \"paymentMethod\": \"online\",\n  \"lines\": [\n    {\n      \"productCode\": \"0000000123\",\n      \"quantity\": 12,\n      \"unitPrice\": 15.42\n    },\n    {\n      \"productCode\": \"0000000124\",\n      \"quantity\": 3,\n      \"unitPrice\": 7.3148,\n      \"discountPct\": 10\n    }\n  ]\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"type\": \"https://httpstatuses.io/400\",\n  \"title\": \"TRADE_ORDER_LOYALTY_CARD_UNKNOWN\",\n  \"status\": 400,\n  \"detail\": \"Cardul de fidelitate 2000000012345 nu e al acestei firme. Verificați numărul sau înscrieți clientul în program.\",\n  \"errorCode\": \"TRADE_ORDER_LOYALTY_CARD_UNKNOWN\"\n}"
            },
            {
              "name": "422 — bonus peste plafonul programului",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/orders",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "orders"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"externalOrderId\": \"shop-100245\",\n  \"orderDate\": \"{{today}}\",\n  \"currency\": \"MDL\",\n  \"customer\": {\n    \"name\": \"Ana Rusu\",\n    \"email\": \"ana.rusu@example.md\",\n    \"phone\": \"+373 69 123 456\"\n  },\n  \"delivery\": {\n    \"method\": \"own_courier\",\n    \"recipientName\": \"Ana Rusu\",\n    \"recipientPhone\": \"+373 69 123 456\",\n    \"city\": \"mun. Chișinău\",\n    \"street\": \"bd. Dacia\",\n    \"building\": \"27\",\n    \"flat\": \"4\",\n    \"postCode\": \"MD-2005\"\n  },\n  \"paymentMethod\": \"online\",\n  \"lines\": [\n    {\n      \"productCode\": \"0000000123\",\n      \"quantity\": 12,\n      \"unitPrice\": 15.42\n    },\n    {\n      \"productCode\": \"0000000124\",\n      \"quantity\": 3,\n      \"unitPrice\": 7.3148,\n      \"discountPct\": 10\n    }\n  ]\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"type\": \"https://httpstatuses.io/422\",\n  \"title\": \"LOYALTY_BONUS_HOLD_EXCEEDS_MAX\",\n  \"status\": 422,\n  \"detail\": \"Pe bonul acesta se pot folosi cel mult 120,50 MDL.\",\n  \"errorCode\": \"LOYALTY_BONUS_HOLD_EXCEEDS_MAX\"\n}"
            },
            {
              "name": "200 — repetare cu același externalOrderId",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/orders",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "orders"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"externalOrderId\": \"shop-100245\",\n  \"orderDate\": \"{{today}}\",\n  \"currency\": \"MDL\",\n  \"customer\": {\n    \"name\": \"Ana Rusu\",\n    \"email\": \"ana.rusu@example.md\",\n    \"phone\": \"+373 69 123 456\"\n  },\n  \"delivery\": {\n    \"method\": \"own_courier\",\n    \"recipientName\": \"Ana Rusu\",\n    \"recipientPhone\": \"+373 69 123 456\",\n    \"city\": \"mun. Chișinău\",\n    \"street\": \"bd. Dacia\",\n    \"building\": \"27\",\n    \"flat\": \"4\",\n    \"postCode\": \"MD-2005\"\n  },\n  \"paymentMethod\": \"online\",\n  \"lines\": [\n    {\n      \"productCode\": \"0000000123\",\n      \"quantity\": 12,\n      \"unitPrice\": 15.42\n    },\n    {\n      \"productCode\": \"0000000124\",\n      \"quantity\": 3,\n      \"unitPrice\": 7.3148,\n      \"discountPct\": 10\n    }\n  ]\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"orderId\": \"3c9a1f52-7d3e-4b8a-a0c4-6e2b91f0d7a8\",\n  \"idempotentReplay\": true,\n  \"status\": \"replayed\",\n  \"orderStatus\": \"Confirmed\",\n  \"paymentDueAt\": \"2026-10-16T10:15:00Z\"\n}"
            },
            {
              "name": "422 — prețul diferă de prețul de la casă",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/orders",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "orders"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"externalOrderId\": \"shop-100245\",\n  \"orderDate\": \"{{today}}\",\n  \"currency\": \"MDL\",\n  \"customer\": {\n    \"name\": \"Ana Rusu\",\n    \"email\": \"ana.rusu@example.md\",\n    \"phone\": \"+373 69 123 456\"\n  },\n  \"delivery\": {\n    \"method\": \"own_courier\",\n    \"recipientName\": \"Ana Rusu\",\n    \"recipientPhone\": \"+373 69 123 456\",\n    \"city\": \"mun. Chișinău\",\n    \"street\": \"bd. Dacia\",\n    \"building\": \"27\",\n    \"flat\": \"4\",\n    \"postCode\": \"MD-2005\"\n  },\n  \"paymentMethod\": \"online\",\n  \"lines\": [\n    {\n      \"productCode\": \"0000000123\",\n      \"quantity\": 12,\n      \"unitPrice\": 15.42\n    },\n    {\n      \"productCode\": \"0000000124\",\n      \"quantity\": 3,\n      \"unitPrice\": 7.3148,\n      \"discountPct\": 10\n    }\n  ]\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                }
              ],
              "body": "{\n  \"type\": \"https://httpstatuses.io/422\",\n  \"title\": \"INTEGRATIONS_ORDER_PRICE_CHANGED\",\n  \"status\": 422,\n  \"detail\": \"Prețul unor linii diferă de prețul de la casă, așa că comanda nu s-a înregistrat. Prețurile curente, fără TVA, sunt în priceMismatches: retrimiteți comanda cu ele.\",\n  \"errorCode\": \"INTEGRATIONS_ORDER_PRICE_CHANGED\",\n  \"priceMismatches\": [\n    {\n      \"productCode\": \"0000000124\",\n      \"sentUnitPrice\": 6.9444,\n      \"currentUnitPrice\": 7.3148\n    }\n  ]\n}"
            },
            {
              "name": "422 — stoc insuficient",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/orders",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "orders"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"externalOrderId\": \"shop-100245\",\n  \"orderDate\": \"{{today}}\",\n  \"currency\": \"MDL\",\n  \"customer\": {\n    \"name\": \"Ana Rusu\",\n    \"email\": \"ana.rusu@example.md\",\n    \"phone\": \"+373 69 123 456\"\n  },\n  \"delivery\": {\n    \"method\": \"own_courier\",\n    \"recipientName\": \"Ana Rusu\",\n    \"recipientPhone\": \"+373 69 123 456\",\n    \"city\": \"mun. Chișinău\",\n    \"street\": \"bd. Dacia\",\n    \"building\": \"27\",\n    \"flat\": \"4\",\n    \"postCode\": \"MD-2005\"\n  },\n  \"paymentMethod\": \"online\",\n  \"lines\": [\n    {\n      \"productCode\": \"0000000123\",\n      \"quantity\": 12,\n      \"unitPrice\": 15.42\n    },\n    {\n      \"productCode\": \"0000000124\",\n      \"quantity\": 3,\n      \"unitPrice\": 7.3148,\n      \"discountPct\": 10\n    }\n  ]\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                }
              ],
              "body": "{\n  \"type\": \"https://httpstatuses.io/422\",\n  \"title\": \"INSUFFICIENT_STOCK\",\n  \"status\": 422,\n  \"detail\": \"Stoc insuficient pentru „Apă minerală Borjomi 0,5 L” în „Depozit central”: sunt necesare 12, disponibile 5, lipsesc 7. Înregistrați întâi intrarea mărfii sau micșorați cantitatea.\",\n  \"errorCode\": \"INSUFFICIENT_STOCK\",\n  \"params\": {\n    \"product\": \"Apă minerală Borjomi 0,5 L\",\n    \"warehouse\": \"Depozit central\",\n    \"requested\": \"12\",\n    \"available\": \"5\",\n    \"missing\": \"7\"\n  }\n}"
            },
            {
              "name": "400 — produs necunoscut",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/orders",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "orders"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"externalOrderId\": \"shop-100245\",\n  \"orderDate\": \"{{today}}\",\n  \"currency\": \"MDL\",\n  \"customer\": {\n    \"name\": \"Ana Rusu\",\n    \"email\": \"ana.rusu@example.md\",\n    \"phone\": \"+373 69 123 456\"\n  },\n  \"delivery\": {\n    \"method\": \"own_courier\",\n    \"recipientName\": \"Ana Rusu\",\n    \"recipientPhone\": \"+373 69 123 456\",\n    \"city\": \"mun. Chișinău\",\n    \"street\": \"bd. Dacia\",\n    \"building\": \"27\",\n    \"flat\": \"4\",\n    \"postCode\": \"MD-2005\"\n  },\n  \"paymentMethod\": \"online\",\n  \"lines\": [\n    {\n      \"productCode\": \"0000000123\",\n      \"quantity\": 12,\n      \"unitPrice\": 15.42\n    },\n    {\n      \"productCode\": \"0000000124\",\n      \"quantity\": 3,\n      \"unitPrice\": 7.3148,\n      \"discountPct\": 10\n    }\n  ]\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"type\": \"https://httpstatuses.io/400\",\n  \"title\": \"Validation Failed\",\n  \"status\": 400,\n  \"detail\": \"Produs necunoscut: codul „0000000999” nu există în catalogul comerciantului.\",\n  \"errorCode\": \"INTEGRATIONS_ORDER_PRODUCT_UNKNOWN\",\n  \"params\": {\n    \"code\": \"0000000999\"\n  }\n}"
            },
            {
              "name": "400 — validare",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/orders",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "orders"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"externalOrderId\": \"shop-100245\",\n  \"orderDate\": \"{{today}}\",\n  \"currency\": \"MDL\",\n  \"customer\": {\n    \"name\": \"Ana Rusu\",\n    \"email\": \"ana.rusu@example.md\",\n    \"phone\": \"+373 69 123 456\"\n  },\n  \"delivery\": {\n    \"method\": \"own_courier\",\n    \"recipientName\": \"Ana Rusu\",\n    \"recipientPhone\": \"+373 69 123 456\",\n    \"city\": \"mun. Chișinău\",\n    \"street\": \"bd. Dacia\",\n    \"building\": \"27\",\n    \"flat\": \"4\",\n    \"postCode\": \"MD-2005\"\n  },\n  \"paymentMethod\": \"online\",\n  \"lines\": [\n    {\n      \"productCode\": \"0000000123\",\n      \"quantity\": 12,\n      \"unitPrice\": 15.42\n    },\n    {\n      \"productCode\": \"0000000124\",\n      \"quantity\": 3,\n      \"unitPrice\": 7.3148,\n      \"discountPct\": 10\n    }\n  ]\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                }
              ],
              "body": "{\n  \"type\": \"https://httpstatuses.io/400\",\n  \"title\": \"VALIDATION\",\n  \"status\": 400,\n  \"detail\": \"Câmpul este obligatoriu.\",\n  \"errorCode\": \"VALIDATION\",\n  \"errors\": {\n    \"externalOrderId\": [\n      \"Câmpul este obligatoriu.\"\n    ]\n  },\n  \"errorCodes\": {\n    \"externalOrderId\": [\n      {\n        \"code\": \"COMMON_REQUIRED\",\n        \"params\": {}\n      }\n    ]\n  }\n}"
            }
          ]
        },
        {
          "name": "Comandă după id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/orders/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "orders",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "orderId din „Creează comandă”"
                }
              ]
            },
            "description": "**Scope necesar:** `orders:read`\n\nO comandă de client, cu liniile, plățile și documentele de vânzare generate din ea. Întoarce orice comandă a comerciantului, inclusiv cele create în panou sau la casă.\n\n### Parametri de cale\n\n| Parametru | Tip | Descriere |\n|---|---|---|\n| `id` | string (uuid) | Id-ul comenzii. |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `id`, `number` | string | Id-ul și numărul comenzii (10 cifre). |\n| `status` | string | Starea; vezi tabelul de mai jos. |\n| `partnerId` | string (uuid) | Clientul. Poate fi partenerul colectiv „Comenzi online”. |\n| `orderDate` | string | `yyyy-MM-dd`. |\n| `expectedShipDate` | string? | Data de livrare planificată, pusă de comerciant. |\n| `currencyCode` | string | Moneda comenzii. |\n| `subTotal`, `vatTotal`, `total` | number | Fără TVA, TVA, cu TVA — în moneda comenzii. |\n| `reservedQtyTotal`, `shippedQtyTotal` | number | Cantități rezervate și livrate, pe toată comanda. |\n| `invoicedSum` | number | Suma vânzărilor contabilizate din comandă, în lei. |\n| `paidSum` | number | Cât s-a plătit (vânzări, avansuri, factura de plată), în lei. |\n| `deliveryAddress`, `deliveryContact`, `trackingNumber` | string? | Livrarea. |\n| `paymentDueAt` | string? | Termenul de plată, ISO 8601 UTC; `null` = fără termen. La termen, comanda confirmată și neplătită deloc se anulează. |\n| `paymentMethod` | string? | `online`, `on_pickup`, `cash_on_delivery` sau `bank_transfer`, ca la creare; `null` = nespusă. |\n| `loyalty` | object? | `{cardNumber, bonusAmount}`: cardul de fidelitate al comenzii și bonusul în lei scăzut acum din prețul liniilor (0 = doar cardul). `null` = comandă fără card. |\n| `cancellationReason` | string? | De ce s-a anulat fără comerciant: `payment_deadline` (neplătită deloc la termen) sau `external_cancelled` (ai anulat-o tu, prin „Anulează comanda”); `null` = anulată de om sau neanulată. Comenzile refuzate la creare (stoc, confirmare căzută) se șterg, deci nu apar aici; le vezi doar în webhook-ul `order.cancelled`. |\n| `description`, `notes` | string? | Completate de comerciant în panou. |\n| `lines[]` | array | `lineNumber`, `productId` (`posfixId` din catalog), `productName`, `unitOfMeasure`, `quantity`, `reservedQty`, `shippedQty`, `unitPrice` (fără TVA), `discountPct`, `vatRatePct`, `lineTotal` (cu TVA). |\n| `generatedSales[]` | array | Documentele de vânzare făcute din comandă: `id`, `number`, `status` (`Draft`/`Posted`), `date`, `total`. |\n\n### Stările comenzii\n\n| Stare | Ce înseamnă |\n|---|---|\n| `Draft` | Primită, neconfirmată, fără rezervare. Așa rămâne comanda din API fără depozit sau a cărei confirmare n-a reușit. |\n| `Confirmed` | Confirmată; marfa se rezervă. Comanda din API cu depozit (al ei sau cel implicit al cheii) se confirmă la creare. |\n| `Fulfilling` | Livrată parțial. |\n| `Fulfilled` | Livrată integral: prin factură, lot de curier sau ridicare de la casă. |\n| `Closed` | Închisă manual; restul rezervării se eliberează. |\n| `Cancelled` | Anulată de comerciant sau, la termenul de plată al cheii, automat. Comerciantul o poate redeschide. |\n\nStarea o schimbă comerciantul și fluxurile de livrare; API-ul n-o poate schimba.\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 404 | — | Comanda nu există sau e ștearsă. Corp gol. |\n| 400 | — | `id` nu e GUID. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- La ridicare de la casă sau prin curier, comanda poate ajunge `Fulfilled` fără nimic în `generatedSales`: nota contabilă o poartă raportul Z sau lotul de livrare."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/orders/:id",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "orders",
                    ":id"
                  ],
                  "variable": [
                    {
                      "key": "id",
                      "value": "",
                      "description": "orderId din „Creează comandă”"
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"id\": \"3c9a1f52-7d3e-4b8a-a0c4-6e2b91f0d7a8\",\n  \"number\": \"0000000042\",\n  \"status\": \"Draft\",\n  \"partnerId\": \"0a1f2b3c-4d5e-4f60-8a71-92b3c4d5e6f7\",\n  \"orderDate\": \"2026-09-25\",\n  \"expectedShipDate\": null,\n  \"currencyCode\": \"MDL\",\n  \"subTotal\": 204.79,\n  \"vatTotal\": 38.59,\n  \"total\": 243.38,\n  \"reservedQtyTotal\": 0,\n  \"shippedQtyTotal\": 0,\n  \"invoicedSum\": 0,\n  \"paidSum\": 0,\n  \"description\": null,\n  \"notes\": null,\n  \"deliveryAddress\": \"mun. Chișinău, bd. Dacia 27, ap. 4\",\n  \"deliveryContact\": \"Ana Rusu, +373 69 123 456\",\n  \"trackingNumber\": null,\n  \"paymentDueAt\": null,\n  \"paymentMethod\": \"cash_on_delivery\",\n  \"cancellationReason\": null,\n  \"lines\": [\n    {\n      \"lineNumber\": 1,\n      \"productId\": \"0b8f5e2a-6c1d-4e7f-9a3b-2d4c6e8f1a01\",\n      \"productName\": \"Apă minerală Borjomi 0,5 L\",\n      \"unitOfMeasure\": \"buc\",\n      \"quantity\": 12,\n      \"reservedQty\": 0,\n      \"shippedQty\": 0,\n      \"unitPrice\": 15.42,\n      \"discountPct\": 0,\n      \"vatRatePct\": 20,\n      \"lineTotal\": 222.05\n    },\n    {\n      \"lineNumber\": 2,\n      \"productId\": \"1c9a6f3b-7d2e-4f80-8b4c-3e5d7f9a2b02\",\n      \"productName\": \"Pâine Franzeluța albă feliată 400 g\",\n      \"unitOfMeasure\": \"buc\",\n      \"quantity\": 3,\n      \"reservedQty\": 0,\n      \"shippedQty\": 0,\n      \"unitPrice\": 7.3148,\n      \"discountPct\": 10,\n      \"vatRatePct\": 8,\n      \"lineTotal\": 21.33\n    }\n  ],\n  \"generatedSales\": []\n}"
            }
          ]
        },
        {
          "name": "Listă comenzi",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/orders?from={{monthStart}}&to={{today}}&page=1&pageSize=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "orders"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "Draft",
                  "description": "Draft · Confirmed · Fulfilling · Fulfilled · Closed · Cancelled",
                  "disabled": true
                },
                {
                  "key": "from",
                  "value": "{{monthStart}}",
                  "description": "yyyy-MM-dd, inclusiv."
                },
                {
                  "key": "to",
                  "value": "{{today}}",
                  "description": "yyyy-MM-dd, inclusiv."
                },
                {
                  "key": "page",
                  "value": "1",
                  "description": "De la 1."
                },
                {
                  "key": "pageSize",
                  "value": "20",
                  "description": "1–200, implicit 20."
                }
              ]
            },
            "description": "**Scope necesar:** `orders:read`\n\nComenzile comerciantului, cele mai noi întâi (după dată, apoi după număr).\n\n### Parametri de interogare\n\n| Parametru | Tip | Implicit | Descriere |\n|---|---|---|---|\n| `status` | string | toate | O stare din tabelul de la „Comandă după id”. Altă valoare → `400`. |\n| `from` / `to` | string | — | Data comenzii, inclusiv. Trimite `yyyy-MM-dd`: serverul acceptă și alte forme și le poate citi greșit (`01.09.2026` devine 9 ianuarie). Un text care nu e dată → `400`. |\n| `page` | integer | 1 | Numărul paginii, de la 1. |\n| `pageSize` | integer | 20 | 1–200. **Peste 200 revine la 20**, nu la 200. |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `items[]` | array | `id`, `number`, `status`, `orderDate`, `expectedShipDate`, `total`, `currencyCode`, `paidSum` (lei), `shippedQtyTotal`, `deliveryAddress`, `deliveryContact`, `trackingNumber`, `paymentDueAt`, `paymentMethod`, `cancellationReason`. |\n| `total` | integer | Câte comenzi corespund filtrului, pe toate paginile. |\n| `page`, `pageSize` | integer | Valorile aplicate. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `VALIDATION` | Stare sau dată invalidă. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |"
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/orders?from={{monthStart}}&to={{today}}&page=1&pageSize=20",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "orders"
                  ],
                  "query": [
                    {
                      "key": "status",
                      "value": "Draft",
                      "description": "Draft · Confirmed · Fulfilling · Fulfilled · Closed · Cancelled",
                      "disabled": true
                    },
                    {
                      "key": "from",
                      "value": "{{monthStart}}",
                      "description": "yyyy-MM-dd, inclusiv."
                    },
                    {
                      "key": "to",
                      "value": "{{today}}",
                      "description": "yyyy-MM-dd, inclusiv."
                    },
                    {
                      "key": "page",
                      "value": "1",
                      "description": "De la 1."
                    },
                    {
                      "key": "pageSize",
                      "value": "20",
                      "description": "1–200, implicit 20."
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items\": [\n    {\n      \"id\": \"3c9a1f52-7d3e-4b8a-a0c4-6e2b91f0d7a8\",\n      \"number\": \"0000000042\",\n      \"status\": \"Draft\",\n      \"orderDate\": \"2026-09-25\",\n      \"expectedShipDate\": null,\n      \"total\": 243.38,\n      \"currencyCode\": \"MDL\",\n      \"paidSum\": 0,\n      \"shippedQtyTotal\": 0,\n      \"deliveryAddress\": \"mun. Chișinău, bd. Dacia 27, ap. 4\",\n      \"deliveryContact\": \"Ana Rusu, +373 69 123 456\",\n      \"trackingNumber\": null,\n      \"paymentDueAt\": null,\n      \"paymentMethod\": \"cash_on_delivery\",\n      \"cancellationReason\": null\n    }\n  ],\n  \"total\": 1,\n  \"page\": 1,\n  \"pageSize\": 20\n}"
            }
          ]
        },
        {
          "name": "Actualizează livrarea",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/orders/:id/delivery",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "orders",
                ":id",
                "delivery"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "orderId"
                }
              ]
            },
            "description": "**Scope necesar:** `orders:write`\n\nPune numărul de urmărire și, la nevoie, adresa sau contactul de livrare, după ce ai expediat.\n\n### Parametri de cale\n\n| Parametru | Tip | Descriere |\n|---|---|---|\n| `id` | string (uuid) | Id-ul comenzii. |\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `trackingNumber` | string | nu | Numărul de urmărire al coletului. |\n| `deliveryAddress` | string | nu | Adresa de livrare. |\n| `deliveryContact` | string | nu | Persoana de contact. |\n\n### Răspuns\n\n**204** fără corp.\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 404 | — | Comanda nu există sau e ștearsă. Corp gol. |\n| 400 | — | `id` nu e GUID. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Se scriu doar câmpurile trimise și nevide. Un câmp **nu se poate goli** prin API.\n- Merge în orice stare a comenzii și nu trimite webhook.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"trackingNumber\": \"RB123456789MD\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "204 — actualizat",
              "originalRequest": {
                "method": "PATCH",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/orders/:id/delivery",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "orders",
                    ":id",
                    "delivery"
                  ],
                  "variable": [
                    {
                      "key": "id",
                      "value": "",
                      "description": "orderId"
                    }
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"trackingNumber\": \"RB123456789MD\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "No Content",
              "code": 204,
              "_postman_previewlanguage": "text",
              "header": [],
              "body": ""
            }
          ]
        },
        {
          "name": "Anulează comanda",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/orders/:id/cancel",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "orders",
                ":id",
                "cancel"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "orderId din „Creează comandă”"
                }
              ]
            },
            "description": "**Scope necesar:** `orders:write`\n\nAnulează o comandă trimisă prin API, cât marfa n-a plecat: e `Draft` sau `Confirmed` și nimic n-a fost livrat sau ridicat. E aceeași anulare ca butonul din panoul comerciantului: rezervarea de stoc se eliberează, loturile de curier încă nepredate se retrag, iar bonusul de fidelitate rezervat pe comandă se întoarce pe card. Folosește-o când clientul renunță sau plata online a căzut.\n\n### Parametri de cale\n\n| Parametru | Tip | Descriere |\n|---|---|---|\n| `id` | string (uuid) | Id-ul comenzii. |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `id` | string (uuid) | Comanda. |\n| `status` | string | `Cancelled`. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 404 | — | Comanda nu există, e ștearsă sau n-a venit prin API (a făcut-o comerciantul în panou sau la casă). Corp gol. |\n| 400 | — | `id` nu e GUID. |\n| 422 | `TRADE_ORDER_API_CANCEL_GOODS_LEFT` | „Comanda 0000000042 nu se mai poate anula din API: marfa a început să plece (o parte e livrată, ridicată sau comanda e închisă). Returul îl face firma.” |\n| 422 | `TRADE_ORDER_CANCEL_HANDED_TO_CARRIER` | Coletul e deja predat curierului; returul îl cere comerciantul. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Repetarea pe o comandă deja anulată răspunde `200` cu `status: Cancelled`, fără eroare: o poți retrimite după un timeout.\n- Anularea declanșează webhook-ul `order.cancelled`, cu `reason: external_cancelled`.\n- Comerciantul poate readuce comanda în ciornă din panou; atunci ea revine fără bonus (bonusul s-a întors pe card la anulare).\n- O comandă livrată, chiar parțial, nu se mai anulează din API: returul îl face comerciantul."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/orders/:id/cancel",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "orders",
                    ":id",
                    "cancel"
                  ],
                  "variable": [
                    {
                      "key": "id",
                      "value": "",
                      "description": "orderId din „Creează comandă”"
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"id\": \"3c9a1f52-7d3e-4b8a-a0c4-6e2b91f0d7a8\",\n  \"status\": \"Cancelled\"\n}"
            },
            {
              "name": "422 — marfa a plecat",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/orders/:id/cancel",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "orders",
                    ":id",
                    "cancel"
                  ],
                  "variable": [
                    {
                      "key": "id",
                      "value": "",
                      "description": "orderId din „Creează comandă”"
                    }
                  ]
                }
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"type\": \"https://httpstatuses.io/422\",\n  \"title\": \"TRADE_ORDER_API_CANCEL_GOODS_LEFT\",\n  \"status\": 422,\n  \"detail\": \"Comanda 0000000042 nu se mai poate anula din API: marfa a început să plece (o parte e livrată, ridicată sau comanda e închisă). Returul îl face firma.\",\n  \"errorCode\": \"TRADE_ORDER_API_CANCEL_GOODS_LEFT\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "5. Vânzări",
      "description": "Vânzările comerciantului: rapoartele Z de la casă, zi cu zi, și toate vânzările pe zile și canale.",
      "item": [
        {
          "name": "Case de marcat",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/terminals",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "terminals"
              ]
            },
            "description": "**Scope necesar:** `sales:read`\n\nCasele de marcat ale comerciantului. Leagă-ți casele după `id` — e stabil. Cheia limitată la anumite case le vede doar pe acelea.\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `items[].id` | string (uuid) | Id-ul casei. |\n| `items[].name` | string | Denumirea; poate fi goală. |\n| `items[].code` | string | Codul casei (`0000-00124-QWERT`); poate fi gol. |\n| `items[].model` | string? | Modelul. |\n| `items[].factoryNumber` | string? | Numărul de fabrică. |\n| `items[].fiscalRegistrationNumber` | string? | Numărul de înregistrare la SFS. |\n| `items[].address` | string? | Adresa de instalare. |\n| `items[].warehouseId` | string (uuid)? | Depozitul casei. |\n| `items[].isFiscalRegistered` | boolean | Casa e înregistrată la SFS. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Lista include și casele dezactivate (nu și pe cele șterse). Nu are paginare."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/terminals",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "terminals"
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items\": [\n    {\n      \"id\": \"5b2d6f3e-8c1a-4f7e-9a2b-3c4d5e6f7a81\",\n      \"name\": \"Casa 1 — Magazin Centru\",\n      \"code\": \"0000-00124-QWERT\",\n      \"model\": \"POSfix\",\n      \"factoryNumber\": \"000000124QWERT\",\n      \"fiscalRegistrationNumber\": \"J403001234\",\n      \"address\": \"mun. Chișinău, str. București 45\",\n      \"warehouseId\": \"0d9e2a8b-1f3c-4b6e-8a7d-2c5f9e1b3a46\",\n      \"isFiscalRegistered\": true\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Rapoarte Z",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/z-reports?from={{monthStart}}&to={{today}}&page=1&pageSize=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "z-reports"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "{{monthStart}}",
                  "description": "yyyy-MM-dd, obligatoriu."
                },
                {
                  "key": "to",
                  "value": "{{today}}",
                  "description": "yyyy-MM-dd, obligatoriu, ≥ from."
                },
                {
                  "key": "terminalId",
                  "value": "",
                  "description": "Id de casă; repetabil.",
                  "disabled": true
                },
                {
                  "key": "page",
                  "value": "1",
                  "description": "De la 1."
                },
                {
                  "key": "pageSize",
                  "value": "50",
                  "description": "1–200, implicit 50."
                }
              ]
            },
            "description": "**Scope necesar:** `sales:read`\n\nRapoartele Z închise într-o perioadă, în ordinea închiderii. De aici pleacă importul vânzărilor în contabilitatea ta: un Z = o zi de vânzări pe o casă.\n\n### Parametri de interogare\n\n| Parametru | Tip | Implicit | Descriere |\n|---|---|---|---|\n| `from` / `to` | string | — | Obligatorii, `yyyy-MM-dd`, inclusiv; `to` ≥ `from`. Filtrează după **momentul închiderii**, în zile UTC. |\n| `terminalId` | string (uuid) | toate | Repetabil: `?terminalId=a&terminalId=b`. |\n| `page` | integer | 1 | ≥ 1. |\n| `pageSize` | integer | 50 | 1–200. |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `items[]` | array | Rapoartele Z; câmpurile sunt în tabelul de mai jos. |\n| `totalCount` | integer | Câte rapoarte corespund filtrului. |\n| `page`, `pageSize` | integer | Valorile aplicate. |\n| `totalPages` | integer | Numărul de pagini; 0 când nu e nimic. |\n| `hasNextPage`, `hasPreviousPage` | boolean | Navigarea. |\n| `summary`, `filterableColumns`, `ignoredFilters` | null | Mereu `null`. |\n\n### Raportul Z\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `id` | string (uuid) | Id-ul raportului Z. Folosește-l drept cheie de idempotență în sistemul tău. |\n| `terminalId` | string (uuid) | Casa. |\n| `number` | integer | Numărul Z de pe casă. |\n| `reportDateTime` | string | Momentul închiderii, ISO 8601 UTC. |\n| `tradingDay` | string | Ziua comercială, `yyyy-MM-dd`: ziua primei vânzări din Z. Datează documentele contabile după ea. |\n| `warehouseId` | string (uuid)? | Depozitul. |\n| `cashierName` | string? | Casierul. |\n| `totalSales`, `totalReturns` | number | Vânzări și retururi, lei, cu TVA. |\n| `totalDiscounts` | number | Reducerile, lei. |\n| `netRevenue` | number | `totalSales − totalReturns`. Net de retururi, **nu** de TVA. |\n| `totalVat` | number | TVA-ul zilei, lei. |\n| `cashAmount`, `cardAmount`, `otherAmount` | number | Încasări: numerar, card, restul formelor de plată. |\n| `receiptsCount`, `returnsCount` | integer | Bonuri și retururi. |\n| `sealedAt` | string? | Momentul sigilării Z-ului, ISO 8601 UTC. |\n| `fiscalSeal` | string? | Amprenta SHA-256 (64 de caractere hex) a Z-ului. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `INTEGRATIONS_API_DATE_REQUIRED`, `INTEGRATIONS_API_DATE_INVALID`, `COMMON_DATE_ON_OR_AFTER`, `COMMON_BETWEEN` | „Parametrul este obligatoriu: o dată în formatul yyyy-MM-dd.”, „Data trebuie să fie cel mai devreme 01.09.2026.”, „Valoarea trebuie să fie între 1 și 200.” — în `errors` și `errorCodes`, pe câmp (formatul de legare). |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Moldova e la UTC+2/+3: un Z închis la 01:30 ora locală cade, la filtrare, pe ziua precedentă.\n- O cheie limitată la anumite case vede doar rapoartele lor. Un filtru pe o casă nepermisă întoarce o pagină goală, nu `403`."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/z-reports?from={{monthStart}}&to={{today}}&page=1&pageSize=50",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "z-reports"
                  ],
                  "query": [
                    {
                      "key": "from",
                      "value": "{{monthStart}}",
                      "description": "yyyy-MM-dd, obligatoriu."
                    },
                    {
                      "key": "to",
                      "value": "{{today}}",
                      "description": "yyyy-MM-dd, obligatoriu, ≥ from."
                    },
                    {
                      "key": "terminalId",
                      "value": "",
                      "description": "Id de casă; repetabil.",
                      "disabled": true
                    },
                    {
                      "key": "page",
                      "value": "1",
                      "description": "De la 1."
                    },
                    {
                      "key": "pageSize",
                      "value": "50",
                      "description": "1–200, implicit 50."
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items\": [\n    {\n      \"id\": \"8f1c2b7a-3d4e-4f5a-9b6c-7d8e9f0a1b2c\",\n      \"terminalId\": \"5b2d6f3e-8c1a-4f7e-9a2b-3c4d5e6f7a81\",\n      \"number\": 67,\n      \"reportDateTime\": \"2026-09-24T17:47:31Z\",\n      \"tradingDay\": \"2026-09-24\",\n      \"warehouseId\": \"0d9e2a8b-1f3c-4b6e-8a7d-2c5f9e1b3a46\",\n      \"cashierName\": \"Ana Rusu\",\n      \"totalSales\": 282,\n      \"totalReturns\": 89.9,\n      \"totalDiscounts\": 0,\n      \"netRevenue\": 192.1,\n      \"totalVat\": 22.56,\n      \"cashAmount\": 102.2,\n      \"cardAmount\": 89.9,\n      \"otherAmount\": 0,\n      \"receiptsCount\": 4,\n      \"returnsCount\": 1,\n      \"sealedAt\": \"2026-09-24T17:47:33Z\",\n      \"fiscalSeal\": \"3f9a0c1e5b7d2a4f6e8c0b1d3f5a7c9e1b3d5f7a9c0e2b4d6f8a0c2e4b6d8f0a\"\n    }\n  ],\n  \"totalCount\": 1,\n  \"page\": 1,\n  \"pageSize\": 50,\n  \"summary\": null,\n  \"filterableColumns\": null,\n  \"ignoredFilters\": null,\n  \"totalPages\": 1,\n  \"hasNextPage\": false,\n  \"hasPreviousPage\": false\n}"
            },
            {
              "name": "400 — perioadă lipsă",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/z-reports?from={{monthStart}}&to={{today}}&page=1&pageSize=50",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "z-reports"
                  ],
                  "query": [
                    {
                      "key": "from",
                      "value": "{{monthStart}}",
                      "description": "yyyy-MM-dd, obligatoriu."
                    },
                    {
                      "key": "to",
                      "value": "{{today}}",
                      "description": "yyyy-MM-dd, obligatoriu, ≥ from."
                    },
                    {
                      "key": "terminalId",
                      "value": "",
                      "description": "Id de casă; repetabil.",
                      "disabled": true
                    },
                    {
                      "key": "page",
                      "value": "1",
                      "description": "De la 1."
                    },
                    {
                      "key": "pageSize",
                      "value": "50",
                      "description": "1–200, implicit 50."
                    }
                  ]
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/problem+json"
                }
              ],
              "body": "{\n  \"statusCode\": 400,\n  \"message\": \"One or more errors occurred!\",\n  \"errors\": {\n    \"from\": [\n      \"Parametrul este obligatoriu: o dată în formatul yyyy-MM-dd.\"\n    ]\n  },\n  \"errorCodes\": {\n    \"from\": [\n      {\n        \"code\": \"INTEGRATIONS_API_DATE_REQUIRED\",\n        \"params\": {}\n      }\n    ]\n  }\n}"
            }
          ]
        },
        {
          "name": "Raport Z",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/z-reports/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "z-reports",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "id din „Rapoarte Z”"
                }
              ]
            },
            "description": "**Scope necesar:** `sales:read`\n\nUn raport Z cu vânzările lui adunate pe produs, formele de plată și TVA-ul pe cote. E tot ce-ți trebuie ca să contabilizezi ziua.\n\n### Parametri de cale\n\n| Parametru | Tip | Descriere |\n|---|---|---|\n| `id` | string (uuid) | Id-ul raportului Z. |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `report` | object | Raportul Z, cu câmpurile de la „Rapoarte Z”. |\n| `lines[]` | array | Vânzările zilei, adunate pe produs × cotă TVA × sens (vânzare/retur). Același produs poate apărea pe mai multe linii; returul are cantitate și total negative. |\n| `lines[].productCode`, `productName` | string | Codul și denumirea; pot fi goale. |\n| `lines[].productId` | string (uuid)? | Produsul din nomenclator; `null` dacă articolul de la casă nu e legat de nomenclator. |\n| `lines[].posProductId` | string (uuid)? | Cardul de produs de la casă. |\n| `lines[].unit` | string? | Unitatea de pe bon (`buc`, `kg`). |\n| `lines[].quantity`, `lines[].total` | number | Cantitatea și totalul **cu TVA**, lei. |\n| `lines[].vatCode` | string | 20% → `A`, 8% → `B`, altfel litera de pe bon (`C`, `D`, `_` = fără TVA). |\n| `lines[].vatAmount`, `lines[].vatPercent` | number | TVA-ul încasat efectiv și cota. Nu recalcula TVA-ul din literă. |\n| `lines[].discountAmount` | number | Reducerea acordată pe bonuri pentru linie, deja scăzută din `total`. `total` + `discountAmount` = valoarea înainte de reducere. |\n| `tenders[]` | array | Încasările pe forme de plată: `code`, `amount`, `isCash`. De aici se decontează plățile. |\n| `vat` | object | `totalA`…`totalD` (cu TVA) și `vatA`…`vatD` pe cote; `total` = suma liniilor. Litera `_` și cele necunoscute intră în `D`. |\n| `cardVoidsTotal` | number | Plăți cu cardul anulate înainte de decontarea băncii: banca decontează mai puțin decât arată `tenders`. |\n| `cardRefundsTotal` | number | Rambursări pe card după decontare; aparțin unei perioade ulterioare. |\n\n### Codurile din `tenders[].code`\n\n`cash` · `card` · `voucher` · `check` · `ticket` · `mealTicket` · `subscription` · `transfer` · `credit` · `leasing` · `advance` · `deposit` · `pledge` · `compensation` · `otherCredit`. `other` apare doar la rapoartele vechi fără defalcare. `isCash` e `true` doar pentru `cash`.\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 404 | — | Z-ul nu există, e al altui comerciant sau casa lui nu e permisă cheii. Corp gol. |\n| 400 | — | `id` nu e GUID (formatul de legare). |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |"
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/z-reports/:id",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "z-reports",
                    ":id"
                  ],
                  "variable": [
                    {
                      "key": "id",
                      "value": "",
                      "description": "id din „Rapoarte Z”"
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"report\": {\n    \"id\": \"8f1c2b7a-3d4e-4f5a-9b6c-7d8e9f0a1b2c\",\n    \"terminalId\": \"5b2d6f3e-8c1a-4f7e-9a2b-3c4d5e6f7a81\",\n    \"number\": 67,\n    \"reportDateTime\": \"2026-09-24T17:47:31Z\",\n    \"tradingDay\": \"2026-09-24\",\n    \"warehouseId\": \"0d9e2a8b-1f3c-4b6e-8a7d-2c5f9e1b3a46\",\n    \"cashierName\": \"Ana Rusu\",\n    \"totalSales\": 282,\n    \"totalReturns\": 89.9,\n    \"totalDiscounts\": 0,\n    \"netRevenue\": 192.1,\n    \"totalVat\": 22.56,\n    \"cashAmount\": 102.2,\n    \"cardAmount\": 89.9,\n    \"otherAmount\": 0,\n    \"receiptsCount\": 4,\n    \"returnsCount\": 1,\n    \"sealedAt\": \"2026-09-24T17:47:33Z\",\n    \"fiscalSeal\": \"3f9a0c1e5b7d2a4f6e8c0b1d3f5a7c9e1b3d5f7a9c0e2b4d6f8a0c2e4b6d8f0a\"\n  },\n  \"lines\": [\n    {\n      \"productCode\": \"4840167001234\",\n      \"productId\": \"a1b2c3d4-0000-4000-8000-000000000001\",\n      \"posProductId\": \"b1c2d3e4-0000-4000-8000-000000000001\",\n      \"productName\": \"Lapte Fabrikant 2,5% 1L\",\n      \"unit\": \"buc\",\n      \"quantity\": 3,\n      \"total\": 59.7,\n      \"vatCode\": \"B\",\n      \"vatAmount\": 4.42,\n      \"vatPercent\": 8,\n      \"discountAmount\": 0\n    },\n    {\n      \"productCode\": \"4840001000057\",\n      \"productId\": \"a1b2c3d4-0000-4000-8000-000000000002\",\n      \"posProductId\": \"b1c2d3e4-0000-4000-8000-000000000002\",\n      \"productName\": \"Pâine albă 500 g\",\n      \"unit\": \"buc\",\n      \"quantity\": 5,\n      \"total\": 42.5,\n      \"vatCode\": \"B\",\n      \"vatAmount\": 3.15,\n      \"vatPercent\": 8,\n      \"discountAmount\": 0\n    },\n    {\n      \"productCode\": \"8714599101230\",\n      \"productId\": \"a1b2c3d4-0000-4000-8000-000000000003\",\n      \"posProductId\": \"b1c2d3e4-0000-4000-8000-000000000003\",\n      \"productName\": \"Cafea Jacobs Monarch 250 g\",\n      \"unit\": \"buc\",\n      \"quantity\": 2,\n      \"total\": 179.8,\n      \"vatCode\": \"A\",\n      \"vatAmount\": 29.97,\n      \"vatPercent\": 20,\n      \"discountAmount\": 0\n    },\n    {\n      \"productCode\": \"8714599101230\",\n      \"productId\": \"a1b2c3d4-0000-4000-8000-000000000003\",\n      \"posProductId\": \"b1c2d3e4-0000-4000-8000-000000000003\",\n      \"productName\": \"Cafea Jacobs Monarch 250 g\",\n      \"unit\": \"buc\",\n      \"quantity\": -1,\n      \"total\": -89.9,\n      \"vatCode\": \"A\",\n      \"vatAmount\": -14.98,\n      \"vatPercent\": 20,\n      \"discountAmount\": 0\n    }\n  ],\n  \"tenders\": [\n    {\n      \"code\": \"cash\",\n      \"amount\": 102.2,\n      \"isCash\": true\n    },\n    {\n      \"code\": \"card\",\n      \"amount\": 89.9,\n      \"isCash\": false\n    }\n  ],\n  \"vat\": {\n    \"totalA\": 89.9,\n    \"totalB\": 102.2,\n    \"totalC\": 0,\n    \"totalD\": 0,\n    \"vatA\": 14.99,\n    \"vatB\": 7.57,\n    \"vatC\": 0,\n    \"vatD\": 0,\n    \"total\": 192.1\n  },\n  \"cardVoidsTotal\": 0,\n  \"cardRefundsTotal\": 0\n}"
            }
          ]
        },
        {
          "name": "Bonurile unui Z",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/z-reports/:id/receipts?page=1&pageSize=100",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "z-reports",
                ":id",
                "receipts"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1",
                  "description": "De la 1."
                },
                {
                  "key": "pageSize",
                  "value": "100",
                  "description": "1–500, implicit 100."
                }
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "id din „Rapoarte Z”"
                }
              ]
            },
            "description": "**Scope necesar:** `sales:read`\n\nBonurile brute ale unui Z, cu liniile și plățile fiecăruia, în ordinea emiterii. Pentru contabilitate ajunge „Raport Z”; bonurile servesc la reconciliere și la analize pe client sau pe oră.\n\n### Parametri de cale\n\n| Parametru | Tip | Descriere |\n|---|---|---|\n| `id` | string (uuid) | Id-ul raportului Z. |\n\n### Parametri de interogare\n\n| Parametru | Tip | Implicit | Descriere |\n|---|---|---|---|\n| `page` | integer | 1 | ≥ 1. |\n| `pageSize` | integer | 100 | 1–500. |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `items[]` | array | Bonurile. |\n| `items[].id`, `transactionNumber` | string, integer | Id-ul și numărul tranzacției. |\n| `items[].receiptNumber` | integer? | Numărul bonului. |\n| `items[].fiscalReceiptNumber`, `sfsFiscalCode`, `sfsReceiptUrl` | string? | Datele fiscale; adresa de verificare la SFS. |\n| `items[].dateTime` | string | ISO 8601 UTC. |\n| `items[].cashierName` | string? | Casierul. |\n| `items[].total`, `vat`, `discount`, `change` | number | Totalul cu TVA, TVA-ul, reducerea, restul dat. **Negative pe retur.** |\n| `items[].currency` | string | Moneda (`MDL`). |\n| `items[].isRefund`, `isFiscal` | boolean | Retur; bon fiscal. |\n| `items[].lines[]` | array | `productCode`, `productId`, `posProductId`, `productName`, `unit`, `quantity` (negativă pe retur), `unitPrice`, `total`, `discountAmount`, `vatCode` (litera de pe bon), `vatAmount`, `vatPercent`. |\n| `items[].payments[]` | array | `code` (codul ACPS de pe bon: `\"1\"` numerar, `\"2\"` card…), `name`, `amount` (la numerar: suma dată de client), `cardMask`, `reference` (RRN), `voidedAmount`, `refundedAmount`, `reversedAt`. |\n| `totalCount`, `page`, `pageSize` | integer | Paginarea. |\n| `terminalId` | string (uuid) | Casa Z-ului. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 404 | — | Z-ul nu există, e al altui comerciant sau casa lui nu e permisă cheii. Corp gol. |\n| 400 | — | `id` nu e GUID, sau `page`/`pageSize` în afara limitelor. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Codurile de plată de aici sunt cele ACPS de pe bon, **alt vocabular** decât `tenders[].code` din „Raport Z”."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/z-reports/:id/receipts?page=1&pageSize=100",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "z-reports",
                    ":id",
                    "receipts"
                  ],
                  "query": [
                    {
                      "key": "page",
                      "value": "1",
                      "description": "De la 1."
                    },
                    {
                      "key": "pageSize",
                      "value": "100",
                      "description": "1–500, implicit 100."
                    }
                  ],
                  "variable": [
                    {
                      "key": "id",
                      "value": "",
                      "description": "id din „Rapoarte Z”"
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items\": [\n    {\n      \"id\": \"c7d8e9f0-1a2b-4c3d-8e4f-5a6b7c8d9e0f\",\n      \"transactionNumber\": 1051,\n      \"receiptNumber\": 41,\n      \"fiscalReceiptNumber\": null,\n      \"dateTime\": \"2026-09-24T07:12:05Z\",\n      \"cashierName\": \"Ana Rusu\",\n      \"total\": 76.7,\n      \"vat\": 5.68,\n      \"discount\": 0,\n      \"change\": 23.3,\n      \"currency\": \"MDL\",\n      \"isRefund\": false,\n      \"isFiscal\": true,\n      \"sfsFiscalCode\": \"7F3A9C21\",\n      \"sfsReceiptUrl\": \"https://mev.sfs.md/receipt-verifier/7F3A9C21\",\n      \"lines\": [\n        {\n          \"productCode\": \"4840167001234\",\n          \"productId\": \"a1b2c3d4-0000-4000-8000-000000000001\",\n          \"posProductId\": \"b1c2d3e4-0000-4000-8000-000000000001\",\n          \"productName\": \"Lapte Fabrikant 2,5% 1L\",\n          \"unit\": \"buc\",\n          \"quantity\": 3,\n          \"unitPrice\": 19.9,\n          \"total\": 59.7,\n          \"discountAmount\": 0,\n          \"vatCode\": \"B\",\n          \"vatAmount\": 4.42,\n          \"vatPercent\": 8\n        },\n        {\n          \"productCode\": \"4840001000057\",\n          \"productId\": \"a1b2c3d4-0000-4000-8000-000000000002\",\n          \"posProductId\": \"b1c2d3e4-0000-4000-8000-000000000002\",\n          \"productName\": \"Pâine albă 500 g\",\n          \"unit\": \"buc\",\n          \"quantity\": 2,\n          \"unitPrice\": 8.5,\n          \"total\": 17,\n          \"discountAmount\": 0,\n          \"vatCode\": \"B\",\n          \"vatAmount\": 1.26,\n          \"vatPercent\": 8\n        }\n      ],\n      \"payments\": [\n        {\n          \"code\": \"1\",\n          \"name\": \"NUMERAR\",\n          \"amount\": 100,\n          \"cardMask\": null,\n          \"reference\": null,\n          \"voidedAmount\": 0,\n          \"refundedAmount\": 0,\n          \"reversedAt\": null\n        }\n      ]\n    }\n  ],\n  \"totalCount\": 1,\n  \"page\": 1,\n  \"pageSize\": 100,\n  \"terminalId\": \"5b2d6f3e-8c1a-4f7e-9a2b-3c4d5e6f7a81\"\n}"
            }
          ]
        },
        {
          "name": "Vânzări pe zile și canale",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/sales/daily?from={{monthStart}}&to={{today}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "sales",
                "daily"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "{{monthStart}}",
                  "description": "yyyy-MM-dd, obligatoriu."
                },
                {
                  "key": "to",
                  "value": "{{today}}",
                  "description": "yyyy-MM-dd, obligatoriu, cel mult 366 de zile după from."
                },
                {
                  "key": "channelId",
                  "value": "",
                  "description": "Canalul de vânzare.",
                  "disabled": true
                },
                {
                  "key": "terminalId",
                  "value": "",
                  "description": "Id de casă; repetabil.",
                  "disabled": true
                }
              ]
            },
            "description": "**Scope necesar:** `sales:read`\n\nToate vânzările comerciantului, adunate pe zile și canale de vânzare: bonurile de la casă, facturile, retururile și rapoartele de amănunt. Sunt aceleași cifre pe care comerciantul le vede pe ecranul „Vânzări” și pe care le trimit webhook-urile `sale.*`.\n\n### Parametri de interogare\n\n| Parametru | Tip | Implicit | Descriere |\n|---|---|---|---|\n| `from` / `to` | string | — | Obligatorii, `yyyy-MM-dd`, inclusiv; `to` ≥ `from`, cel mult 366 de zile. Filtrează după **ziua vânzării**, ora Moldovei. |\n| `channelId` | string (uuid) | toate | Un singur canal de vânzare. |\n| `terminalId` | string (uuid) | toate | Repetabil. Cu case alese intră doar vânzările lor — facturile n-au casă, deci nu intră. |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `items[]` | array | Un rând pe zi și canal, în ordinea zilelor. |\n| `items[].date` | string | Ziua, `yyyy-MM-dd`. |\n| `items[].channelId`, `channelName` | string? | Canalul de vânzare; `null` când documentul n-are canal. |\n| `items[].channelType` | string | `Store`, `DirectSales`, `Online`, `SocialNetwork`, `Phone`, `Marketplace`, `Vending` sau `Other`. |\n| `items[].documents` | integer | Câte documente (bonuri, facturi, retururi) intră în rând. |\n| `items[].netAmount` | number | Vânzările fără TVA, lei, retururile scăzute. |\n| `items[].vatAmount` | number | TVA-ul, lei. |\n| `items[].totalAmount` | number | `netAmount + vatAmount`. |\n| `currency` | string | Mereu `MDL`. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `INTEGRATIONS_API_DATE_REQUIRED`, `INTEGRATIONS_API_DATE_INVALID`, `COMMON_DATE_ON_OR_AFTER`, `INTEGRATIONS_API_PERIOD_TOO_LONG` | „Perioada poate avea cel mult 366 de zile. Cereți raportul pe bucăți.” — în `errors` și `errorCodes`, pe câmp (formatul de legare). |\n| 422 | `REPORTS_SALES_LEDGER_DISABLED` | „Registrul comun de vânzări nu e pornit încă pe acest mediu. Vânzările rămân în rapoartele de casă și de contabilitate.” |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Raportul nu are paginare: un an are cel mult 366 de zile × numărul canalelor comerciantului.\n- Vânzările din ultimele minute pot lipsi: registrul se completează din evenimente, de obicei în câteva secunde.\n- O cheie limitată la anumite case vede doar vânzările lor. Un filtru pe o casă nepermisă întoarce o listă goală, nu `403`."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/sales/daily?from={{monthStart}}&to={{today}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "sales",
                    "daily"
                  ],
                  "query": [
                    {
                      "key": "from",
                      "value": "{{monthStart}}",
                      "description": "yyyy-MM-dd, obligatoriu."
                    },
                    {
                      "key": "to",
                      "value": "{{today}}",
                      "description": "yyyy-MM-dd, obligatoriu, cel mult 366 de zile după from."
                    },
                    {
                      "key": "channelId",
                      "value": "",
                      "description": "Canalul de vânzare.",
                      "disabled": true
                    },
                    {
                      "key": "terminalId",
                      "value": "",
                      "description": "Id de casă; repetabil.",
                      "disabled": true
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items\": [\n    {\n      \"date\": \"2026-09-24\",\n      \"channelId\": \"3c1e9a7b-5d2f-4b8e-9a1c-7e5d3b2f1a90\",\n      \"channelName\": \"Magazin\",\n      \"channelType\": \"Store\",\n      \"documents\": 41,\n      \"netAmount\": 9634.26,\n      \"vatAmount\": 1926.85,\n      \"totalAmount\": 11561.11\n    }\n  ],\n  \"currency\": \"MDL\"\n}"
            },
            {
              "name": "400 — perioadă prea lungă",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/sales/daily?from={{monthStart}}&to={{today}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "sales",
                    "daily"
                  ],
                  "query": [
                    {
                      "key": "from",
                      "value": "{{monthStart}}",
                      "description": "yyyy-MM-dd, obligatoriu."
                    },
                    {
                      "key": "to",
                      "value": "{{today}}",
                      "description": "yyyy-MM-dd, obligatoriu, cel mult 366 de zile după from."
                    },
                    {
                      "key": "channelId",
                      "value": "",
                      "description": "Canalul de vânzare.",
                      "disabled": true
                    },
                    {
                      "key": "terminalId",
                      "value": "",
                      "description": "Id de casă; repetabil.",
                      "disabled": true
                    }
                  ]
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/problem+json"
                }
              ],
              "body": "{\n  \"statusCode\": 400,\n  \"message\": \"One or more errors occurred!\",\n  \"errors\": {\n    \"to\": [\n      \"Perioada poate avea cel mult 366 de zile. Cereți raportul pe bucăți.\"\n    ]\n  },\n  \"errorCodes\": {\n    \"to\": [\n      {\n        \"code\": \"INTEGRATIONS_API_PERIOD_TOO_LONG\",\n        \"params\": {\n          \"days\": 366\n        }\n      }\n    ]\n  }\n}"
            },
            {
              "name": "422 — registrul nu e pornit",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/sales/daily?from={{monthStart}}&to={{today}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "sales",
                    "daily"
                  ],
                  "query": [
                    {
                      "key": "from",
                      "value": "{{monthStart}}",
                      "description": "yyyy-MM-dd, obligatoriu."
                    },
                    {
                      "key": "to",
                      "value": "{{today}}",
                      "description": "yyyy-MM-dd, obligatoriu, cel mult 366 de zile după from."
                    },
                    {
                      "key": "channelId",
                      "value": "",
                      "description": "Canalul de vânzare.",
                      "disabled": true
                    },
                    {
                      "key": "terminalId",
                      "value": "",
                      "description": "Id de casă; repetabil.",
                      "disabled": true
                    }
                  ]
                }
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                }
              ],
              "body": "{\n  \"type\": \"https://httpstatuses.io/422\",\n  \"title\": \"REPORTS_SALES_LEDGER_DISABLED\",\n  \"status\": 422,\n  \"detail\": \"Registrul comun de vânzări nu e pornit încă pe acest mediu. Vânzările rămân în rapoartele de casă și de contabilitate.\",\n  \"errorCode\": \"REPORTS_SALES_LEDGER_DISABLED\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "6. Webhook-uri",
      "description": "Evenimentele trimise la adresa ta: produse, stoc, comenzi, vânzări, facturi recunoscute, fidelitate. Contractul livrării e în descrierea colecției.",
      "item": [
        {
          "name": "Creează webhook",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/webhooks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "webhooks"
              ]
            },
            "description": "**Scope necesar:** `webhooks:manage`\n\nAbonează o adresă a ta la evenimente. Răspunsul conține secretul de semnare — **singura dată** când îl vezi.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `url` | string | da | Adresă `https`, max 2048 caractere. O adresă IP privată e refuzată. |\n| `eventTypes[]` | string[] | da | Cel puțin un eveniment din lista din descrierea colecției. |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `id` | string (uuid) | Id-ul abonamentului. |\n| `url` | string | Adresa. |\n| `eventTypes[]` | string[] | Evenimentele, cu litere mici, fără dubluri. |\n| `signingSecret` | string | `whsec_…` — cheia cu care verifici semnătura. Nu se mai poate citi și nu se poate schimba: pentru un secret nou, șterge abonamentul și creează altul. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `VALIDATION` | „Adresa webhook-ului trebuie să folosească https.”, „Alegeți cel puțin un tip de eveniment.”, „Tip de eveniment necunoscut. Tipurile permise: ….” — codul fiecăruia în `errorCodes`, pe câmp. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Răspunsul e `200`, nu `201`.\n- Aceeași adresă înregistrată de două ori primește fiecare eveniment de două ori.\n- Numele de domeniu se verifică la fiecare trimitere, nu la înregistrare: un domeniu care duce la o adresă privată se acceptă aici, dar livrările eșuează.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://erp.magazin.md/hooks/posfix\",\n  \"eventTypes\": [\n    \"order.created\",\n    \"order.paid\",\n    \"inventory.updated\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/webhooks",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "webhooks"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"url\": \"https://erp.magazin.md/hooks/posfix\",\n  \"eventTypes\": [\n    \"order.created\",\n    \"order.paid\",\n    \"inventory.updated\"\n  ]\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"id\": \"e3b5c1d2-7a8f-4c6e-9d0b-1a2b3c4d5e6f\",\n  \"url\": \"https://erp.magazin.md/hooks/posfix\",\n  \"eventTypes\": [\n    \"order.created\",\n    \"order.paid\",\n    \"inventory.updated\"\n  ],\n  \"signingSecret\": \"whsec_Q2x4bE9mY2J3aVZ5dGNWa0Q2ZUx0ZlQ5c2h1Tk1jUgA\"\n}"
            },
            {
              "name": "400 — adresă http",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/webhooks",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "webhooks"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"url\": \"https://erp.magazin.md/hooks/posfix\",\n  \"eventTypes\": [\n    \"order.created\",\n    \"order.paid\",\n    \"inventory.updated\"\n  ]\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                }
              ],
              "body": "{\n  \"type\": \"https://httpstatuses.io/400\",\n  \"title\": \"VALIDATION\",\n  \"status\": 400,\n  \"detail\": \"Adresa webhook-ului trebuie să folosească https.\",\n  \"errorCode\": \"VALIDATION\",\n  \"errors\": {\n    \"url\": [\n      \"Adresa webhook-ului trebuie să folosească https.\"\n    ]\n  },\n  \"errorCodes\": {\n    \"url\": [\n      {\n        \"code\": \"INTEGRATIONS_WEBHOOK_URL_HTTPS\",\n        \"params\": {}\n      }\n    ]\n  }\n}"
            }
          ]
        },
        {
          "name": "Listă webhook-uri",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/webhooks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "webhooks"
              ]
            },
            "description": "**Scope necesar:** `webhooks:manage`\n\nAbonamentele active, cele mai noi întâi. Secretul nu apare.\n\n### Răspuns 200 — listă\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `[].id` | string (uuid) | Id-ul abonamentului. |\n| `[].url` | string | Adresa. |\n| `[].eventTypes[]` | string[] | Evenimentele. |\n| `[].isActive` | boolean | Mereu `true`: un abonament se oprește doar prin ștergere. |\n| `[].createdAt` | string | ISO 8601 UTC. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Răspunsul e direct o listă JSON, fără paginare."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/webhooks",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "webhooks"
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "[\n  {\n    \"id\": \"e3b5c1d2-7a8f-4c6e-9d0b-1a2b3c4d5e6f\",\n    \"url\": \"https://erp.magazin.md/hooks/posfix\",\n    \"eventTypes\": [\n      \"order.created\",\n      \"order.paid\",\n      \"inventory.updated\"\n    ],\n    \"isActive\": true,\n    \"createdAt\": \"2026-09-25T08:02:11.482913Z\"\n  }\n]"
            }
          ]
        },
        {
          "name": "Șterge webhook",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/webhooks/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "webhooks",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "id din „Creează webhook”"
                }
              ]
            },
            "description": "**Scope necesar:** `webhooks:manage`\n\nOprește un abonament. Livrările din coadă nu mai pleacă.\n\n### Parametri de cale\n\n| Parametru | Tip | Descriere |\n|---|---|---|\n| `id` | string (uuid) | Id-ul abonamentului. |\n\n### Răspuns\n\n**204** fără corp.\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 404 | `INTEGRATIONS_RESOURCE_NOT_FOUND` | Id necunoscut, al altui comerciant sau **deja șters** (format simplu). |\n| 400 | — | `id` nu e GUID. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- A doua ștergere a aceluiași id dă `404`, nu `204`."
          },
          "response": [
            {
              "name": "204 — șters",
              "originalRequest": {
                "method": "DELETE",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/webhooks/:id",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "webhooks",
                    ":id"
                  ],
                  "variable": [
                    {
                      "key": "id",
                      "value": "",
                      "description": "id din „Creează webhook”"
                    }
                  ]
                }
              },
              "status": "No Content",
              "code": 204,
              "_postman_previewlanguage": "text",
              "header": [],
              "body": ""
            },
            {
              "name": "404 — necunoscut sau deja șters",
              "originalRequest": {
                "method": "DELETE",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/webhooks/:id",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "webhooks",
                    ":id"
                  ],
                  "variable": [
                    {
                      "key": "id",
                      "value": "",
                      "description": "id din „Creează webhook”"
                    }
                  ]
                }
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"type\": \"https://httpstatuses.io/404\",\n  \"title\": \"Not Found\",\n  \"status\": 404,\n  \"detail\": \"Resursa cerută nu există sau nu aparține acestui comerciant.\",\n  \"errorCode\": \"INTEGRATIONS_RESOURCE_NOT_FOUND\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "7. Recunoaștere facturi",
      "description": "Facturi PDF sau fotografiate → date structurate, potrivite pe nomenclatorul tău. Pașii: (1) încarci nomenclatorul (o dată, apoi doar schimbările); (2) trimiți fișierul și primești `202` cu id; (3) aștepți webhook-ul `docai.recognition.completed` sau întrebi pe id din 10 în 10 secunde; (4) citești factura și o imporți; (5) confirmi cu `ack`. Serviciul e contra cost: fiecare fișier nou consumă din pachetul comerciantului.",
      "item": [
        {
          "name": "Nomenclator — încărcare / actualizare",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/docai/catalog/items",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "docai",
                "catalog",
                "items"
              ]
            },
            "description": "**Scope necesar:** `docai:catalog`\n\nNomenclatorul **programului tău** (1C, ERP), pe care se potrivesc liniile facturilor recunoscute. Codul tău (`externalId`) e cheia: o a doua trimitere actualizează poziția, nu o dublează. Trimite-l în bucăți de cel mult 1000 de poziții. Nimic nu se șterge — o poziție scoasă din uz se trimite cu `isActive: false`.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `items[]` | array | da | 1–1000 de poziții; un cod o singură dată pe cerere. |\n| `items[].externalId` | string | da | Codul poziției la tine, cel mult 100 de caractere. |\n| `items[].name` | string | da, pe cele active | Cel mult 500 de caractere. |\n| `items[].unit` | string | nu | Unitatea ta de măsură, cel mult 50; nu se convertește. |\n| `items[].barcodes` | string[] | nu | Coduri de bare. Se păstrează doar cele de 8–14 cifre; restul se numără în `ignoredBarcodes`. |\n| `items[].isActive` | boolean | nu | Implicit `true`. `false` = retrasă (nu se mai propune). |\n| `items[].nomenclatureType` | string | nu | Tipul de nomenclatură la tine (ex. Lactate), cel mult 200. Fără el, poziția rămâne fără tip. Iese înapoi cu același nume în listă, pe `match` și în export. |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `created`, `updated`, `unchanged`, `deactivated` | integer | Ce s-a întâmplat cu pozițiile trimise. |\n| `ignoredBarcodes` | integer | Coduri de bare lăsate deoparte (nu sunt 8–14 cifre). |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `DOCAI_CATALOG_API_TOO_MANY_ITEMS`, `DOCAI_CATALOG_API_DUPLICATE_CODE`, validare pe câmp | „O cerere poate avea cel mult 1.000 de poziții, iar aceasta are 1.200. Trimiteți nomenclatorul în mai multe cereri.” |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Odată ce programul tău ține nomenclatorul prin API, încărcarea lui din Excel în panou se refuză — altfel sincronizarea următoare ar rescrie ce a încărcat omul.\n- Pozițiile create de oameni în PosFix au coduri `DX-…` (vin la tine prin listă, `match` și export). O trimitere care nimerește codul unei astfel de poziții nu o rescrie — se numără la `unchanged`. Nu folosi prefixul `DX-` pentru codurile tale.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"items\": [\n    {\n      \"externalId\": \"LAC-0012\",\n      \"name\": \"Lapte Lactis 2,5% 1l\",\n      \"unit\": \"buc\",\n      \"barcodes\": [\n        \"4840811000181\"\n      ],\n      \"nomenclatureType\": \"Lactate\"\n    },\n    {\n      \"externalId\": \"LAC-0099\",\n      \"isActive\": false\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "PUT",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/docai/catalog/items",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "docai",
                    "catalog",
                    "items"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"items\": [\n    {\n      \"externalId\": \"LAC-0012\",\n      \"name\": \"Lapte Lactis 2,5% 1l\",\n      \"unit\": \"buc\",\n      \"barcodes\": [\n        \"4840811000181\"\n      ],\n      \"nomenclatureType\": \"Lactate\"\n    },\n    {\n      \"externalId\": \"LAC-0099\",\n      \"isActive\": false\n    }\n  ]\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"created\": 1,\n  \"updated\": 0,\n  \"unchanged\": 0,\n  \"deactivated\": 1,\n  \"ignoredBarcodes\": 0\n}"
            }
          ]
        },
        {
          "name": "Nomenclator — listă",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/docai/catalog/items?includeInactive=false&page=1&pageSize=100",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "docai",
                "catalog",
                "items"
              ],
              "query": [
                {
                  "key": "search",
                  "value": "",
                  "description": "Text din cod sau denumire; un cod de bare întreg se caută exact.",
                  "disabled": true
                },
                {
                  "key": "includeInactive",
                  "value": "false",
                  "description": "Și pozițiile retrase."
                },
                {
                  "key": "page",
                  "value": "1",
                  "description": "De la 1."
                },
                {
                  "key": "pageSize",
                  "value": "100",
                  "description": "1–200, implicit 100."
                }
              ]
            },
            "description": "**Scope necesar:** `docai:catalog`\n\nCe a primit PosFix din nomenclatorul tău, în ordine alfabetică — pentru verificarea sincronizării. Lista cuprinde și pozițiile create de oameni în PosFix (coduri `DX-…`), ca programul tău să le preia.\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `items[].externalId`, `name`, `unit`, `barcodes` |  | Cum le-ai trimis (sau cum le-a creat omul, pentru `DX-…`). |\n| `items[].nomenclatureType` | string? | Tipul de nomenclatură; `null` când nu are. |\n| `items[].isActive` | boolean | Poziția se propune la potrivire. |\n| `items[].usageCount` | integer | Pe câte linii a fost aleasă de oameni la verificare. |\n| `items[].updatedAt` | string | Ultima schimbare, ISO 8601 UTC. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |"
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/docai/catalog/items?includeInactive=false&page=1&pageSize=100",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "docai",
                    "catalog",
                    "items"
                  ],
                  "query": [
                    {
                      "key": "search",
                      "value": "",
                      "description": "Text din cod sau denumire; un cod de bare întreg se caută exact.",
                      "disabled": true
                    },
                    {
                      "key": "includeInactive",
                      "value": "false",
                      "description": "Și pozițiile retrase."
                    },
                    {
                      "key": "page",
                      "value": "1",
                      "description": "De la 1."
                    },
                    {
                      "key": "pageSize",
                      "value": "100",
                      "description": "1–200, implicit 100."
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items\": [\n    {\n      \"externalId\": \"DX-000001\",\n      \"name\": \"Chefir Lactis 2,5% 0,5l\",\n      \"unit\": \"buc\",\n      \"barcodes\": [],\n      \"isActive\": true,\n      \"usageCount\": 1,\n      \"updatedAt\": \"2026-11-02T09:12:40Z\",\n      \"nomenclatureType\": \"Lactate\"\n    },\n    {\n      \"externalId\": \"LAC-0012\",\n      \"name\": \"Lapte Lactis 2,5% 1l\",\n      \"unit\": \"buc\",\n      \"barcodes\": [\n        \"4840811000181\"\n      ],\n      \"isActive\": true,\n      \"usageCount\": 7,\n      \"updatedAt\": \"2026-09-27T16:02:11Z\",\n      \"nomenclatureType\": \"Lactate\"\n    }\n  ],\n  \"totalCount\": 2,\n  \"page\": 1,\n  \"pageSize\": 50,\n  \"totalPages\": 1,\n  \"hasNextPage\": false,\n  \"hasPreviousPage\": false,\n  \"summary\": null,\n  \"filterableColumns\": null,\n  \"ignoredFilters\": null\n}"
            }
          ]
        },
        {
          "name": "Nomenclator — retragere",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/docai/catalog/items/:externalId",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "docai",
                "catalog",
                "items",
                ":externalId"
              ],
              "variable": [
                {
                  "key": "externalId",
                  "value": "LAC-0099",
                  "description": "Codul poziției la tine (URL-encoded)."
                }
              ]
            },
            "description": "**Scope necesar:** `docai:catalog`\n\nRetrage o poziție (echivalent cu `isActive: false`). Nu o șterge: facturile potrivite deja pe ea rămân legate de codul ei. Întoarce poziția.\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 404 | — | Codul nu e în nomenclatorul trimis. Corp gol. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |"
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "DELETE",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/docai/catalog/items/:externalId",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "docai",
                    "catalog",
                    "items",
                    ":externalId"
                  ],
                  "variable": [
                    {
                      "key": "externalId",
                      "value": "LAC-0099",
                      "description": "Codul poziției la tine (URL-encoded)."
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"externalId\": \"LAC-0099\",\n  \"name\": \"Chefir 1l\",\n  \"unit\": \"buc\",\n  \"barcodes\": [],\n  \"isActive\": false,\n  \"usageCount\": 0,\n  \"updatedAt\": \"2026-09-28T08:00:00Z\",\n  \"nomenclatureType\": null\n}"
            }
          ]
        },
        {
          "name": "Trimite o factură la recunoaștere",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/docai/recognitions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "docai",
                "recognitions"
              ]
            },
            "description": "**Scope necesar:** `docai:recognize`\n\nPune fișierul în coada de recunoaștere și răspunde imediat, **fără** să aștepte citirea (durează de obicei 20–90 de secunde). Rezultatul îl primești ca webhook `docai.recognition.completed` / `docai.recognition.failed`, sau îl citești pe id.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `file` | fișier (multipart) | da | PDF cel mult 12 MB; fotografie cel mult 20 MB. Tipul se citește din conținut (PDF, JPEG, PNG, GIF, WebP); antetul părții contează doar când conținutul nu e recunoscut. |\n| `documentKind` | string | nu | `supplier_invoice` (factura primită de la furnizor, implicit) sau `sales_invoice`. |\n| `externalRef` | string | nu | Cheia ta (ex. numărul înregistrării la tine). A doua trimitere cu aceeași cheie întoarce recunoașterea existentă, fără alt cost. |\n\n### Răspuns\n\n**202** — fișierul a intrat în coadă. **200** — recunoașterea exista deja: aceeași `externalRef`, același fișier încă în lucru (`existing: true`), sau un fișier deja recunoscut, copiat fără cost (`duplicate: true`, `status: completed`).\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `id` | string (uuid) | Id-ul recunoașterii. |\n| `status` | string | `queued` \\| `processing` \\| `completed` \\| `failed`. |\n| `externalRef` | string? | Cheia ta. |\n| `existing` | boolean | Nimic nou nu s-a pus în coadă. |\n| `duplicate` | boolean | Rezultatul e copiat de la același fișier recunoscut deja. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `DOCAI_FILE_REQUIRED`, `DOCAI_FILE_TYPE_NOT_SUPPORTED`, `DOCAI_FILE_TOO_LARGE`, `DOCAI_PDF_TOO_LARGE`, `DOCAI_DOCUMENT_KIND_UNKNOWN` | „Tipul de fișier „application/zip” nu este acceptat. Se acceptă JPEG, PNG, WebP, GIF și PDF.” |\n| 402 | `DOCAI_FREE_TRIAL_USED`, `DOCAI_MONTHLY_LIMIT_REACHED`, `DOCAI_BATCH_EXCEEDS_MONTHLY_LIMIT`, `DOCAI_LIMIT_DENIED` | Recunoașterile s-au terminat: fără contract, cele 3 gratuite; cu contract, plafonul lunar din el (contractul fără plafon nu refuză). Format simplu, cu `title: \"Payment Required\"`. |\n| 422 | `DOCAI_EXTERNAL_REF_REUSED` | „Cheia externă „INV-2026-000812” a fost folosită deja pentru alt document. Folosiți o cheie nouă pentru fiecare document.” |\n| 422 | `DOCAI_EXTERNAL_REF_FILE_IN_FLIGHT` | Același fișier se citește chiar acum sub altă recunoaștere. Retrimite-l cu aceeași cheie după ce aceea se termină: primești o copie gratuită, sub cheia ta. |\n| 422 | `DOCAI_DISABLED`, `DOCAI_API_KEY_MISSING` | Recunoașterea e oprită pe platformă. Reîncearcă mai târziu. |\n| 503 | `SERVICE_UNAVAILABLE` | Prea multe trimiteri simultane sau serviciul nu răspunde. Retrimite cu aceeași `externalRef` — nu se plătește de două ori. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Fiecare fișier **nou** consumă o recunoaștere: primele 3 sunt gratuite, apoi doar cât prevede contractul comerciantului cu PosFix (fără plafon în contract = nelimitat). Același fișier trimis din nou nu se mai plătește, dar fără `externalRef` devine o recunoaștere nouă (copie), cu id și webhook proprii — trimite mereu o cheie.\n- Trimiterea **nu se reia automat** la noi: la un timeout sau `503`, retrimite tu, cu aceeași `externalRef`.\n- Cel mult două trimiteri simultane pe cheie; restul așteaptă la rând. Nu trimite zeci de fișiere în paralel — coada le citește oricum câte două.",
            "body": {
              "mode": "formdata",
              "formdata": [
                {
                  "key": "file",
                  "type": "file",
                  "src": [],
                  "description": "PDF (≤ 12 MB) sau fotografie JPEG/PNG/WebP/GIF (≤ 20 MB)."
                },
                {
                  "key": "documentKind",
                  "type": "text",
                  "value": "supplier_invoice",
                  "description": "supplier_invoice (implicit) | sales_invoice"
                },
                {
                  "key": "externalRef",
                  "type": "text",
                  "value": "INV-2026-000812",
                  "description": "Cheia ta de idempotență, ≤ 100 de caractere."
                }
              ]
            }
          },
          "response": [
            {
              "name": "202",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/docai/recognitions",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "docai",
                    "recognitions"
                  ]
                },
                "body": {
                  "mode": "formdata",
                  "formdata": [
                    {
                      "key": "file",
                      "type": "file",
                      "src": [],
                      "description": "PDF (≤ 12 MB) sau fotografie JPEG/PNG/WebP/GIF (≤ 20 MB)."
                    },
                    {
                      "key": "documentKind",
                      "type": "text",
                      "value": "supplier_invoice",
                      "description": "supplier_invoice (implicit) | sales_invoice"
                    },
                    {
                      "key": "externalRef",
                      "type": "text",
                      "value": "INV-2026-000812",
                      "description": "Cheia ta de idempotență, ≤ 100 de caractere."
                    }
                  ]
                }
              },
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"id\": \"6a1f3c9e-2b4d-4e8f-a1c3-5d7e9f0b2a46\",\n  \"status\": \"queued\",\n  \"externalRef\": \"INV-2026-000812\",\n  \"existing\": false,\n  \"duplicate\": false\n}"
            },
            {
              "name": "200 existentă",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/docai/recognitions",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "docai",
                    "recognitions"
                  ]
                },
                "body": {
                  "mode": "formdata",
                  "formdata": [
                    {
                      "key": "file",
                      "type": "file",
                      "src": [],
                      "description": "PDF (≤ 12 MB) sau fotografie JPEG/PNG/WebP/GIF (≤ 20 MB)."
                    },
                    {
                      "key": "documentKind",
                      "type": "text",
                      "value": "supplier_invoice",
                      "description": "supplier_invoice (implicit) | sales_invoice"
                    },
                    {
                      "key": "externalRef",
                      "type": "text",
                      "value": "INV-2026-000812",
                      "description": "Cheia ta de idempotență, ≤ 100 de caractere."
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"id\": \"6a1f3c9e-2b4d-4e8f-a1c3-5d7e9f0b2a46\",\n  \"status\": \"completed\",\n  \"externalRef\": \"INV-2026-000812\",\n  \"existing\": true,\n  \"duplicate\": false\n}"
            },
            {
              "name": "402",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/docai/recognitions",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "docai",
                    "recognitions"
                  ]
                },
                "body": {
                  "mode": "formdata",
                  "formdata": [
                    {
                      "key": "file",
                      "type": "file",
                      "src": [],
                      "description": "PDF (≤ 12 MB) sau fotografie JPEG/PNG/WebP/GIF (≤ 20 MB)."
                    },
                    {
                      "key": "documentKind",
                      "type": "text",
                      "value": "supplier_invoice",
                      "description": "supplier_invoice (implicit) | sales_invoice"
                    },
                    {
                      "key": "externalRef",
                      "type": "text",
                      "value": "INV-2026-000812",
                      "description": "Cheia ta de idempotență, ≤ 100 de caractere."
                    }
                  ]
                }
              },
              "status": "Payment Required",
              "code": 402,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"type\": \"https://httpstatuses.io/402\",\n  \"title\": \"Payment Required\",\n  \"status\": 402,\n  \"detail\": \"Ați atins limita lunară din contract: 100 din 100 recunoașteri. Pentru mai multe, contactați PosFix ca să mărească limita din contract.\",\n  \"errorCode\": \"DOCAI_MONTHLY_LIMIT_REACHED\",\n  \"params\": {\n    \"used\": 100,\n    \"limit\": 100\n  }\n}"
            },
            {
              "name": "400 tip",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/docai/recognitions",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "docai",
                    "recognitions"
                  ]
                },
                "body": {
                  "mode": "formdata",
                  "formdata": [
                    {
                      "key": "file",
                      "type": "file",
                      "src": [],
                      "description": "PDF (≤ 12 MB) sau fotografie JPEG/PNG/WebP/GIF (≤ 20 MB)."
                    },
                    {
                      "key": "documentKind",
                      "type": "text",
                      "value": "supplier_invoice",
                      "description": "supplier_invoice (implicit) | sales_invoice"
                    },
                    {
                      "key": "externalRef",
                      "type": "text",
                      "value": "INV-2026-000812",
                      "description": "Cheia ta de idempotență, ≤ 100 de caractere."
                    }
                  ]
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"type\": \"https://httpstatuses.io/400\",\n  \"title\": \"DOCAI_FILE_TYPE_NOT_SUPPORTED\",\n  \"status\": 400,\n  \"detail\": \"Tipul de fișier „application/zip” nu este acceptat. Se acceptă JPEG, PNG, WebP, GIF și PDF.\",\n  \"errorCode\": \"DOCAI_FILE_TYPE_NOT_SUPPORTED\"\n}"
            }
          ]
        },
        {
          "name": "Recunoașteri — listă",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/docai/recognitions?status=completed&acknowledged=false&page=1&pageSize=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "docai",
                "recognitions"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "completed",
                  "description": "queued | processing | completed | failed"
                },
                {
                  "key": "acknowledged",
                  "value": "false",
                  "description": "false = doar cele încă nepreluate"
                },
                {
                  "key": "source",
                  "value": "",
                  "description": "api | control_panel",
                  "disabled": true
                },
                {
                  "key": "reviewed",
                  "value": "",
                  "description": "true = doar cele verificate de un om în panou",
                  "disabled": true
                },
                {
                  "key": "createdFrom",
                  "value": "",
                  "description": "ISO 8601, inclusiv",
                  "disabled": true
                },
                {
                  "key": "createdTo",
                  "value": "",
                  "description": "ISO 8601, exclusiv",
                  "disabled": true
                },
                {
                  "key": "externalRef",
                  "value": "",
                  "description": "Cheia ta",
                  "disabled": true
                },
                {
                  "key": "page",
                  "value": "1",
                  "description": "De la 1."
                },
                {
                  "key": "pageSize",
                  "value": "50",
                  "description": "1–200, implicit 50."
                }
              ]
            },
            "description": "**Scope necesar:** `docai:read`\n\nRecunoașterile comerciantului, **din orice sursă** — și cele trimise de tine, și cele făcute de oameni în panou —, cea mai veche întâi. Preluarea obișnuită: `status=completed&acknowledged=false`, detaliul fiecăreia pe id, apoi `POST …/ack` după ce ai importat-o.\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `items[].id`, `status`, `source`, `externalRef`, `fileName` |  | Ca la detaliu. |\n| `items[].reviewed` | boolean | Un om a verificat-o în panou. |\n| `items[].acknowledgedAt` | string? | Când ai confirmat preluarea. |\n| `items[].supplierName`, `supplierIdno`, `documentSeries`, `documentNumber`, `issueDate`, `total`, `currency`, `lineCount` |  | Rezumatul facturii, pentru afișare; datele de import sunt în detaliu. |\n| `totalCount`, `page`, `pageSize`, `totalPages`, `hasNextPage` |  | Paginarea. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `COMMON_INVALID_REQUEST_FIELD`, `COMMON_BETWEEN` | Valoare de filtru necunoscută, pagină în afara intervalului. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Ca inbox (`acknowledged=false`), citește **mereu pagina 1**: după fiecare `ack` lista se scurtează, iar o recunoaștere terminată mai târziu poate avea o dată de creare mai veche decât cele deja citite. Cu `page++` ai sări peste facturi."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/docai/recognitions?status=completed&acknowledged=false&page=1&pageSize=50",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "docai",
                    "recognitions"
                  ],
                  "query": [
                    {
                      "key": "status",
                      "value": "completed",
                      "description": "queued | processing | completed | failed"
                    },
                    {
                      "key": "acknowledged",
                      "value": "false",
                      "description": "false = doar cele încă nepreluate"
                    },
                    {
                      "key": "source",
                      "value": "",
                      "description": "api | control_panel",
                      "disabled": true
                    },
                    {
                      "key": "reviewed",
                      "value": "",
                      "description": "true = doar cele verificate de un om în panou",
                      "disabled": true
                    },
                    {
                      "key": "createdFrom",
                      "value": "",
                      "description": "ISO 8601, inclusiv",
                      "disabled": true
                    },
                    {
                      "key": "createdTo",
                      "value": "",
                      "description": "ISO 8601, exclusiv",
                      "disabled": true
                    },
                    {
                      "key": "externalRef",
                      "value": "",
                      "description": "Cheia ta",
                      "disabled": true
                    },
                    {
                      "key": "page",
                      "value": "1",
                      "description": "De la 1."
                    },
                    {
                      "key": "pageSize",
                      "value": "50",
                      "description": "1–200, implicit 50."
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items\": [\n    {\n      \"id\": \"6a1f3c9e-2b4d-4e8f-a1c3-5d7e9f0b2a46\",\n      \"status\": \"completed\",\n      \"source\": \"api\",\n      \"externalRef\": \"INV-2026-000812\",\n      \"fileName\": \"lactis-1791127.pdf\",\n      \"documentKind\": \"supplier_invoice\",\n      \"createdAt\": \"2026-09-28T09:14:02Z\",\n      \"completedAt\": \"2026-09-28T09:14:41Z\",\n      \"reviewed\": false,\n      \"acknowledgedAt\": null,\n      \"supplierName\": \"Lactis SRL\",\n      \"supplierIdno\": \"1003600012345\",\n      \"documentSeries\": \"AAY\",\n      \"documentNumber\": \"1791127\",\n      \"issueDate\": \"2026-09-21\",\n      \"total\": 410.31,\n      \"currency\": \"MDL\",\n      \"lineCount\": 1\n    }\n  ],\n  \"totalCount\": 1,\n  \"page\": 1,\n  \"pageSize\": 50,\n  \"totalPages\": 1,\n  \"hasNextPage\": false,\n  \"hasPreviousPage\": false,\n  \"summary\": null,\n  \"filterableColumns\": null,\n  \"ignoredFilters\": null\n}"
            }
          ]
        },
        {
          "name": "Recunoaștere — detaliu",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/docai/recognitions/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "docai",
                "recognitions",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "6a1f3c9e-2b4d-4e8f-a1c3-5d7e9f0b2a46",
                  "description": "Id-ul recunoașterii."
                }
              ]
            },
            "description": "**Scope necesar:** `docai:read`\n\nStarea și, când e gata, factura citită. Dacă un om a verificat-o în panou (`reviewed: true`), `invoice` e varianta lui, iar pe linii `match` e poziția aleasă de el. Altfel, pe fiecare linie vin `suggestions` — propunerile pe nomenclatorul tău, cea mai sigură întâi.\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `status` | string | `queued` \\| `processing` \\| `completed` \\| `failed`. |\n| `error` | object? | Pe `failed`: `code` (stabil) și `message` (text, în română). |\n| `confidence` | number? | 0..1, din verificări aritmetice (linii, totaluri, IDNO), nu din model. Sub 0,8 merită o privire. |\n| `duplicateOf` | string? | Id-ul recunoașterii din care s-a copiat rezultatul. |\n| `invoice` | object? | Doar pe `completed`: antet, `supplier`/`buyer`/`carrier`, `lines[]`, `totals`. |\n| `invoice.lines[].match` | object? | `{type, id, name, unit, nomenclatureType}`; `type` = `external` (`id` = codul tău sau `DX-…` pentru o poziție creată în PosFix) sau `platform`. |\n| `invoice.lines[].suggestions[]` | array | `{externalId, name, unit, score, method, unitMismatch}`; `method`: `learned` (confirmată deja la același furnizor), `barcode`, `name`. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 404 | — | Nu există în datele comerciantului. Corp gol. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- O recunoaștere `failed` cu `DOCAI_RESPONSE_TRUNCATED` e o factură prea lungă pentru bugetul curent; nu importa nimic din ea.\n- Câmpurile pe care modelul nu le-a putut citi vin `null` — nu le înlocui cu zero."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/docai/recognitions/:id",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "docai",
                    "recognitions",
                    ":id"
                  ],
                  "variable": [
                    {
                      "key": "id",
                      "value": "6a1f3c9e-2b4d-4e8f-a1c3-5d7e9f0b2a46",
                      "description": "Id-ul recunoașterii."
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"id\": \"6a1f3c9e-2b4d-4e8f-a1c3-5d7e9f0b2a46\",\n  \"status\": \"completed\",\n  \"source\": \"api\",\n  \"externalRef\": \"INV-2026-000812\",\n  \"fileName\": \"lactis-1791127.pdf\",\n  \"documentKind\": \"supplier_invoice\",\n  \"createdAt\": \"2026-09-28T09:14:02Z\",\n  \"completedAt\": \"2026-09-28T09:14:41Z\",\n  \"reviewed\": false,\n  \"reviewedAt\": null,\n  \"acknowledgedAt\": null,\n  \"confidence\": 0.96,\n  \"duplicateOf\": null,\n  \"error\": null,\n  \"invoice\": {\n    \"documentType\": \"Factură fiscală\",\n    \"series\": \"AAY\",\n    \"number\": \"1791127\",\n    \"issueDate\": \"2026-09-21\",\n    \"deliveryDate\": \"2026-09-21\",\n    \"currency\": \"MDL\",\n    \"waybillNumber\": null,\n    \"supplier\": {\n      \"name\": \"Lactis SRL\",\n      \"idno\": \"1003600012345\",\n      \"vatCode\": \"0401234\",\n      \"address\": \"mun. Chișinău, str. Uzinelor 1\",\n      \"iban\": \"MD24AG000000022512345678\",\n      \"bank\": \"Moldova Agroindbank\",\n      \"bankCode\": \"AGRNMD2X\"\n    },\n    \"buyer\": {\n      \"name\": \"Aroma Corner SRL\",\n      \"idno\": \"1015600098765\",\n      \"vatCode\": null,\n      \"address\": null,\n      \"iban\": null,\n      \"bank\": null,\n      \"bankCode\": null\n    },\n    \"carrier\": null,\n    \"lines\": [\n      {\n        \"index\": 0,\n        \"barcode\": \"4840811000181\",\n        \"tariffCode\": null,\n        \"name\": \"Lapte 2,5% 1L\",\n        \"unit\": \"buc\",\n        \"quantity\": 24,\n        \"unitPriceExclVat\": 15.83,\n        \"amountExclVat\": 379.92,\n        \"vatRate\": 8,\n        \"vatAmount\": 30.39,\n        \"amountInclVat\": 410.31,\n        \"match\": null,\n        \"suggestions\": [\n          {\n            \"externalId\": \"LAC-0012\",\n            \"name\": \"Lapte Lactis 2,5% 1l\",\n            \"unit\": \"buc\",\n            \"score\": 100,\n            \"method\": \"barcode\",\n            \"unitMismatch\": false\n          },\n          {\n            \"externalId\": \"LAC-0013\",\n            \"name\": \"Lapte Lactis 3,2% 1l\",\n            \"unit\": \"buc\",\n            \"score\": 67,\n            \"method\": \"name\",\n            \"unitMismatch\": false\n          }\n        ]\n      }\n    ],\n    \"totals\": {\n      \"amountExclVat\": 379.92,\n      \"vatAmount\": 30.39,\n      \"total\": 410.31\n    },\n    \"notes\": null\n  }\n}"
            }
          ]
        },
        {
          "name": "Recunoaștere — confirmă preluarea",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/docai/recognitions/:id/ack",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "docai",
                "recognitions",
                ":id",
                "ack"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "6a1f3c9e-2b4d-4e8f-a1c3-5d7e9f0b2a46",
                  "description": "Id-ul recunoașterii."
                }
              ]
            },
            "description": "**Scope necesar:** `docai:read`\n\nMarchează factura ca preluată de sistemul tău: lista `acknowledged=false` n-o mai arată. Idempotent — a doua confirmare păstrează data primei. Confirmarea e a comerciantului, nu a cheii: două sisteme care preiau aceleași facturi trebuie să se înțeleagă între ele.\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 422 | `DOCAI_ACK_NOT_READY` | „Recunoașterea nu s-a terminat încă. Confirmați preluarea după ce ajunge la „completed” sau „failed”.” |\n| 409 | `CONCURRENCY_CONFLICT` | Factura a fost modificată în același moment (verificare în panou). Repetă cererea. |\n| 404 | — | Nu există în datele comerciantului. Corp gol. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut sau modulul „API Integrare” nu e activ. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |"
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/docai/recognitions/:id/ack",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "docai",
                    "recognitions",
                    ":id",
                    "ack"
                  ],
                  "variable": [
                    {
                      "key": "id",
                      "value": "6a1f3c9e-2b4d-4e8f-a1c3-5d7e9f0b2a46",
                      "description": "Id-ul recunoașterii."
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"id\": \"6a1f3c9e-2b4d-4e8f-a1c3-5d7e9f0b2a46\",\n  \"status\": \"completed\",\n  \"source\": \"api\",\n  \"externalRef\": \"INV-2026-000812\",\n  \"fileName\": \"lactis-1791127.pdf\",\n  \"documentKind\": \"supplier_invoice\",\n  \"createdAt\": \"2026-09-28T09:14:02Z\",\n  \"completedAt\": \"2026-09-28T09:14:41Z\",\n  \"reviewed\": false,\n  \"acknowledgedAt\": \"2026-09-28T09:20:00Z\",\n  \"supplierName\": \"Lactis SRL\",\n  \"supplierIdno\": \"1003600012345\",\n  \"documentSeries\": \"AAY\",\n  \"documentNumber\": \"1791127\",\n  \"issueDate\": \"2026-09-21\",\n  \"total\": 410.31,\n  \"currency\": \"MDL\",\n  \"lineCount\": 1\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "8. Fidelitate",
      "description": "Programul de fidelitate al comerciantului, din sistemul tău: înscrii clienți, le citești soldul, le acorzi puncte sau bonus pe comenzile plătite, îi lași să-și folosească bonusul în lei pe comenzi și le retragi la retur. Cere scope-urile `loyalty:*` **și** modulul „Programe de fidelitate” activ la comerciant. Membrul se identifică după numărul cardului (13 cifre).\n\nFiecare program are un tip (`mechanic`): **puncte** (`points`), **ștampile** (`stamps`) sau **bonus în lei** (`money`). Regulile lui le setează comerciantul în panou; tu trimiți doar suma plătită, iar platforma calculează ce primește membrul, la fel ca la casă. Tipurile și stările sunt explicate la „Programe”.\n\n### Două feluri de a lucra cu fidelitatea dintr-un magazin online\n\nAlegi unul dintre ele pentru o comandă, nu pe amândouă: altfel membrul primește de două ori.\n\n| | **A. Magazinul își ține comenzile** | **B. Comenzile trec prin PosFix** |\n|---|---|---|\n| Unde stă comanda | În sistemul tău; PosFix nu o vede. | În PosFix, ca orice comandă de client („4. Comenzi”). |\n| Ce chemi | Cererile din acest dosar: cotare, folosire de bonus, acordare, anulare. | Doar „Creează comandă” cu `loyalty: {cardNumber, bonusAmount}` și, la nevoie, „Anulează comanda”. |\n| Bonusul folosit | Se ia de pe card pe loc, la „Folosește bonus pe o comandă”. | Se rezervă la creare și se ia la încheierea comenzii, doar pe marfa livrată. |\n| Punctele și cashback-ul | Le acorzi tu, după plată („Acordă pe o comandă”). | Le acordă PosFix la încheiere, pe valoarea livrată. |\n| Plata căzută, clientul renunță | Anulezi comanda fără sumă („Anulează o comandă (retur)”). | „Anulează comanda” din „4. Comenzi”: bonusul revine singur. |\n| Returul | Anulezi partea returnată, cu sumă. | Îl face comerciantul, în panou. |\n| Clientul în PosFix | Membrul rămâne doar în fidelitate. | Membrul se leagă de clientul comenzii; comenzile următoare merg pe același client, chiar dacă își schimbă telefonul. |\n| Scope-uri | `loyalty:read`, `loyalty:redeem`, `loyalty:earn`. | `orders:write`, `loyalty:earn` și, cu bonus, `loyalty:redeem`. |\n\nÎn ambele, clientul se identifică după numărul cardului, pe care îl primești la „Înscrie un membru” și îl păstrezi lângă clientul tău. Telefonul servește doar la prima potrivire.\n\n### Varianta A, pas cu pas\n\nPentru magazinul care își ține singur comenzile și plățile și cheamă direct cererile de mai jos:\n\n1. **Clientul se identifică.** Dacă n-are card, îl înscrii („Înscrie un membru”) și păstrezi `cardNumber` lângă clientul tău. Dacă are, îl găsești după card („Membru după card”) sau după telefon („Membri — listă”, cu `search`).\n2. **Coșul.** Arăți cât bonus poate folosi („Cât bonus se poate folosi”, cu suma comenzii). Cererea nu scade nimic.\n3. **Plasarea comenzii.** Dacă clientul folosește bonus, îl scazi („Folosește bonus pe o comandă”, cu `orderId` al comenzii tale). Bonusul dispare de pe card pe loc, pentru toate canalele: nu se mai poate cheltui și la casă.\n4. **Plata a reușit.** Acorzi comanda („Acordă pe o comandă”) pe suma plătită în bani, adică fără partea plătită cu bonus.\n5. **Plata a căzut sau clientul renunță.** Anulezi comanda fără sumă („Anulează o comandă (retur)”): bonusul revine întreg. Comanda anulată nu mai folosește bonus; o nouă încercare de plată e o comandă nouă, cu alt `orderId`.\n6. **Returul.** Anulezi partea returnată, cu `amount`: se retrage ce a primit membrul pe ea și revine partea de bonus folosită pe ea.\n\nCererile care schimbă soldul sunt idempotente: la un timeout sau la `503` le retrimiți cu aceeași cheie (`orderId`, respectiv `reversalId`), iar operația se face o singură dată. La `409` retrimiți la fel: altă cerere pe același card a trecut în aceeași clipă, iar răspunsul nou o ia în calcul.",
      "item": [
        {
          "name": "Programe",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/loyalty/programs",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "loyalty",
                "programs"
              ]
            },
            "description": "**Scope necesar:** `loyalty:read`\n\nProgramele de fidelitate ale comerciantului. De aici iei `programId` pentru înscriere. Regulile (câte puncte pe leu, procentul de bonus, treptele, recompensele) le setează comerciantul în panou și nu vin în răspuns: tu trimiți suma plătită, platforma calculează, iar răspunsul acordării spune ce a primit membrul.\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `items[].id` | string (uuid) | Id-ul programului. |\n| `items[].name` | string | Denumirea. |\n| `items[].mechanic` | string | Tipul programului — ce strânge membrul: `points` (puncte), `stamps` (ștampile) sau `money` (bonus în lei). Ce înseamnă fiecare: primul tabel de mai jos. |\n| `items[].status` | string | Starea programului: `active`, `draft`, `paused` sau `closed`. Ce poți face în fiecare: al doilea tabel de mai jos. |\n| `items[].currency` | string | Valuta programului, `MDL`: a sumelor trimise la acordare și a bonusului în lei. |\n| `items[].acceptsEnrollment` | boolean | `true` cât timp programul nu e `closed` — se poate înscrie și într-un program `draft` sau `paused`. |\n| `items[].hasBonus` | boolean | `true` = membrii au portofel în lei (câmpul `bonus` la membru). Mereu `true` la `money`; la `points` — când comerciantul a adăugat și bonus în lei, iar membrul strânge atunci puncte **și** lei. |\n\n### Tipul programului (`mechanic`)\n\n| Valoare | Ce strânge membrul | Cât primește la o comandă |\n|---|---|---|\n| `points` | Puncte, pe care le schimbă pe recompense la casă. | Suma plătită × punctele pe leu ale programului × coeficientul treptei, rotunjit în jos. La un punct pentru fiecare 10 lei, comanda de 749,90 lei aduce 74 de puncte. |\n| `stamps` | Ștampile, pe care le schimbă pe o recompensă („a 10-a cafea gratuită”). | Același număr de ștampile la fiecare comandă, oricât ar fi suma — de obicei una. |\n| `money` | Bonus în lei, într-un portofel; îl cheltuie la plată. | Un procent din suma plătită (cashback), rotunjit în jos la ban. La 5%, comanda de 749,90 lei aduce 37,49 lei. |\n\n### Starea programului (`status`)\n\n| Valoare | Ce înseamnă | Înscriere | Acordare |\n|---|---|---|---|\n| `active` | Programul funcționează. | da | da, după reguli |\n| `draft` | Creat, încă nelansat de comerciant. | da | răspunde `201`, dar cu 0 puncte, 0 ștampile, 0 lei; se numără doar cumpărătura |\n| `paused` | Oprit de comerciant („Dezactivează” în panou); se poate reporni. | da | ca la `draft` |\n| `closed` | Închis definitiv. | nu, `422` | nu, `422`; anularea unei acordări vechi merge |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 403 | — | Cheii îi lipsește scope-ul cerut, sau comerciantul nu are activ modulul „API Integrare” ori modulul „Programe de fidelitate”. Corp gol. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- O comandă trimisă cât programul e `draft` sau `paused` rămâne fără puncte și după repornire: retrimiterea ei e o repetare (`duplicate: true`), nu o acordare nouă."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/programs",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "programs"
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items\": [\n    {\n      \"id\": \"6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f\",\n      \"name\": \"Aroma Club\",\n      \"mechanic\": \"points\",\n      \"status\": \"active\",\n      \"currency\": \"MDL\",\n      \"acceptsEnrollment\": true,\n      \"hasBonus\": false\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Înscrie un membru",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/loyalty/members",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "loyalty",
                "members"
              ]
            },
            "description": "**Scope necesar:** `loyalty:write`\n\nÎnscrie un client în program și întoarce membrul, cu numărul cardului și linkurile de salvare în telefon. Telefonul e cheia: dacă e deja membru al programului, primești membrul existent, neschimbat.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `programId` | string (uuid) | da | Un program din „Programe”. |\n| `firstName`, `lastName` | string | da | Cel mult 100 de caractere fiecare. |\n| `phone` | string | da | 8–15 cifre, cu sau fără `+` în față, fără spații: `+37369123456`. Se păstrează și se întoarce doar cu cifre (`37369123456`). Nu se mai poate schimba după înscriere. |\n| `email` | string | nu | Cel mult 200 de caractere. |\n\n### Răspuns\n\n**201** — membru nou (`created: true`). **200** — telefonul era deja în program (`created: false`).\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `created` | boolean | `false` = membrul exista. |\n| `member` | object | Membrul, ca la „Membru după card”. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `LOYALTY_MEMBER_PHONE_FORMAT`, validare pe câmp | „Telefonul trebuie scris în format internațional (+37369123456) sau național, din 8–15 cifre.” |\n| 422 | `LOYALTY_PROGRAM_NOT_FOUND` | „Programul de fidelitate nu a fost găsit. Poate a fost șters între timp; reîncărcați pagina.” — `programId` nu e un program al comerciantului. |\n| 422 | `LOYALTY_PROGRAM_CLOSED` | „Programul de fidelitate „Aroma Club” este închis — nu se mai pot înscrie membri noi.” |\n| 422 | `DUPLICATE_MEMBER_EMAIL` | „Un membru cu această adresă de e-mail există deja în program.” |\n| 403 | — | Cheii îi lipsește scope-ul cerut, sau comerciantul nu are activ modulul „API Integrare” ori modulul „Programe de fidelitate”. Corp gol. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Membrul nou declanșează webhook-ul `loyalty.member.enrolled` — la fel și cel înscris la casă sau în panou.\n- Dacă programul dă bonus de bun venit, pleacă și un `loyalty.balance.changed`, care poate sosi înaintea lui `loyalty.member.enrolled`: ordinea livrărilor nu e garantată.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"programId\": \"6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f\",\n  \"firstName\": \"Ana\",\n  \"lastName\": \"Rusu\",\n  \"phone\": \"+37369123456\",\n  \"email\": \"ana.rusu@example.md\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"programId\": \"6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f\",\n  \"firstName\": \"Ana\",\n  \"lastName\": \"Rusu\",\n  \"phone\": \"+37369123456\",\n  \"email\": \"ana.rusu@example.md\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"created\": true,\n  \"member\": {\n    \"cardNumber\": \"2000000012345\",\n    \"programId\": \"6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f\",\n    \"programName\": \"Aroma Club\",\n    \"firstName\": \"Ana\",\n    \"lastName\": \"Rusu\",\n    \"phone\": \"37369123456\",\n    \"email\": \"ana.rusu@example.md\",\n    \"tier\": null,\n    \"balance\": {\n      \"unit\": \"points\",\n      \"value\": 0,\n      \"lifetime\": 0\n    },\n    \"bonus\": null,\n    \"totalSpent\": 0,\n    \"purchaseCount\": 0,\n    \"enrolledAt\": \"2026-09-01T08:00:00Z\",\n    \"lastActivityAt\": null,\n    \"wallet\": {\n      \"googleSaveUrl\": \"https://pay.google.com/gp/v/save/eyJhbGciOiJSUzI1NiJ9…\",\n      \"applePassUrl\": \"https://cp.posfix.md/loyalty-api/api/v1/loyalty/public/members/3e5f9a1c-…/apple-pass\"\n    }\n  }\n}"
            },
            {
              "name": "200 existent",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"programId\": \"6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f\",\n  \"firstName\": \"Ana\",\n  \"lastName\": \"Rusu\",\n  \"phone\": \"+37369123456\",\n  \"email\": \"ana.rusu@example.md\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"created\": false,\n  \"member\": {\n    \"cardNumber\": \"2000000012345\",\n    \"programId\": \"6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f\",\n    \"programName\": \"Aroma Club\",\n    \"firstName\": \"Ana\",\n    \"lastName\": \"Rusu\",\n    \"phone\": \"37369123456\",\n    \"email\": \"ana.rusu@example.md\",\n    \"tier\": \"Argint\",\n    \"balance\": {\n      \"unit\": \"points\",\n      \"value\": 120,\n      \"lifetime\": 340\n    },\n    \"bonus\": null,\n    \"totalSpent\": 2450.5,\n    \"purchaseCount\": 7,\n    \"enrolledAt\": \"2026-09-01T08:00:00Z\",\n    \"lastActivityAt\": \"2026-10-27T15:42:10Z\",\n    \"wallet\": {\n      \"googleSaveUrl\": \"https://pay.google.com/gp/v/save/eyJhbGciOiJSUzI1NiJ9…\",\n      \"applePassUrl\": \"https://cp.posfix.md/loyalty-api/api/v1/loyalty/public/members/3e5f9a1c-…/apple-pass\"\n    }\n  }\n}"
            },
            {
              "name": "400 telefon",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"programId\": \"6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f\",\n  \"firstName\": \"Ana\",\n  \"lastName\": \"Rusu\",\n  \"phone\": \"+37369123456\",\n  \"email\": \"ana.rusu@example.md\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"statusCode\": 400,\n  \"message\": \"One or more errors occurred!\",\n  \"errors\": {\n    \"phone\": [\n      \"Telefonul trebuie scris în format internațional (+37369123456) sau național, din 8–15 cifre.\"\n    ]\n  },\n  \"errorCodes\": {\n    \"phone\": [\n      {\n        \"code\": \"LOYALTY_MEMBER_PHONE_FORMAT\",\n        \"params\": {}\n      }\n    ]\n  }\n}"
            },
            {
              "name": "422 program închis",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"programId\": \"6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f\",\n  \"firstName\": \"Ana\",\n  \"lastName\": \"Rusu\",\n  \"phone\": \"+37369123456\",\n  \"email\": \"ana.rusu@example.md\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"type\": \"https://httpstatuses.io/422\",\n  \"title\": \"LOYALTY_PROGRAM_CLOSED\",\n  \"status\": 422,\n  \"detail\": \"Programul de fidelitate „Aroma Club” este închis — nu se mai pot înscrie membri noi.\",\n  \"errorCode\": \"LOYALTY_PROGRAM_CLOSED\"\n}"
            }
          ]
        },
        {
          "name": "Membru după card",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "loyalty",
                "members",
                ":cardNumber"
              ],
              "variable": [
                {
                  "key": "cardNumber",
                  "value": "2000000012345",
                  "description": "Numărul cardului, 13 cifre."
                }
              ]
            },
            "description": "**Scope necesar:** `loyalty:read`\n\nMembrul cu soldul la zi, bonusul în lei și linkurile cardului din telefon.\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `cardNumber` | string | Numărul cardului virtual, 13 cifre — identificatorul membrului în API. |\n| `programId`, `programName` | string | Programul în care e înscris membrul: id-ul din „Programe” și denumirea lui. |\n| `firstName`, `lastName`, `phone`, `email` |  | Datele membrului; `email` poate fi `null`. |\n| `tier` | string? | Treapta membrului (de exemplu „Argint”). Urcă singură, după punctele strânse de la înscriere (`balance.lifetime`), și poate aduce mai multe puncte pe leu sau o reducere la casă, după cum a setat comerciantul. `null` când programul n-are trepte sau membrul n-a ajuns la prima. |\n| `balance.unit` | string | Ce numără soldul, aceeași valoare ca `mechanic` al programului: `points` (puncte), `stamps` (ștampile) sau `money` (lei). |\n| `balance.value` | number | Soldul de acum: cât are membrul de cheltuit. La `money` e suma din portofel, în lei. Nu e niciodată negativ: la un retur se retrage cât mai e pe card, iar ce membrul cheltuise deja nu se mai cere. |\n| `balance.lifetime` | number | Cât a strâns membrul de la înscriere. Nu scade când cheltuie. La `points` și `stamps` scade când i se retrage o acordare la retur; la `money` e tot bonusul primit vreodată și nu scade nici la retur. |\n| `bonus` | object? | Portofelul în lei, separat de sold — doar când programul are bonus în lei (`hasBonus: true`), altfel `null`. La un program `money` are aceleași cifre ca `balance`. |\n| `bonus.amount` | number | Lei în portofel. Nu e niciodată negativ: bonusul retras la un retur, dacă fusese deja cheltuit, nu se mai cere. |\n| `bonus.available` | number | Cât poate cheltui membrul chiar acum: `amount` fără suma rezervată de o casă pe un bon în lucru și fără bonusul abia primit, încă blocat câteva ore (dacă programul blochează). |\n| `bonus.currency` | string | Valuta portofelului, `MDL`. |\n| `bonus.nextExpiresAt` | string? | Când expiră cel mai apropiat bonus primit (bonusul are termen de valabilitate, setat în program). `null` = nimic nu expiră. |\n| `totalSpent`, `purchaseCount` | number, integer | Cât a cumpărat cu cardul — la casă și prin comenzile acordate prin API, după anulări. |\n| `enrolledAt`, `lastActivityAt` | string | ISO 8601 UTC; `lastActivityAt` poate fi `null`. |\n| `wallet` | object? | `{googleSaveUrl, applePassUrl}` — linkurile de salvare a cardului în telefon. Vine doar la înscriere și la „Membru după card”; în listă și după modificare e `null`. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `INTEGRATIONS_LOYALTY_CARD_NUMBER_FORMAT` | „Numărul cardului are exact 13 cifre, fără spații.” |\n| 404 | — | Cardul nu e al comerciantului. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut, sau comerciantul nu are activ modulul „API Integrare” ori modulul „Programe de fidelitate”. Corp gol. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |"
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members",
                    ":cardNumber"
                  ],
                  "variable": [
                    {
                      "key": "cardNumber",
                      "value": "2000000012345",
                      "description": "Numărul cardului, 13 cifre."
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"cardNumber\": \"2000000012345\",\n  \"programId\": \"6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f\",\n  \"programName\": \"Aroma Club\",\n  \"firstName\": \"Ana\",\n  \"lastName\": \"Rusu\",\n  \"phone\": \"37369123456\",\n  \"email\": \"ana.rusu@example.md\",\n  \"tier\": \"Argint\",\n  \"balance\": {\n    \"unit\": \"points\",\n    \"value\": 120,\n    \"lifetime\": 340\n  },\n  \"bonus\": null,\n  \"totalSpent\": 2450.5,\n  \"purchaseCount\": 7,\n  \"enrolledAt\": \"2026-09-01T08:00:00Z\",\n  \"lastActivityAt\": \"2026-10-27T15:42:10Z\",\n  \"wallet\": {\n    \"googleSaveUrl\": \"https://pay.google.com/gp/v/save/eyJhbGciOiJSUzI1NiJ9…\",\n    \"applePassUrl\": \"https://cp.posfix.md/loyalty-api/api/v1/loyalty/public/members/3e5f9a1c-…/apple-pass\"\n  }\n}"
            },
            {
              "name": "400 card",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members",
                    ":cardNumber"
                  ],
                  "variable": [
                    {
                      "key": "cardNumber",
                      "value": "2000000012345",
                      "description": "Numărul cardului, 13 cifre."
                    }
                  ]
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"statusCode\": 400,\n  \"message\": \"One or more errors occurred!\",\n  \"errors\": {\n    \"cardNumber\": [\n      \"Numărul cardului are exact 13 cifre, fără spații.\"\n    ]\n  },\n  \"errorCodes\": {\n    \"cardNumber\": [\n      {\n        \"code\": \"INTEGRATIONS_LOYALTY_CARD_NUMBER_FORMAT\",\n        \"params\": {}\n      }\n    ]\n  }\n}"
            },
            {
              "name": "404",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members",
                    ":cardNumber"
                  ],
                  "variable": [
                    {
                      "key": "cardNumber",
                      "value": "2000000012345",
                      "description": "Numărul cardului, 13 cifre."
                    }
                  ]
                }
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "text",
              "header": [],
              "body": ""
            }
          ]
        },
        {
          "name": "Membri — listă",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/loyalty/members?limit=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "loyalty",
                "members"
              ],
              "query": [
                {
                  "key": "programId",
                  "value": "",
                  "description": "Doar membrii unui program; gol = toate.",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "",
                  "description": "Telefon, nume sau număr de card.",
                  "disabled": true
                },
                {
                  "key": "cursor",
                  "value": "",
                  "description": "nextCursor din pagina precedentă.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "50",
                  "description": "1–200, implicit 50."
                }
              ]
            },
            "description": "**Scope necesar:** `loyalty:read`\n\nMembrii comerciantului, paginați cu cursor.\n\n### Parametri de interogare\n\n| Parametru | Tip | Implicit | Descriere |\n|---|---|---|---|\n| `programId` | string (uuid) | toate | Filtru pe program. |\n| `search` | string | — | Telefon, nume sau număr de card; cel mult 100 de caractere. |\n| `cursor` | string | — | Valoarea `nextCursor` din pagina precedentă. |\n| `limit` | integer | 50 | 1–200. |\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `items[]` | array | Membrii, cu aceleași câmpuri ca la „Membru după card”; `wallet` e `null`. |\n| `nextCursor` | string? | Cursorul paginii următoare; `null` pe ultima pagină. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `COMMON_BETWEEN`, `COMMON_MAX_LENGTH` | „Valoarea trebuie să fie între 1 și 200.” |\n| 403 | — | Cheii îi lipsește scope-ul cerut, sau comerciantul nu are activ modulul „API Integrare” ori modulul „Programe de fidelitate”. Corp gol. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Parcurge paginile până la `nextCursor: null`; cursorul nu se construiește de mână."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members?limit=50",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members"
                  ],
                  "query": [
                    {
                      "key": "programId",
                      "value": "",
                      "description": "Doar membrii unui program; gol = toate.",
                      "disabled": true
                    },
                    {
                      "key": "search",
                      "value": "",
                      "description": "Telefon, nume sau număr de card.",
                      "disabled": true
                    },
                    {
                      "key": "cursor",
                      "value": "",
                      "description": "nextCursor din pagina precedentă.",
                      "disabled": true
                    },
                    {
                      "key": "limit",
                      "value": "50",
                      "description": "1–200, implicit 50."
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items\": [\n    {\n      \"cardNumber\": \"2000000012345\",\n      \"programId\": \"6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f\",\n      \"programName\": \"Aroma Club\",\n      \"firstName\": \"Ana\",\n      \"lastName\": \"Rusu\",\n      \"phone\": \"37369123456\",\n      \"email\": \"ana.rusu@example.md\",\n      \"tier\": \"Argint\",\n      \"balance\": {\n        \"unit\": \"points\",\n        \"value\": 120,\n        \"lifetime\": 340\n      },\n      \"bonus\": null,\n      \"totalSpent\": 2450.5,\n      \"purchaseCount\": 7,\n      \"enrolledAt\": \"2026-09-01T08:00:00Z\",\n      \"lastActivityAt\": \"2026-10-27T15:42:10Z\"\n    }\n  ],\n  \"nextCursor\": null\n}"
            }
          ]
        },
        {
          "name": "Modifică un membru",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "loyalty",
                "members",
                ":cardNumber"
              ],
              "variable": [
                {
                  "key": "cardNumber",
                  "value": "2000000012345",
                  "description": "Numărul cardului, 13 cifre."
                }
              ]
            },
            "description": "**Scope necesar:** `loyalty:write`\n\nSchimbă numele sau emailul membrului. Trimite doar câmpurile de schimbat; cele lipsă rămân cum sunt. Telefonul nu se schimbă.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `firstName`, `lastName` | string | nu | Dacă le trimiți, nu pot fi goale; cel mult 100 de caractere. |\n| `email` | string | nu | Cel mult 200 de caractere. `\"\"` șterge emailul. |\n\n### Răspuns\n\n**200** — membrul după schimbare, ca la „Membru după card”, cu `wallet: null`.\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `INTEGRATIONS_LOYALTY_CARD_NUMBER_FORMAT`, validare pe câmp | Card greșit, nume gol, email invalid. |\n| 404 | — | Cardul nu e al comerciantului. Corp gol. |\n| 422 | `DUPLICATE_MEMBER_EMAIL` | „Un membru cu această adresă de e-mail există deja în program.” |\n| 403 | — | Cheii îi lipsește scope-ul cerut, sau comerciantul nu are activ modulul „API Integrare” ori modulul „Programe de fidelitate”. Corp gol. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"lastName\": \"Rusu-Ciobanu\",\n  \"email\": \"ana.ciobanu@example.md\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "PATCH",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members",
                    ":cardNumber"
                  ],
                  "variable": [
                    {
                      "key": "cardNumber",
                      "value": "2000000012345",
                      "description": "Numărul cardului, 13 cifre."
                    }
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"lastName\": \"Rusu-Ciobanu\",\n  \"email\": \"ana.ciobanu@example.md\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"cardNumber\": \"2000000012345\",\n  \"programId\": \"6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f\",\n  \"programName\": \"Aroma Club\",\n  \"firstName\": \"Ana\",\n  \"lastName\": \"Rusu-Ciobanu\",\n  \"phone\": \"37369123456\",\n  \"email\": \"ana.ciobanu@example.md\",\n  \"tier\": \"Argint\",\n  \"balance\": {\n    \"unit\": \"points\",\n    \"value\": 120,\n    \"lifetime\": 340\n  },\n  \"bonus\": null,\n  \"totalSpent\": 2450.5,\n  \"purchaseCount\": 7,\n  \"enrolledAt\": \"2026-09-01T08:00:00Z\",\n  \"lastActivityAt\": \"2026-10-27T15:42:10Z\",\n  \"wallet\": {\n    \"googleSaveUrl\": \"https://pay.google.com/gp/v/save/eyJhbGciOiJSUzI1NiJ9…\",\n    \"applePassUrl\": \"https://cp.posfix.md/loyalty-api/api/v1/loyalty/public/members/3e5f9a1c-…/apple-pass\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Istoricul membrului",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber/transactions?limit=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "loyalty",
                "members",
                ":cardNumber",
                "transactions"
              ],
              "query": [
                {
                  "key": "cursor",
                  "value": "",
                  "description": "nextCursor din pagina precedentă.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "50",
                  "description": "1–200, implicit 50."
                }
              ],
              "variable": [
                {
                  "key": "cardNumber",
                  "value": "2000000012345",
                  "description": "Numărul cardului, 13 cifre."
                }
              ]
            },
            "description": "**Scope necesar:** `loyalty:read`\n\nMișcările de pe card, cea mai nouă întâi: acumulări, cheltuiri, anulări, expirări, bonus în lei.\n\n### Răspuns 200\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `items[].id` | string | Id-ul mișcării. |\n| `items[].occurredAt` | string | ISO 8601 UTC. |\n| `items[].kind` | string | Ce s-a întâmplat; valorile sunt în tabelul de mai jos. Cele cu prefixul `bonus_` ating portofelul în lei; celelalte sunt cumpărăturile, returul și punctele sau ștampilele. |\n| `items[].source` | string | `pos` (bon la casă), `api` (comandă prin API), `panel` (un om, în panou), `system` (bun venit, expirare). |\n| `items[].points`, `stamps`, `bonus` | number? | Ce a mișcat rândul, cu semn; `null` pe ce nu atinge. Leii stau doar pe rândurile `bonus_`: la un program `money`, rândul `earn` sau `reversal` are toate trei `null` și poartă doar suma, iar bonusul vine pe un rând separat. |\n| `items[].purchaseAmount` | number? | Suma cumpărăturii (la `earn`) sau a returului (la `reversal`). |\n| `items[].orderId` | string? | Comanda ta, la mișcările făcute prin API. |\n| `items[].receiptRef` | string? | Codul bonului fiscal, la mișcările de la casă. |\n| `nextCursor` | string? | Cursorul paginii următoare; `null` pe ultima pagină. |\n\n### Felul mișcării (`kind`)\n\n| Valoare | Ce înseamnă |\n|---|---|\n| `earn` | O cumpărătură (bon la casă sau comandă prin API), cu punctele sau ștampilele primite pe ea. La `money` doar suma; bonusul e rândul `bonus_grant`. |\n| `reversal` | Un retur, cu punctele sau ștampilele retrase. La `money` doar suma; bonusul retras e rândul `bonus_revoke`. |\n| `redeem` | Puncte sau ștampile schimbate pe o recompensă. |\n| `adjustment` | Corecție făcută de un om în panou (plus sau minus). |\n| `expiry` | Puncte expirate. |\n| `bonus_grant` | Bonus în lei primit: cashback pe cumpărătură, bun venit, campanie. |\n| `bonus_redeem` | Bonus cheltuit la plată. |\n| `bonus_expiry` | Bonus expirat. |\n| `bonus_restore` | Bonus întors în portofel: bonul plătit cu bonus a fost returnat. |\n| `bonus_revoke` | Bonus retras: la un retur, sau corectat de un om în panou. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 404 | — | Cardul nu e al comerciantului. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut, sau comerciantul nu are activ modulul „API Integrare” ori modulul „Programe de fidelitate”. Corp gol. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |"
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber/transactions?limit=50",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members",
                    ":cardNumber",
                    "transactions"
                  ],
                  "query": [
                    {
                      "key": "cursor",
                      "value": "",
                      "description": "nextCursor din pagina precedentă.",
                      "disabled": true
                    },
                    {
                      "key": "limit",
                      "value": "50",
                      "description": "1–200, implicit 50."
                    }
                  ],
                  "variable": [
                    {
                      "key": "cardNumber",
                      "value": "2000000012345",
                      "description": "Numărul cardului, 13 cifre."
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items\": [\n    {\n      \"id\": \"a71f0c2e-5b3d-4e8a-9c1f-2d4e6f8a0b1c\",\n      \"occurredAt\": \"2026-10-27T15:42:10Z\",\n      \"kind\": \"earn\",\n      \"source\": \"api\",\n      \"points\": 74,\n      \"stamps\": null,\n      \"bonus\": null,\n      \"purchaseAmount\": 749.9,\n      \"orderId\": \"WEB-10482\",\n      \"receiptRef\": null\n    },\n    {\n      \"id\": \"c93e1a4b-7d5f-4a2c-8e0b-1f3a5c7e9d2b\",\n      \"occurredAt\": \"2026-10-20T11:05:33Z\",\n      \"kind\": \"earn\",\n      \"source\": \"pos\",\n      \"points\": 23,\n      \"stamps\": null,\n      \"bonus\": null,\n      \"purchaseAmount\": 231,\n      \"orderId\": null,\n      \"receiptRef\": \"J403001234-0067-0012\"\n    }\n  ],\n  \"nextCursor\": null\n}"
            }
          ]
        },
        {
          "name": "Cât bonus se poate folosi",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber/bonus-quote?orderAmount=749.90&otherDiscount=0",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "loyalty",
                "members",
                ":cardNumber",
                "bonus-quote"
              ],
              "query": [
                {
                  "key": "orderAmount",
                  "value": "749.90",
                  "description": "Suma comenzii înainte de bonus, în lei."
                },
                {
                  "key": "otherDiscount",
                  "value": "0",
                  "description": "Reducerea deja dată pe comandă din alte surse (cupon, promoție); lipsă = 0."
                }
              ],
              "variable": [
                {
                  "key": "cardNumber",
                  "value": "2000000012345",
                  "description": "Numărul cardului, 13 cifre."
                }
              ]
            },
            "description": "**Scope necesar:** `loyalty:read`\n\nCât bonus în lei poate folosi membrul pe o comandă, după regulile programului — aceleași ca la casă: plafonul pe comandă (procent sau sumă), suma minimă a comenzii, cumulul cu alte reduceri. Nu scade nimic; arată clientului în coș cât poate folosi.\n\n### Răspuns\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `cardNumber` | string | Membrul. |\n| `orderAmount` | number | Suma comenzii pe care s-a calculat cotarea. |\n| `available` | number | Cât e cheltuibil acum pe card. |\n| `maxRedeemable` | number | Cât se poate folosi pe comanda asta: disponibilul, plafonat de regulile programului. 0 = nimic. |\n| `currency` | string? | Valuta portofelului, `MDL`. |\n| `refusal` | object? | De ce nu se poate folosi bonusul: `{code, message}`. `null` când `maxRedeemable` e peste zero. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `INTEGRATIONS_LOYALTY_AMOUNT_INVALID`, validare pe câmp | „Suma trebuie să fie mai mare decât zero, cu cel mult două zecimale (de exemplu 249.90).” |\n| 404 | — | Cardul nu e al comerciantului. Corp gol. |\n| 403 | — | Cheii îi lipsește scope-ul cerut, sau comerciantul nu are activ modulul „API Integrare” ori modulul „Programe de fidelitate”. Corp gol. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Cotarea nu ține banii: între cotare și folosire clientul poate cheltui bonusul la casă. Suma finală o verifică „Folosește bonus pe o comandă”."
          },
          "response": [
            {
              "name": "200",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber/bonus-quote?orderAmount=749.90&otherDiscount=0",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members",
                    ":cardNumber",
                    "bonus-quote"
                  ],
                  "query": [
                    {
                      "key": "orderAmount",
                      "value": "749.90",
                      "description": "Suma comenzii înainte de bonus, în lei."
                    },
                    {
                      "key": "otherDiscount",
                      "value": "0",
                      "description": "Reducerea deja dată pe comandă din alte surse (cupon, promoție); lipsă = 0."
                    }
                  ],
                  "variable": [
                    {
                      "key": "cardNumber",
                      "value": "2000000012345",
                      "description": "Numărul cardului, 13 cifre."
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"cardNumber\": \"2000000012345\",\n  \"orderAmount\": 749.9,\n  \"available\": 150,\n  \"maxRedeemable\": 149.98,\n  \"currency\": \"MDL\",\n  \"refusal\": null\n}"
            },
            {
              "name": "200 fără bonus",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber/bonus-quote?orderAmount=749.90&otherDiscount=0",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members",
                    ":cardNumber",
                    "bonus-quote"
                  ],
                  "query": [
                    {
                      "key": "orderAmount",
                      "value": "749.90",
                      "description": "Suma comenzii înainte de bonus, în lei."
                    },
                    {
                      "key": "otherDiscount",
                      "value": "0",
                      "description": "Reducerea deja dată pe comandă din alte surse (cupon, promoție); lipsă = 0."
                    }
                  ],
                  "variable": [
                    {
                      "key": "cardNumber",
                      "value": "2000000012345",
                      "description": "Numărul cardului, 13 cifre."
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"cardNumber\": \"2000000012345\",\n  \"orderAmount\": 749.9,\n  \"available\": 0,\n  \"maxRedeemable\": 0,\n  \"currency\": \"MDL\",\n  \"refusal\": {\n    \"code\": \"LOYALTY_QUOTE_NOTHING_AVAILABLE\",\n    \"message\": \"Clientul nu are bonus disponibil acum.\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Folosește bonus pe o comandă",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber/redemptions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "loyalty",
                "members",
                ":cardNumber",
                "redemptions"
              ],
              "variable": [
                {
                  "key": "cardNumber",
                  "value": "2000000012345",
                  "description": "Numărul cardului, 13 cifre."
                }
              ]
            },
            "description": "**Scope necesar:** `loyalty:redeem`\n\nScade bonus în lei de pe cardul membrului pentru o comandă: clientul plătește `orderAmount` minus `amount`. Aceleași reguli ca la casă (plafon, minim, cumul), iar banii se scad pe loc, pentru toate canalele deodată — același bonus nu se poate cheltui și la casă, și pe site. Trimite-o când clientul plasează comanda.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `orderId` | string | da | Comanda la tine, cel mult 100 de caractere — cheia de idempotență. |\n| `amount` | number | da | Bonusul folosit, în lei: mai mare decât zero, cel mult două zecimale, cel mult `maxRedeemable` din cotare. |\n| `orderAmount` | number | da | Suma comenzii înainte de bonus, în lei. |\n| `otherDiscount` | number | nu | Reducerea deja dată din alte surse; lipsă = 0. Contează când programul nu cumulează bonusul cu alte reduceri. |\n\n### Răspuns\n\n**201** — bonus folosit acum. **200** — comanda își folosise deja bonusul (`duplicate: true`), nu s-a mișcat nimic.\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `cardNumber`, `orderId` | string | Membrul și comanda. |\n| `duplicate` | boolean | `true` = cererea fusese deja făcută; acum nu s-a mișcat nimic. |\n| `amount` | number | Bonusul scăzut de pe card, în lei. |\n| `orderAmount` | number | Suma comenzii înainte de bonus. |\n| `balance`, `bonusBalance` | object, object? | Soldul membrului după operație, ca la membru. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `INTEGRATIONS_LOYALTY_AMOUNT_INVALID`, `INTEGRATIONS_LOYALTY_CARD_NUMBER_FORMAT`, validare pe câmp | „Suma trebuie să fie mai mare decât zero, cu cel mult două zecimale (de exemplu 249.90).” |\n| 404 | — | Cardul nu e al comerciantului. Corp gol. |\n| 409 | `CONCURRENCY_CONFLICT` | Același card a cheltuit bonus în aceeași clipă pe alt canal, sau aceeași comandă a fost trimisă deodată de două ori. Retrimite cererea: vede soldul nou și primește răspunsul corect pentru comandă. |\n| 422 | `LOYALTY_BONUS_HOLD_EXCEEDS_MAX` | „Pe bonul acesta se pot folosi cel mult 149,98 MDL.” |\n| 422 | `LOYALTY_QUOTE_NOTHING_AVAILABLE` și celelalte motive ale cotării | „Clientul nu are bonus disponibil acum.” |\n| 422 | `LOYALTY_ORDER_REDEMPTION_AMOUNT_MISMATCH` | „Pe comanda WEB-10482 s-au folosit deja 100,00 de bonus. Pe aceeași comandă nu se poate folosi altă sumă — anulați comanda și folosiți bonusul pe o comandă nouă.” |\n| 422 | `LOYALTY_ORDER_REDEEMED_ON_OTHER_CARD` | „Pe comanda WEB-10482 s-a folosit deja bonusul altui card. Bonusul unei comenzi vine de pe un singur card.” |\n| 422 | `LOYALTY_ORDER_CANCELLED_NO_REDEMPTION` | „Comanda WEB-10482 a fost anulată, deci pe ea nu se mai poate folosi bonus. O nouă încercare de plată se trimite ca o comandă nouă, cu alt număr.” |\n| 403 | — | Cheii îi lipsește scope-ul cerut, sau comerciantul nu are activ modulul „API Integrare” ori modulul „Programe de fidelitate”. Corp gol. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- La un timeout sau `503`, retrimite cererea **cu același** `orderId` și aceeași sumă: bonusul se scade o singură dată.\n- Dacă plata cade sau clientul renunță, anulează comanda cu „Anulează o comandă (retur)” fără `amount`: bonusul revine întreg pe card. Bonusul nu revine singur.\n- O comandă anulată nu mai folosește bonus. O nouă încercare de plată se trimite ca o comandă nouă, cu alt `orderId`.\n- După plată, acordă comanda („Acordă pe o comandă”) pe suma plătită în bani, adică `orderAmount` minus bonusul.\n- Cererea e pentru varianta A (comanda stă la tine). Dacă trimiți comanda în PosFix, pune bonusul în `loyalty.bonusAmount` la „Creează comandă” și nu mai chema cererea asta.\n- Folosirea declanșează webhook-ul `loyalty.balance.changed`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"orderId\": \"WEB-10482\",\n  \"amount\": 100,\n  \"orderAmount\": 749.9\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber/redemptions",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members",
                    ":cardNumber",
                    "redemptions"
                  ],
                  "variable": [
                    {
                      "key": "cardNumber",
                      "value": "2000000012345",
                      "description": "Numărul cardului, 13 cifre."
                    }
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"orderId\": \"WEB-10482\",\n  \"amount\": 100,\n  \"orderAmount\": 749.9\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"cardNumber\": \"2000000012345\",\n  \"orderId\": \"WEB-10482\",\n  \"duplicate\": false,\n  \"amount\": 100,\n  \"orderAmount\": 749.9,\n  \"balance\": {\n    \"unit\": \"money\",\n    \"value\": 50,\n    \"lifetime\": 350\n  },\n  \"bonusBalance\": {\n    \"amount\": 50,\n    \"available\": 50,\n    \"currency\": \"MDL\",\n    \"nextExpiresAt\": \"2027-01-31T00:00:00Z\"\n  }\n}"
            },
            {
              "name": "200 repetată",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber/redemptions",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members",
                    ":cardNumber",
                    "redemptions"
                  ],
                  "variable": [
                    {
                      "key": "cardNumber",
                      "value": "2000000012345",
                      "description": "Numărul cardului, 13 cifre."
                    }
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"orderId\": \"WEB-10482\",\n  \"amount\": 100,\n  \"orderAmount\": 749.9\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"cardNumber\": \"2000000012345\",\n  \"orderId\": \"WEB-10482\",\n  \"duplicate\": true,\n  \"amount\": 100,\n  \"orderAmount\": 749.9,\n  \"balance\": {\n    \"unit\": \"money\",\n    \"value\": 50,\n    \"lifetime\": 350\n  },\n  \"bonusBalance\": {\n    \"amount\": 50,\n    \"available\": 50,\n    \"currency\": \"MDL\",\n    \"nextExpiresAt\": \"2027-01-31T00:00:00Z\"\n  }\n}"
            },
            {
              "name": "422 peste plafon",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber/redemptions",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members",
                    ":cardNumber",
                    "redemptions"
                  ],
                  "variable": [
                    {
                      "key": "cardNumber",
                      "value": "2000000012345",
                      "description": "Numărul cardului, 13 cifre."
                    }
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"orderId\": \"WEB-10482\",\n  \"amount\": 100,\n  \"orderAmount\": 749.9\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"type\": \"https://httpstatuses.io/422\",\n  \"title\": \"LOYALTY_BONUS_HOLD_EXCEEDS_MAX\",\n  \"status\": 422,\n  \"detail\": \"Pe bonul acesta se pot folosi cel mult 149,98 MDL.\",\n  \"errorCode\": \"LOYALTY_BONUS_HOLD_EXCEEDS_MAX\"\n}"
            }
          ]
        },
        {
          "name": "Acordă pe o comandă",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber/accruals",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "loyalty",
                "members",
                ":cardNumber",
                "accruals"
              ],
              "variable": [
                {
                  "key": "cardNumber",
                  "value": "2000000012345",
                  "description": "Numărul cardului, 13 cifre."
                }
              ]
            },
            "description": "**Scope necesar:** `loyalty:earn`\n\nAcordă membrului ce-i revine pentru o comandă plătită în sistemul tău (magazin online, aplicație), după regulile programului — aceleași ca la casă. Trimite-o după ce comanda e plătită.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `orderId` | string | da | Comanda la tine, cel mult 100 de caractere — cheia de idempotență. |\n| `amount` | number | da | Suma plătită, după reduceri, în lei: mai mare decât zero, cel mult două zecimale. |\n| `occurredAt` | string | nu | Momentul cumpărăturii, ISO 8601; lipsă = acum. |\n\n### Răspuns\n\n**201** — acordare nouă. **200** — comanda fusese deja acordată (`duplicate: true`), nu s-a mișcat nimic.\n\n| Câmp | Tip | Descriere |\n|---|---|---|\n| `cardNumber`, `orderId` | string | Membrul și comanda. |\n| `reversalId` | string? | Doar la anulare: referința returului. |\n| `duplicate` | boolean | `true` = operația fusese deja făcută cu aceeași cheie; acum nu s-a mișcat nimic. |\n| `amount` | number | Suma pe care s-a calculat operația: plătită la acordare, returnată la anulare. |\n| `points`, `stamps`, `bonus` | number | Ce a primit membrul pe comandă (negativ la anulare). Iese nenulă doar unitatea programului: `points` la puncte, `stamps` la ștampile, `bonus` (lei) la bonus în lei; la un program cu puncte care are și portofel — `points` și `bonus`. Toate 0 când programul e `draft` sau `paused`. |\n| `remainingAmount` | number | Cât din comandă a rămas neanulat după operație. |\n| `bonusRestored` | number | Doar la anulare: bonusul folosit pe comandă (prin „Folosește bonus pe o comandă”) pus la loc pe card acum, în lei. 0 la acordare. |\n| `balance`, `bonusBalance` | object, object? | Soldul membrului după operație, ca `balance` și `bonus` la membru; `bonusBalance` e `null` când programul n-are bonus în lei. |\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `INTEGRATIONS_LOYALTY_AMOUNT_INVALID`, `INTEGRATIONS_LOYALTY_CARD_NUMBER_FORMAT`, validare pe câmp | „Suma trebuie să fie mai mare decât zero, cu cel mult două zecimale (de exemplu 249.90).” |\n| 404 | — | Cardul nu e al comerciantului. Corp gol. |\n| 422 | `LOYALTY_ORDER_ACCRUAL_AMOUNT_MISMATCH` | „Comanda WEB-10482 a fost deja acordată pentru 749,90. Pe aceeași comandă nu se poate acorda altă sumă — pentru o corecție anulați comanda (integral sau parțial).” |\n| 422 | `LOYALTY_ORDER_ACCRUED_TO_OTHER_CARD` | „Comanda WEB-10482 a primit deja puncte pe alt card. O comandă se acordă o singură dată, unui singur membru.” |\n| 422 | `LOYALTY_PROGRAM_CLOSED_ACCRUAL` | „Programul de fidelitate „Aroma Club” este închis — nu se mai acordă puncte.” |\n| 403 | — | Cheii îi lipsește scope-ul cerut, sau comerciantul nu are activ modulul „API Integrare” ori modulul „Programe de fidelitate”. Corp gol. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- La un timeout sau `503`, retrimite cererea **cu același** `orderId`: se acordă o singură dată.\n- Comanda se acordă o singură dată, unui singur card, pentru o singură sumă. O corecție se face prin anulare (integrală sau parțială), nu printr-o a doua acordare.\n- Acordarea declanșează webhook-ul `loyalty.balance.changed`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"orderId\": \"WEB-10482\",\n  \"amount\": 749.9,\n  \"occurredAt\": \"2026-10-27T15:40:00Z\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber/accruals",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members",
                    ":cardNumber",
                    "accruals"
                  ],
                  "variable": [
                    {
                      "key": "cardNumber",
                      "value": "2000000012345",
                      "description": "Numărul cardului, 13 cifre."
                    }
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"orderId\": \"WEB-10482\",\n  \"amount\": 749.9,\n  \"occurredAt\": \"2026-10-27T15:40:00Z\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"cardNumber\": \"2000000012345\",\n  \"orderId\": \"WEB-10482\",\n  \"reversalId\": null,\n  \"duplicate\": false,\n  \"amount\": 749.9,\n  \"points\": 74,\n  \"stamps\": 0,\n  \"bonus\": 0,\n  \"remainingAmount\": 749.9,\n  \"bonusRestored\": 0,\n  \"balance\": {\n    \"unit\": \"points\",\n    \"value\": 194,\n    \"lifetime\": 414\n  },\n  \"bonusBalance\": null\n}"
            },
            {
              "name": "200 repetată",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber/accruals",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members",
                    ":cardNumber",
                    "accruals"
                  ],
                  "variable": [
                    {
                      "key": "cardNumber",
                      "value": "2000000012345",
                      "description": "Numărul cardului, 13 cifre."
                    }
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"orderId\": \"WEB-10482\",\n  \"amount\": 749.9,\n  \"occurredAt\": \"2026-10-27T15:40:00Z\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"cardNumber\": \"2000000012345\",\n  \"orderId\": \"WEB-10482\",\n  \"reversalId\": null,\n  \"duplicate\": true,\n  \"amount\": 749.9,\n  \"points\": 74,\n  \"stamps\": 0,\n  \"bonus\": 0,\n  \"remainingAmount\": 749.9,\n  \"bonusRestored\": 0,\n  \"balance\": {\n    \"unit\": \"points\",\n    \"value\": 194,\n    \"lifetime\": 414\n  },\n  \"bonusBalance\": null\n}"
            },
            {
              "name": "400 sumă",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber/accruals",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members",
                    ":cardNumber",
                    "accruals"
                  ],
                  "variable": [
                    {
                      "key": "cardNumber",
                      "value": "2000000012345",
                      "description": "Numărul cardului, 13 cifre."
                    }
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"orderId\": \"WEB-10482\",\n  \"amount\": 749.9,\n  \"occurredAt\": \"2026-10-27T15:40:00Z\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"statusCode\": 400,\n  \"message\": \"One or more errors occurred!\",\n  \"errors\": {\n    \"amount\": [\n      \"Suma trebuie să fie mai mare decât zero, cu cel mult două zecimale (de exemplu 249.90).\"\n    ]\n  },\n  \"errorCodes\": {\n    \"amount\": [\n      {\n        \"code\": \"INTEGRATIONS_LOYALTY_AMOUNT_INVALID\",\n        \"params\": {}\n      }\n    ]\n  }\n}"
            },
            {
              "name": "422 altă sumă",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/members/:cardNumber/accruals",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "members",
                    ":cardNumber",
                    "accruals"
                  ],
                  "variable": [
                    {
                      "key": "cardNumber",
                      "value": "2000000012345",
                      "description": "Numărul cardului, 13 cifre."
                    }
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"orderId\": \"WEB-10482\",\n  \"amount\": 749.9,\n  \"occurredAt\": \"2026-10-27T15:40:00Z\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"type\": \"https://httpstatuses.io/422\",\n  \"title\": \"LOYALTY_ORDER_ACCRUAL_AMOUNT_MISMATCH\",\n  \"status\": 422,\n  \"detail\": \"Comanda WEB-10482 a fost deja acordată pentru 749,90. Pe aceeași comandă nu se poate acorda altă sumă — pentru o corecție anulați comanda (integral sau parțial).\",\n  \"errorCode\": \"LOYALTY_ORDER_ACCRUAL_AMOUNT_MISMATCH\"\n}"
            }
          ]
        },
        {
          "name": "Anulează o comandă (retur)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/loyalty/accruals/:orderId/reversals",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "v1",
                "loyalty",
                "accruals",
                ":orderId",
                "reversals"
              ],
              "variable": [
                {
                  "key": "orderId",
                  "value": "WEB-10482",
                  "description": "Comanda acordată (URL-encoded)."
                }
              ]
            },
            "description": "**Scope necesar:** `loyalty:earn`\n\nRetrage din acordarea unei comenzi partea returnată, proporțional, și pune la loc pe card partea de bonus folosit pe comandă. Fără `amount` se anulează tot ce a rămas din comandă. O comandă care doar a folosit bonus (plata a căzut înainte de acordare) se anulează fără `amount`: bonusul revine întreg.\n\n### Corpul cererii\n\n| Câmp | Tip | Obligatoriu | Reguli |\n|---|---|---|---|\n| `reversalId` | string | da | Returul sau anularea la tine, cel mult 100 de caractere, fără două puncte (`:`) — cheia de idempotență. |\n| `amount` | number | nu | Suma returnată, în lei: mai mare decât zero, cel mult două zecimale, cel mult cât a rămas neanulat. |\n\n### Răspuns\n\n**201** — anulare nouă. **200** — anularea fusese deja făcută (`duplicate: true`).\n\nCâmpurile sunt ca la „Acordă pe o comandă”: ce se retrage vine negativ (unitatea programului), restul 0; `bonusRestored` e bonusul pus la loc.\n\n### Erori\n\n| HTTP | `errorCode` | Când |\n|---|---|---|\n| 400 | `INTEGRATIONS_LOYALTY_AMOUNT_INVALID`, validare pe câmp | Sumă greșită, `reversalId` lipsă. |\n| 400 | `INTEGRATIONS_LOYALTY_REFERENCE_COLON` | „Referința returului nu poate conține două puncte („:”). Folosește litere, cifre, cratimă sau bară.” |\n| 404 | — | Comanda n-a fost nici acordată, nici n-a folosit bonus la acest comerciant. Corp gol. |\n| 422 | `LOYALTY_ORDER_REVERSAL_FULL_ONLY_WITHOUT_ACCRUAL` | „Comanda WEB-10482 n-a primit acordare, doar a folosit bonus: anularea ei pune la loc tot bonusul, deci se trimite fără sumă.” |\n| 422 | `LOYALTY_REVERSAL_EXCEEDS_REMAINING` | „Din comanda WEB-10482 au rămas neanulați 500,00; nu se pot anula 600,00.” |\n| 422 | `LOYALTY_REVERSAL_AMOUNT_MISMATCH` | „Anularea RET-2231 a fost deja făcută pentru 249,90. Pentru o altă sumă folosiți o referință de anulare nouă.” |\n| 403 | — | Cheii îi lipsește scope-ul cerut, sau comerciantul nu are activ modulul „API Integrare” ori modulul „Programe de fidelitate”. Corp gol. |\n| 401 | — | Cheia lipsește, e greșită, revocată sau expirată. Corp gol. |\n| 429 | `RATE_LIMITED` | Prea multe cereri; vezi „Limite” în descrierea colecției. |\n| 503 | `SERVICE_UNAVAILABLE` | Un serviciu intern nu răspunde. Reîncearcă peste câteva secunde. |\n\n### De reținut\n\n- Retrimite cu același `reversalId` la un timeout: se anulează o singură dată. Un retur nou are nevoie de un `reversalId` nou.\n- Soldul nu coboară sub zero: se retrage cât mai e pe card. Ce membrul a cheltuit deja nu se mai cere — nici acum, nici din acordările următoare.\n- Bonusul folosit pe comandă revine în aceeași cotă ca suma întoarsă din cea plătită în bani: comandă 749,90, bonus 100, plătit 649,90 — un retur de 324,95 pune la loc 50.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reversalId\": \"RET-2231\",\n  \"amount\": 249.9\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/accruals/:orderId/reversals",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "accruals",
                    ":orderId",
                    "reversals"
                  ],
                  "variable": [
                    {
                      "key": "orderId",
                      "value": "WEB-10482",
                      "description": "Comanda acordată (URL-encoded)."
                    }
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"reversalId\": \"RET-2231\",\n  \"amount\": 249.9\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"cardNumber\": \"2000000012345\",\n  \"orderId\": \"WEB-10482\",\n  \"reversalId\": \"RET-2231\",\n  \"duplicate\": false,\n  \"amount\": 249.9,\n  \"points\": -24,\n  \"stamps\": 0,\n  \"bonus\": 0,\n  \"remainingAmount\": 500,\n  \"bonusRestored\": 0,\n  \"balance\": {\n    \"unit\": \"points\",\n    \"value\": 170,\n    \"lifetime\": 414\n  },\n  \"bonusBalance\": null\n}"
            },
            {
              "name": "422 peste rest",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/public/v1/loyalty/accruals/:orderId/reversals",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "v1",
                    "loyalty",
                    "accruals",
                    ":orderId",
                    "reversals"
                  ],
                  "variable": [
                    {
                      "key": "orderId",
                      "value": "WEB-10482",
                      "description": "Comanda acordată (URL-encoded)."
                    }
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"reversalId\": \"RET-2231\",\n  \"amount\": 249.9\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"type\": \"https://httpstatuses.io/422\",\n  \"title\": \"LOYALTY_REVERSAL_EXCEEDS_REMAINING\",\n  \"status\": 422,\n  \"detail\": \"Din comanda WEB-10482 au rămas neanulați 500,00; nu se pot anula 600,00.\",\n  \"errorCode\": \"LOYALTY_REVERSAL_EXCEEDS_REMAINING\"\n}"
            }
          ]
        }
      ]
    }
  ]
}
