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ă | Cale | Descriere |
|---|---|---|
| GET | /api/v1/orders | Listă 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/orders | Creează o comandă. |
| PUT | /api/v1/orders/{id}/status | Actualizează 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-requests | Cere anularea sau modificarea adresei de livrare a unei comenzi (inclusiv pentru chei de client externe). |
| GET | /api/v1/orders/{id}/change-requests | Citeș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 permisiuneaorders:write). Comanda se creează mereupendingși pe numele clientului cheii;status,client_idsau 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 permisiuneaorders: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:
{
"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âmp | Note |
|---|---|
id, order_number, status | |
external_order_id, external_source, external_order_ref | Referințele sistemului tău. |
customer_name, recipient_phone, recipient_email, shipping_address, recipient_county_name, recipient_locality_name | Destinatarul. |
order_date, required_date, shipped_date, awb_barcode, shipping_method | |
total_value, cash_repayment, observations | |
api_warnings | Produsele 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ă):
{
"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:
{
"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
| Parametru | Tip | Note |
|---|---|---|
status | string | Filtrează după status. |
order_number | string | Potrivire exactă pe numărul intern al comenzii. |
external_order_id | string | Potrivire exactă pe id-ul din sistemul extern. |
customer_name | string | Potrivire parțială pe numele clientului. |
date_from | YYYY-MM-DD | Comenzi cu order_date ≥ valoarea dată. |
date_to | YYYY-MM-DD | Comenzi cu order_date ≤ valoarea dată. |
per_page | integer | Implicit 50. |
Creare comandă
POST /api/v1/orders. Câmpuri obligatorii: customer_name, shipping_method și cel puțin o linie în items.
| Câmp | Tip | Obligatoriu | Note |
|---|---|---|---|
customer_name | string ≤ 255 | da | Numele destinatarului. |
shipping_method | string | da | Trebuie să fie unul din codurile returnate de GET /api/v1/couriers. |
items | array (min 1) | da | Liniile comenzii. |
items[].product_id | integer | da* | *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[].sku | string ≤ 100 | nu | Alternativă la product_id; linia se rezolvă după SKU. |
items[].name | string ≤ 255 | nu | Numele produsului, păstrat pentru liniile negăsite în WMS. |
items[].quantity | integer ≥ 1 | da | Cantitatea comandată. |
items[].unit_price | numeric | da | Preț unitar. Liniile cu preț negativ (discount/corecție) sunt omise din comandă — nu ajung la picking; valorile nenumerice devin 0. |
items[].notes | text | nu | Notă pe linie. |
order_number | string ≤ 50 | nu | Generat automat (ORD-000123) dacă lipsește. |
external_order_id | string ≤ 100 | nu | Id-ul comenzii în sistemul tău. |
external_source | string ≤ 50 | nu | Sursa (ex. shopify, woocommerce). |
type | string ≤ 50 | nu | |
warehouse_id | integer | nu | Trebuie să existe; vezi rezolvarea depozitului mai jos. |
customer_email | email ≤ 255 | nu | |
shipping_address | text | nu | |
address_text | text | nu | Adresă liberă, dintr-o singură linie. |
recipient_phone | string ≤ 20 | nu | |
recipient_contact_person | string ≤ 255 | nu | |
recipient_county_name | string ≤ 100 | nu | Județul destinatarului (folosit la AWB). |
recipient_locality_name | string ≤ 100 | nu | Localitatea destinatarului. |
total_value | numeric ≥ 0 | da | Totalul 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_method | OP | RAMBURS | da | OP = plătit în avans, curierul nu încasează nimic. RAMBURS = curierul încasează total_value (bifează „Cont Colector" pe AWB). |
total_weight | numeric ≥ 0 | nu | |
notes | text | nu | |
order_date | YYYY-MM-DD | nu | Implicit acum. |
required_date | YYYY-MM-DD | nu | |
priority | enum | nu | low | normal | high | urgent. |
invoice_reference | string ≤ 100 | nu | Numărul facturii care însoțește comanda (stocat ca atare pe comandă). |
client_cui | string ≤ 20 | nu | CUI-ul clientului — folosit doar în numele fișierului de factură. |
min_shelf_life_days | integer 0–3650 | nu | Valabilitatea 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_pdf | string | nu | Documentul facturii, ca PDF codat base64. Vezi mai jos. |
Comportamente la creare:
- Curieri obligatorii. Dacă tenantul nu are niciun curier configurat, cererea returnează
422cucode: no_couriers_configuredînainte de validare. Configurează un curier în Setări → Curieri, apoi citește valorile valide dinGET /api/v1/couriers. - Linii de discount. Liniile cu
unit_pricenegativ 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ă422cucode: only_discount_lines. - Deduplicare linii. Două linii cu același produs (același
product_idsau acelașisku) 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 înnotes), apare în interfață ca problemă de stoc și este raportată în array-ulwarningsdin răspuns și înapi_warningspe 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" }înapi_warningspe comandă (lângăskipped_products) și un text în array-ulwarningsdin răspuns. Cantitatea disponibilă ține cont de cererile celorlalte comenzi deschise. Un produs găsit în WMS dar inactiv respinge comanda cu422ș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 bodyidempotency_key; maximum 128 de caractere, altfel422cuerror: invalid_idempotency_key). Aceeași cheie cu același conținut returnează comanda deja creată, cu200și headerulIdempotent-Replayed: true, fără o a doua comandă. Aceeași cheie cu conținut diferit returnează409cu{ "error": "idempotency_conflict" }. Cheile sunt separate pe cheie API. Fără cheie, fiecare cerere creează o comandă nouă (comportamentul anterior). - Valoare și plată.
total_valueeste 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_methodesteRAMBURS, suma pe care o încasează curierul. LaOPnu se încasează nimic. Dacă nu trimițitotal_valuecorect, AWB-ul se generează cu ramburs greșit. - Status inițial. Comanda se creează cu
status = pending. - Rezolvarea depozitului. În ordine:
warehouse_iddin body → depozitul de care e legată cheia API → headerX-Warehouse-Id→ depozitul implicit al tenantului. Dacă trimiți unwarehouse_idcare diferă de depozitul de care e legată cheia, primești403. - Documentul facturii. Dacă trimiți
invoice_pdf, platforma îl decodează din base64, verifică să fie un PDF valid (altfel422cucode: invalid_invoice_pdf, înainte de a crea comanda) și îl salvează caorder_pdf_invoices/factura-{referință}[-cui-RO{cui}].pdf. Calea ajunge îninvoice_file_pathpe comandă, împreună cuinvoice_uploaded_at.invoice_referencepoate fi trimis și singur, fără document.
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:
{
"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:
{
"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:
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âmp | Tip | Obligatoriu | Note |
|---|---|---|---|
status | string ≤ 50 | da | Noul status (vezi lista de statusuri de mai sus). |
notes | text | nu | Notă 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, încancelled,shippedsaudelivered; shippedmerge doar înainte:delivered,completed,returned,partially_returned,fully_returned;deliveredșicompletedla fel (completeddoar spre returnări);cancelled,returned,partially_returnedșifully_returnednu 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.
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
422cucode: 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âmp | Tip | Obligatoriu | Note |
|---|---|---|---|
kind | string | da | cancel sau edit. |
reason | text | da | Motivul cererii, 3–1000 de caractere. |
fields | obiect | pentru edit | Doar 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:
| Status | Când |
|---|---|
applied | O anulare cerută înainte de începerea picking-ului: comanda este anulată imediat (201). |
requested | O anulare după începerea picking-ului sau o modificare înainte de expediere: așteaptă decizia depozitului (201). |
too_late | Comanda 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.
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:
{ "error": "Only pending or cancelled orders can be deleted." }curl -X DELETE https://tenant.notsowms.ro/api/v1/orders/4471 \
-H "Authorization: Bearer tenant.7c4a8d09ca3762af61e59520943dc26494f8941b"Răspuns:
{ "message": "Order deleted." }