Sari la conținut

Comenzi ​

Creează și gestionează comenzi. Comenzile parcurg statusuri — pending (statusul cu care se creează), processing, assigned, picked, ready_to_ship, shipped, delivered, completed, plus stările terminale cancelled, returned, partially_returned și fully_returned.

Endpoint-uri ​

MetodăCaleDescriere
GET/api/v1/ordersListă paginată; filtre status, order_number, external_order_id, customer_name, date_from, date_to.
GET/api/v1/orders/{id}Citește o comandă cu liniile, istoricul de status și operatorul asignat.
POST/api/v1/ordersCreează o comandă.
PUT/api/v1/orders/{id}/statusActualizează statusul unei comenzi (nu pentru chei de client externe).
DELETE/api/v1/orders/{id}Șterge definitiv o comandă (doar pending sau cancelled; nu pentru chei de client externe).
POST/api/v1/orders/{id}/change-requestsCere anularea sau modificarea adresei de livrare a unei comenzi (inclusiv pentru chei de client externe).
GET/api/v1/orders/{id}/change-requestsCitește cererile făcute pentru o comandă (inclusiv pentru chei de client externe).

Chei API de client (3PL) ​

Aceste reguli se aplică identic pe /api/v1 și /api/v2.

O cheie API legată de un client extern (3PL, client care nu este depozitul propriu) poate doar:

  • citi comenzile proprii (GET /orders, GET /orders/{id}); comenzile altor clienți nu sunt vizibile;
  • crea comenzi (POST /orders, cu permisiunea orders:write). Comanda se creează mereu pending și pe numele clientului cheii; status, client_id sau alte câmpuri de proprietate trimise în body sunt ignorate;
  • cere anularea sau modificarea adresei de livrare a propriilor comenzi (POST /orders/{id}/change-requests, cu permisiunea orders:write) și citi cererile făcute (GET /orders/{id}/change-requests). O cerere nu setează niciodată un status (vezi Cereri de anulare și modificare).

Orice altă operație de scriere pe comenzi este refuzată, indiferent de permisiunile cheii (inclusiv * sau permisiuni nesetate): PUT /orders/{id}/status (orice status, inclusiv cancelled), DELETE /orders/{id} și orice rută viitoare de scriere pe comenzi. Cererea primește 403 înainte de orice modificare. Textul din error este tradus în funcție de limba activă a aplicației (mai jos, exemplul în română; în engleză mesajul are același sens); nu depinde de o setare a cheii sau a tenantului. Valoarea code este stabilă și este cea pe care trebuie să te bazezi în cod:

json
{
  "error": "Cheile API de client pot crea și citi comenzi. Schimbarea statusului și ștergerea comenzilor, inclusiv anularea, se fac de către depozit.",
  "code": "client_order_operation_forbidden"
}

Anularea. Un client nu anulează o comandă prin PUT .../status sau DELETE: răspunsul 403 nu înregistrează nicio cerere. Anularea se cere prin POST /orders/{id}/change-requests (mai jos), iar depozitul o execută operațional (inclusiv readucerea mărfii deja pregătite în stoc).

Erorile existente rămân neschimbate și au prioritate: modul 3PL inactiv, client suspendat sau șters, cheie inactivă sau lipsa permisiunii orders:read / orders:write (403).

Cheile tenantului (fără client) și cheile istorice legate de clientul propriu al depozitului (house) păstrează comportamentul descris în restul paginii, cu aceleași limite de permisiuni. Tipul cheii se stabilește din clientul salvat în baza de date, niciodată din valori trimise în cerere (body sau headere).

Schema răspunsului pentru chei de client ​

O cheie legată de un client extern primește o schemă explicită, la citire, la creare și la reluarea unei cereri cu același Idempotency-Key. Câmpurile interne ale depozitului nu fac parte din ea: nicio notă internă sau de depozit, niciun cost de transport, nicio decizie de rutare, nicio cale a facturii, nicio identitate de personal. Cheile tenantului și cheile house păstrează schema de integrare descrisă în restul paginii.

CâmpNote
id, order_number, status
external_order_id, external_source, external_order_refReferințele sistemului tău.
customer_name, recipient_phone, recipient_email, shipping_address, recipient_county_name, recipient_locality_nameDestinatarul.
order_date, required_date, shipped_date, awb_barcode, shipping_method
total_value, cash_repayment, observations
api_warningsProdusele ignorate sau stocul insuficient la creare; null când nu există.
warehouse{ "code": "…" } sau null.
delivery_promise{ carrier, service, promised_on, verdict } sau null.
items[]product_id, sku, name, quantity, picked_quantity, unit_price.
status_history[]Doar la GET /orders/{id}: { status, at }, fără cine a schimbat statusul și fără motiv.
created_at, updated_at

Un câmp intern nou al comenzii nu apare aici de la sine: lista de mai sus este singura sursă a schemei.

Răspuns 200 la GET /api/v1/orders/{id} pentru o cheie de client (exemplul este verificat automat față de schema înghețată):

json
{
  "data": {
    "id": 4471,
    "order_number": "ORD-000123",
    "external_order_id": "SHOP-88213",
    "external_source": "woocommerce",
    "external_order_ref": null,
    "status": "processing",
    "customer_name": "Andrei Popescu",
    "recipient_phone": "+40712345678",
    "recipient_email": "andrei@example.com",
    "shipping_address": "Str. Memorandumului 28",
    "recipient_county_name": "Cluj",
    "recipient_locality_name": "Cluj-Napoca",
    "order_date": "2026-06-11T08:42:11.000000Z",
    "required_date": null,
    "shipped_date": null,
    "awb_barcode": null,
    "shipping_method": "cargus",
    "total_value": "362.76",
    "cash_repayment": "0.00",
    "observations": null,
    "api_warnings": null,
    "warehouse": { "code": "MAIN" },
    "delivery_promise": null,
    "items": [
      { "product_id": 812, "sku": "BRK-PAD-001", "name": "Plăcuțe frână față", "quantity": 2, "picked_quantity": 0, "unit_price": "149.90" }
    ],
    "status_history": [
      { "status": "pending", "at": "2026-06-11T08:42:11.000000Z" },
      { "status": "processing", "at": "2026-06-11T09:05:40.000000Z" }
    ],
    "created_at": "2026-06-11T08:42:11.000000Z",
    "updated_at": "2026-06-11T09:05:40.000000Z"
  }
}

Rândurile din GET /api/v1/orders au aceeași formă, fără status_history:

json
{
  "data": [
    {
      "id": 4471,
      "order_number": "ORD-000123",
      "external_order_id": "SHOP-88213",
      "external_source": "woocommerce",
      "external_order_ref": null,
      "status": "processing",
      "customer_name": "Andrei Popescu",
      "recipient_phone": "+40712345678",
      "recipient_email": "andrei@example.com",
      "shipping_address": "Str. Memorandumului 28",
      "recipient_county_name": "Cluj",
      "recipient_locality_name": "Cluj-Napoca",
      "order_date": "2026-06-11T08:42:11.000000Z",
      "required_date": null,
      "shipped_date": null,
      "awb_barcode": null,
      "shipping_method": "cargus",
      "total_value": "362.76",
      "cash_repayment": "0.00",
      "observations": null,
      "api_warnings": null,
      "warehouse": null,
      "delivery_promise": null,
      "items": [
        { "product_id": 812, "sku": "BRK-PAD-001", "name": "Plăcuțe frână față", "quantity": 2, "picked_quantity": 0, "unit_price": "149.90" }
      ],
      "created_at": "2026-06-11T08:42:11.000000Z",
      "updated_at": "2026-06-11T09:05:40.000000Z"
    }
  ]
}

Exemplul arată doar rândurile din data; metadatele de paginare sunt cele descrise în prezentarea generală.

Filtre la listare ​

ParametruTipNote
statusstringFiltrează după status.
order_numberstringPotrivire exactă pe numărul intern al comenzii.
external_order_idstringPotrivire exactă pe id-ul din sistemul extern.
customer_namestringPotrivire parțială pe numele clientului.
date_fromYYYY-MM-DDComenzi cu order_date ≥ valoarea dată.
date_toYYYY-MM-DDComenzi cu order_date ≤ valoarea dată.
per_pageintegerImplicit 50.

Creare comandă ​

POST /api/v1/orders. Câmpuri obligatorii: customer_name, shipping_method și cel puțin o linie în items.

CâmpTipObligatoriuNote
customer_namestring ≤ 255daNumele destinatarului.
shipping_methodstringdaTrebuie să fie unul din codurile returnate de GET /api/v1/couriers.
itemsarray (min 1)daLiniile comenzii.
items[].product_idintegerda**Sau items[].sku. Produsele negăsite în WMS nu resping comanda — devin linii fără produs (afișate ca probleme de stoc) și sunt raportate în warnings.
items[].skustring ≤ 100nuAlternativă la product_id; linia se rezolvă după SKU.
items[].namestring ≤ 255nuNumele produsului, păstrat pentru liniile negăsite în WMS.
items[].quantityinteger ≥ 1daCantitatea comandată.
items[].unit_pricenumericdaPreț unitar. Liniile cu preț negativ (discount/corecție) sunt omise din comandă — nu ajung la picking; valorile nenumerice devin 0.
items[].notestextnuNotă pe linie.
order_numberstring ≤ 50nuGenerat automat (ORD-000123) dacă lipsește.
external_order_idstring ≤ 100nuId-ul comenzii în sistemul tău.
external_sourcestring ≤ 50nuSursa (ex. shopify, woocommerce).
typestring ≤ 50nu
warehouse_idintegernuTrebuie să existe; vezi rezolvarea depozitului mai jos.
customer_emailemail ≤ 255nu
shipping_addresstextnu
address_texttextnuAdresă liberă, dintr-o singură linie.
recipient_phonestring ≤ 20nu
recipient_contact_personstring ≤ 255nu
recipient_county_namestring ≤ 100nuJudețul destinatarului (folosit la AWB).
recipient_locality_namestring ≤ 100nuLocalitatea destinatarului.
total_valuenumeric ≥ 0daTotalul facturii, cu TVA inclus. Nu este calculat din linii (acelea sunt fără TVA și fără discounturi). Devine declared_value (valoarea asigurată pe AWB) și, la RAMBURS, suma de încasat.
payment_methodOP | RAMBURSdaOP = plătit în avans, curierul nu încasează nimic. RAMBURS = curierul încasează total_value (bifează „Cont Colector" pe AWB).
total_weightnumeric ≥ 0nu
notestextnu
order_dateYYYY-MM-DDnuImplicit acum.
required_dateYYYY-MM-DDnu
priorityenumnulow | normal | high | urgent.
invoice_referencestring ≤ 100nuNumărul facturii care însoțește comanda (stocat ca atare pe comandă).
client_cuistring ≤ 20nuCUI-ul clientului — folosit doar în numele fișierului de factură.
min_shelf_life_daysinteger 0–3650nuValabilitatea minimă (în zile) pe care trebuie să o mai aibă un lot ca să fie livrat pe această comandă. Suprascrie valoarea implicită a clientului; 0 este o valoare, nu „nesetat”. Loturile expirate nu sunt niciodată preluate.
invoice_pdfstringnuDocumentul facturii, ca PDF codat base64. Vezi mai jos.

Comportamente la creare:

  • Curieri obligatorii. Dacă tenantul nu are niciun curier configurat, cererea returnează 422 cu code: no_couriers_configured înainte de validare. Configurează un curier în Setări → Curieri, apoi citește valorile valide din GET /api/v1/couriers.
  • Linii de discount. Liniile cu unit_price negativ sunt tratate ca discount/corecție de facturare, nu ca produse fizice: sunt omise din comandă și nu ajung la picking. Dacă toate liniile au preț negativ, cererea returnează 422 cu code: only_discount_lines.
  • Deduplicare linii. Două linii cu același produs (același product_id sau același sku) sunt comasate într-o singură linie: cantitățile se adună, iar prețul păstrat este primul preț pozitiv întâlnit.
  • Produse negăsite în WMS. O linie al cărei produs nu există (de ex. produse virtuale — tichete, vouchere) nu respinge comanda: este păstrată ca linie fără produs (product_id: null, preț 0, SKU-ul și numele original în notes), apare în interfață ca problemă de stoc și este raportată în array-ul warnings din răspuns și în api_warnings pe comandă.
  • Stoc. O comandă primită prin API nu este refuzată pentru lipsă de stoc (magazinul a vândut deja marfa): lipsa este acceptată și raportată ca avertisment — o intrare { "code": "insufficient_stock", "sku", "product_id", "requested", "available" } în api_warnings pe comandă (lângă skipped_products) și un text în array-ul warnings din răspuns. Cantitatea disponibilă ține cont de cererile celorlalte comenzi deschise. Un produs găsit în WMS dar inactiv respinge comanda cu 422 și { "error": "inactive_product", "sku": "..." }. Comanda, liniile ei și chitanța de idempotență se scriu într-o singură tranzacție: o eroare la mijloc nu lasă o comandă parțială.
  • Trimitere idempotentă. Trimite un identificator unic al cererii în headerul Idempotency-Key (sau, dacă nu poți seta headere, în câmpul de body idempotency_key; maximum 128 de caractere, altfel 422 cu error: invalid_idempotency_key). Aceeași cheie cu același conținut returnează comanda deja creată, cu 200 și headerul Idempotent-Replayed: true, fără o a doua comandă. Aceeași cheie cu conținut diferit returnează 409 cu { "error": "idempotency_conflict" }. Cheile sunt separate pe cheie API. Fără cheie, fiecare cerere creează o comandă nouă (comportamentul anterior).
  • Valoare și plată. total_value este preluat exact cum îl trimiți (totalul facturii, cu TVA) — nu este recalculat din linii, pentru că liniile sunt fără TVA și fără discounturi. Din el se derivă declared_value (valoarea asigurată pe AWB) și, dacă payment_method este RAMBURS, suma pe care o încasează curierul. La OP nu se încasează nimic. Dacă nu trimiți total_value corect, AWB-ul se generează cu ramburs greșit.
  • Status inițial. Comanda se creează cu status = pending.
  • Rezolvarea depozitului. În ordine: warehouse_id din body → depozitul de care e legată cheia API → header X-Warehouse-Id → depozitul implicit al tenantului. Dacă trimiți un warehouse_id care diferă de depozitul de care e legată cheia, primești 403.
  • Documentul facturii. Dacă trimiți invoice_pdf, platforma îl decodează din base64, verifică să fie un PDF valid (altfel 422 cu code: invalid_invoice_pdf, înainte de a crea comanda) și îl salvează ca order_pdf_invoices/factura-{referință}[-cui-RO{cui}].pdf. Calea ajunge în invoice_file_path pe comandă, împreună cu invoice_uploaded_at. invoice_reference poate fi trimis și singur, fără document.
bash
curl -X POST https://tenant.notsowms.ro/api/v1/orders \
  -H "Authorization: Bearer tenant.7c4a8d09ca3762af61e59520943dc26494f8941b" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_name": "Andrei Popescu",
    "customer_email": "andrei@example.com",
    "shipping_method": "cargus",
    "recipient_phone": "+40712345678",
    "recipient_county_name": "Cluj",
    "recipient_locality_name": "Cluj-Napoca",
    "address_text": "Str. Memorandumului 28",
    "priority": "high",
    "total_value": 362.76,
    "payment_method": "RAMBURS",
    "items": [
      { "product_id": 812, "quantity": 2, "unit_price": 149.90 }
    ]
  }'

Răspuns 201 — comanda creată, cu liniile ei:

json
{
  "data": {
    "id": 4471,
    "order_number": "ORD-000123",
    "status": "pending",
    "customer_name": "Andrei Popescu",
    "shipping_method": "cargus",
    "priority": "high",
    "order_date": "2026-06-11T08:42:11.000000Z",
    "items": [
      { "id": 9912, "product_id": 812, "quantity": 2, "quantity_ordered": 2, "unit_price": "149.90" }
    ]
  }
}

Când comanda conține produse care nu există în WMS, răspunsul 201 include și warnings, iar linia respectivă apare cu product_id: null și missing_product:

json
{
  "data": {
    "id": 4472,
    "order_number": "ORD-000124",
    "status": "pending",
    "items": [
      { "id": 9913, "product_id": 812, "quantity": 2, "unit_price": "149.90", "missing_product": null },
      { "id": 9914, "product_id": null, "quantity": 1, "unit_price": "0.00", "missing_product": { "sku": "TICHET-GAZ", "name": "Tichete gaz" } }
    ]
  },
  "warnings": [
    "Următoarele produse au fost ignorate deoarece nu au fost găsite în baza de date: TICHET-GAZ - Tichete gaz (cantitate: 1)"
  ]
}

Atașarea facturii la creare ​

Codează PDF-ul în base64 și trimite-l în invoice_pdf, alături de invoice_reference (numărul facturii) și, opțional, client_cui:

bash
INVOICE_PDF=$(base64 -i factura.pdf)

curl -X POST https://tenant.notsowms.ro/api/v1/orders \
  -H "Authorization: Bearer tenant.7c4a8d09ca3762af61e59520943dc26494f8941b" \
  -H "Content-Type: application/json" \
  -d "{
    \"customer_name\": \"Andrei Popescu\",
    \"shipping_method\": \"cargus\",
    \"invoice_reference\": \"FAC-2026-0042\",
    \"client_cui\": \"RO12345678\",
    \"invoice_pdf\": \"${INVOICE_PDF}\",
    \"items\": [ { \"product_id\": 812, \"quantity\": 2, \"unit_price\": 149.90 } ]
  }"

În răspunsul 201, comanda include invoice_reference și invoice_file_path (ex. order_pdf_invoices/factura-FAC-2026-0042-cui-RO12345678.pdf). Documentul se poate descărca ulterior din interfața WMS sau prin GET /api/v1/orders/{id} (câmpul invoice_file_path).

Actualizare status ​

PUT /api/v1/orders/{id}/status. Disponibil doar cheilor tenantului și cheilor house; cheile de client externe primesc 403 cu code: client_order_operation_forbidden (vezi Chei API de client).

CâmpTipObligatoriuNote
statusstring ≤ 50daNoul status (vezi lista de statusuri de mai sus).
notestextnuNotă salvată în istoricul de status.

Folosește doar una din valorile enumerate mai sus — o valoare necunoscută primește 422 de validare.

Statusul se schimbă după un tabel declarat, același pentru ecranul de birou, cheile tenantului și ale house și automatizări:

  • înainte de expediere (pending, processing, assigned, picked, packing, packed, ready_to_ship) comanda poate trece în oricare alt status dinainte de expediere, în cancelled, shipped sau delivered;
  • shipped merge doar înainte: delivered, completed, returned, partially_returned, fully_returned; delivered și completed la fel (completed doar spre returnări);
  • cancelled, returned, partially_returned și fully_returned nu mai pleacă nicăieri prin această rută (o comandă anulată revine doar prin acțiunea „Restaurează” din birou).

O mutare care nu este în tabel primește 422 cu code: transition_not_allowed și un mesaj care numește statusul actual și cel cerut. Cererea pentru statusul în care comanda deja se află nu scrie nimic (nici istoric, nici evenimente). O comandă cu o modificare sau o recuperare de lot în curs nu poate fi mutată (409 sau 422).

Schimbarea se înregistrează o singură dată în istoricul comenzii. Trecerea la shipped declanșează o singură dată evenimentul de expediere (facturare, SLA, taxarea manoperei), oricare ar fi ruta. status: cancelled este anularea sigură descrisă mai jos.

bash
curl -X PUT https://tenant.notsowms.ro/api/v1/orders/4471/status \
  -H "Authorization: Bearer tenant.7c4a8d09ca3762af61e59520943dc26494f8941b" \
  -H "Content-Type: application/json" \
  -d '{ "status": "cancelled", "notes": "Anulată la cererea clientului" }'

Anularea unei comenzi ​

Orice ușă care anulează o comandă (ecranul de birou, PUT .../status cu cancelled, automatizările, o cerere de client acceptată) trece prin același serviciu, sub blocarea comenzii:

  • o comandă deja expediată (sau mai departe) primește 422 cu code: order_already_shipped;
  • o comandă nepickată iese din valul și din lotul ei, își eliberează alocările de stoc, își închide promisiunea SLA cu verdictul cancelled (care nu intră în rapoartele SLA) și declanșează o singură dată OrderCancelled;
  • o comandă deja pickată își primește stocul înapoi pe raft, cu legătura către pick-ul inversat;
  • o comandă dintr-un lot deja în picking sau sigilată devine o sarcină de repunere în stoc și se anulează când marfa a revenit;
  • repetarea anulării, pe aceeași ușă sau pe alta, nu mai face nimic.

Cereri de anulare și modificare ​

POST /api/v1/orders/{id}/change-requests. Disponibil și cheilor de client externe, doar pentru comenzile proprii (altă comandă: 404).

CâmpTipObligatoriuNote
kindstringdacancel sau edit.
reasontextdaMotivul cererii, 3–1000 de caractere.
fieldsobiectpentru editDoar shipping_address, address_text, recipient_phone, recipient_contact_person, observations, recipient_county_name, recipient_locality_name, recipient_street_name, recipient_building_number. Orice alt câmp (status, valori, client, număr) este respins cu 422.

Antetul Idempotency-Key (sau câmpul idempotency_key, cel mult 128 de caractere) face cererea idempotentă, separat pentru fiecare cheie API: aceeași cheie cu același conținut întoarce răspunsul stocat (200 cu Idempotent-Replayed: true), aceeași cheie cu alt conținut este 409 cu code: idempotency_conflict.

Rezultatul din data.status:

StatusCând
appliedO anulare cerută înainte de începerea picking-ului: comanda este anulată imediat (201).
requestedO anulare după începerea picking-ului sau o modificare înainte de expediere: așteaptă decizia depozitului (201).
too_lateComanda a plecat deja din depozit: 422 cu code: order_already_shipped; cererea se înregistrează o singură dată.

Depozitul acceptă (anularea rulează prin serviciul de mai sus, modificarea se scrie pe comandă și rămâne în jurnal) sau respinge cu o notă; clientul poate retrage din portal o cerere aflată în așteptare. Statusurile accepted, rejected și withdrawn apar la citire.

GET /api/v1/orders/{id}/change-requests întoarce data[] cu id, kind, status, reason, fields, decision_note, decided_at și created_at.

bash
curl -X POST https://tenant.notsowms.ro/api/v1/orders/4471/change-requests \
  -H "Authorization: Bearer tenant.7c4a8d09ca3762af61e59520943dc26494f8941b" \
  -H "Idempotency-Key: cancel-4471-1" \
  -H "Content-Type: application/json" \
  -d '{ "kind": "cancel", "reason": "Clientul a renunțat" }'

Ștergere ​

Disponibil doar cheilor tenantului și cheilor house; cheile de client externe primesc 403 cu code: client_order_operation_forbidden.

DELETE /api/v1/orders/{id} șterge definitiv comanda împreună cu liniile și istoricul ei de status — nu este o anulare „soft”. Pentru a anula o comandă păstrând-o, folosește în schimb PUT .../status cu status: cancelled (anularea sigură de mai sus).

Ștergerea este permisă doar dacă statusul comenzii este pending (neprocesată încă) sau cancelled; o comandă care a intrat în fluxul de depozit nu poate fi ștearsă și primești 422:

json
{ "error": "Only pending or cancelled orders can be deleted." }
bash
curl -X DELETE https://tenant.notsowms.ro/api/v1/orders/4471 \
  -H "Authorization: Bearer tenant.7c4a8d09ca3762af61e59520943dc26494f8941b"

Răspuns:

json
{ "message": "Order deleted." }

Documentație API notso WMS