HOCO B2B
SRSAPI reference

Cart & checkout

The reseller's server-side cart shared by the app and the web — lines, re-validated prices and availability, promotions, totals, blocking issues — and the idempotent checkout that submits an order for back-office confirmation.

The cart is the reseller's draft order. It lives on the server (ORD-01), so the reseller app (Imad, MRA-04) and the reseller web (Anis, WEB-03) always show the same lines; it is built in SRV-15.

  • Signed-in resellers only. Guests browse without prices and get 401 UNAUTHENTICATED here (the app shows the sign-up prompt of RA-41). Delegates build assisted orders through delegate.assisted.*, never through this cart.
  • Re-validated on every read and write. Prices (the reseller's customer-type price), promotions, visibility, quantity rules, availability and the minimum amount are recomputed each time a cart is returned (PRC-05, cahier §5.3 « Le système revalide prix et stock avant envoi »). Problems come back as Issues on the line or the cart; canCheckout sums them up.
  • No stock numbers. A line is either available or not (STOCK_INSUFFICIENT); the cart never says how many units remain (STK-07).
  • Checkout submits, people confirm. cart.checkout re-validates everything, creates the order in SUBMITTED and empties the cart; the order then waits for the back-office confirmation before any preparation (ORD-14). The apps say so: « Commande envoyée — elle sera confirmée par l'équipe HOCO avant sa préparation ».
MethodPathOperationAccessAppsRelease
GET/cartcart.get
My cart with lines, re-validated prices/stock, applied promotions, totals and blocking issues
selfRA · RWV1
POST/cart/itemscart.items.add
Add a variant (or increase its quantity)
self
Idempotency-Key
RA · RWV1
PATCH/cart/items/{itemId}cart.items.update
Change a line quantity
selfRA · RWV1
DELETE/cart/items/{itemId}cart.items.remove
Remove a line
selfRA · RWV1
DELETE/cartcart.clear
Empty the cart
selfRA · RWV1
GET/cart/checkout-optionscart.checkoutOptions
Allowed payment methods, delivery addresses, delivery fee and credit availability for this cart
selfRA · RWV1
POST/cart/checkoutcart.checkout
Submit the cart as an order (Idempotency-Key required); re-validates everything; returns the order
self
Idempotency-Key
RA · RWV1
POST/cart/packscart.packs.add
Add a pack to the cart (V2)
self
Idempotency-Key
RA · RWV2

Every operation can also answer 401 UNAUTHENTICATED, 403 ACCOUNT_SUSPENDED, 403 PERMISSION_DENIED (caller is not a reseller), 426 UPGRADE_REQUIRED, 429 RATE_LIMITED, 500 INTERNAL_ERROR and 503 MAINTENANCE — see the error registry. Operations marked Idempotency-Key required answer 400 IDEMPOTENCY_KEY_REQUIRED without the header, 409 IDEMPOTENCY_KEY_REUSED and 409 IDEMPOTENCY_IN_PROGRESS as described in the conventions.

Reference screen 5 — lines with quantity steppers, Sous-total / Frais de livraison / Total TTC, delivery address, order note and « Valider la commande ».
Reference screen 5 — lines with quantity steppers, Sous-total / Frais de livraison / Total TTC, delivery address, order note and « Valider la commande ». — client reference (cahier des charges)

How the cart computes amounts

  1. Unit price. For each line, the variant's price for the reseller's customerType (priceSuperGros, priceDemiGros or priceDetail, stored HT) — no quantity tiers in V1 (D-37). price.ttc = round(price.ht × (1 + vatRate / 10 000)).
  2. Promotion. The best applicable promotion under the stacking rules (PRM-03, PRM-04) gives a promotional unit price promotionApplied.unitPrice (HT, then TTC rounded the same way). PERCENT_OFF and AMOUNT_OFF apply to the HT unit price; FIXED_PRICE replaces it; FREE_DELIVERY sets the delivery fee to 0.
  3. Line totals (LineTotals): subtotalHt = price.ht × qty; discountHt = (price.ht − unitPrice.ht) × qty; totalHt = subtotalHt − discountHt; totalTtc = unit TTC actually paid × qty; vat = totalTtc − totalHt. Computing VAT as a difference keeps the sum of the lines equal to the total to the centime.
  4. Cart totals (shared Totals): sums of the lines, plus the delivery fee (HT, its VAT added to vatTotal); totalTtc = subtotalHt − discountTotal + deliveryFee + vatTotal. displayTotals gives the same figures in TTC for the summary block (Sous-total, Remises, Frais de livraison, Total TTC).
  5. Delivery fee (DLV-06, D-20): setting delivery.feePolicy — FREE (default: every order that reaches the minimum is delivered free, shown « Offerts ») or WILAYA_TABLE (fee per wilaya delivery.feeByWilaya, HT, waived from delivery.freeThresholdTtc of merchandise). The cart uses the default address; checkout uses the chosen one.
  6. Minimum amount (ORD-02, D-19): orders.minAmount (default 1 000 000 = 10 000,00 DA) compared with the merchandise total TTC after promotions, delivery fee excluded.

Assumption — the setting names delivery.feePolicy, delivery.feeByWilaya and delivery.freeThresholdTtc are proposed here; the settings catalogue owns them.

Issues

Line issues sit in lines[].issues (with lineId); cart-level issues in issues. canCheckout is true when the cart has at least one line and no BLOCKING issue anywhere.

CodeSeverityWhereRaised whenWhat the reseller does
PRICE_CHANGEDWARNINGlineThe unit price (or its promotion) differs from the price recorded when the line was last added or updated — new price list, promotion started, customer type changed by the back-office. details: { previous: Price, current: Price }Nothing (informative); updating the line, even to the same quantity, acknowledges it
PROMOTION_ENDEDWARNINGlineA promotion the line had is over (details.reason: END_DATE, CAP_REACHED, PAUSED) — « Offre terminée », normal price appliesNothing
PRODUCT_UNAVAILABLEBLOCKINGlineProduct hidden, archived, no longer visible to the reseller, or variant switched off (details.reason: HIDDEN, ARCHIVED, NOT_VISIBLE, VARIANT_INACTIVE)Remove the line
STOCK_INSUFFICIENTBLOCKINGlineThe available stock does not cover the line's quantity, or the variant is out of stock (details.availability: IN_STOCK or OUT_OF_STOCK). No quantity is givenLower the quantity or remove the line
QUANTITY_NOT_ALLOWEDBLOCKINGlineThe line breaks a limit changed since it was added (details.rule: MIN_QTY, QTY_STEP, MAX_PER_ORDER, with minQty, qtyStep, maxQtyPerOrder, suggestedQty)Accept suggestedQty
ORDER_MIN_AMOUNT_NOT_METBLOCKINGcartMerchandise total below orders.minAmount (details: { minAmount, currentAmount, missingAmount }) — « Plus que 3 750,00 DA pour commander »Add products
ACCOUNT_PENDING_VALIDATIONBLOCKINGcartAccountStatus PENDING_VALIDATION — the cart can be prepared, checkout waits for validationWait for validation (RA-06)
RESELLER_SUSPENDEDBLOCKINGcartResellerStatus SUSPENDED (commercial block)Contact HOCO

Schemas

interface CartView {
  id: Uuid;
  updatedAt: Iso;
  itemsCount: number;                 // number of lines — « 4 articles » and the cart badge
  unitsCount: number;                 // Σ qty
  lines: CartLine[];                  // oldest first
  totals: Totals;                     // shared schema
  displayTotals: DisplayTotals;
  appliedPromotions: AppliedPromotion[];
  delivery: {
    addressId: Uuid | null;           // address used for the fee (the default address)
    feePolicy: "FREE" | "WILAYA_TABLE";
    isFree: boolean;                  // « Offerts »
    freeThresholdTtc: Money | null;   // WILAYA_TABLE only
  };
  minimumOrder: { amountTtc: Money; met: boolean; missingTtc: Money };
  issues: Issue[];                    // cart-level issues (shared schema)
  canCheckout: boolean;
}

interface CartLine {
  id: Uuid;                           // CartItem id
  kind: "VARIANT" | "PACK";           // V1: always VARIANT; PACK arrives with V2 packs
  product: { id: Uuid; name: string; brandName: string; imageUrl: string | null };
  variant: { id: Uuid; sku: string; label: string };
  qty: number;
  limits: { minQty: number; qtyStep: number; maxQtyPerOrder: number | null };
  price: Price;                       // unit price for the reseller's customer type, before promotion
  promotionApplied: PromotionApplied | null;
  lineTotals: LineTotals;
  availability: "IN_STOCK" | "OUT_OF_STOCK";
  addedAt: Iso;
  issues: Issue[];
}

interface PromotionApplied {
  promotionId: Uuid;
  label: string;                      // "Offre flash -30 %"
  type: "PERCENT_OFF" | "AMOUNT_OFF" | "FIXED_PRICE";
  value: number;                      // PERCENT_OFF: basis points · AMOUNT_OFF: HT centimes off · FIXED_PRICE: unit price HT
  unitPrice: Price;                   // unit price after the promotion
  endsAt: Iso;
}

interface LineTotals {
  subtotalHt: Money;                  // price.ht × qty
  discountHt: Money;
  totalHt: Money;
  vat: Money;                         // totalTtc − totalHt
  totalTtc: Money;
}

interface AppliedPromotion {          // one entry per promotion used anywhere in the cart
  promotionId: Uuid;
  label: string;
  type: "PERCENT_OFF" | "AMOUNT_OFF" | "FIXED_PRICE" | "FREE_DELIVERY";
  lineIds: Uuid[];                    // empty for FREE_DELIVERY
  discountHt: Money;
  discountTtc: Money;
}

interface DisplayTotals {             // TTC figures of the summary block
  itemsTtc: Money;                    // « Sous-total »: Σ price.ttc × qty, before promotions
  discountTtc: Money;                 // « Remises »
  deliveryFeeTtc: Money;              // « Frais de livraison » (0 = « Offerts »)
  totalTtc: Money;                    // « Total TTC » = totals.totalTtc
}

OrderDetail, returned by cart.checkout, is defined on the orders page; it reuses PromotionApplied, LineTotals, AppliedPromotion and DisplayTotals.

Operations

GET /cart

cart.get · Access session — reseller (self) · Apps RA, RW · Idempotency-Key — · Release V1 · Requirements ORD-01, ORD-02, PRC-05, PRM-04

Purpose. Returns my cart re-priced and re-validated now: lines, applied promotions, totals and every issue that would stop the checkout.

Request — no parameters.

Response — 200 OK, Cache-Control: private, no-store: a CartView. A reseller who never added anything gets an empty cart (the Cart row is created on first access).

{
  "id": "01a1396c-e400-70fe-bda9-3dc62fcad676",
  "updatedAt": "2026-11-03T08:05:00Z",
  "itemsCount": 4,
  "unitsCount": 41,
  "lines": [
    {
      "id": "01a198bd-8680-7243-9a37-cfe6f29d10ee",
      "kind": "VARIANT",
      "product": { "id": "01a0901f-d380-7a88-9746-4ec8936c3d68", "name": "BOROFONE BW06 Écouteurs TWS", "brandName": "BOROFONE", "imageUrl": "https://cdn.hoco.dz/products/bw06/bw06-white-1-240.webp" },
      "variant": { "id": "01a0901f-d768-77a1-94f2-1bca2251bdcd", "sku": "BW06_WHITE", "label": "Blanc" },
      "qty": 6,
      "limits": { "minQty": 1, "qtyStep": 1, "maxQtyPerOrder": 50 },
      "price": { "ht": 250420, "ttc": 298000, "vatRate": 1900 },
      "promotionApplied": null,
      "lineTotals": { "subtotalHt": 1502520, "discountHt": 0, "totalHt": 1502520, "vat": 285480, "totalTtc": 1788000 },
      "availability": "IN_STOCK",
      "addedAt": "2026-11-01T20:12:00Z",
      "issues": [
        {
          "code": "PRICE_CHANGED",
          "severity": "WARNING",
          "lineId": "01a198bd-8680-7243-9a37-cfe6f29d10ee",
          "message": "Le prix de BOROFONE BW06 est passé de 2 880,00 DA à 2 980,00 DA.",
          "details": {
            "previous": { "ht": 242017, "ttc": 288000, "vatRate": 1900 },
            "current": { "ht": 250420, "ttc": 298000, "vatRate": 1900 }
          }
        }
      ]
    },
    {
      "id": "01a19d8f-a800-7ac2-82ce-808c19175f97",
      "kind": "VARIANT",
      "product": { "id": "01a08ac2-8900-7f6d-937c-c81b37e89a5b", "name": "HOCO N32 Chargeur rapide 30W (USB-C + USB-A)", "brandName": "HOCO", "imageUrl": "https://cdn.hoco.dz/products/n32/n32-black-1-240.webp" },
      "variant": { "id": "01a08ac2-8ce8-7069-8328-28fd7158a572", "sku": "N32_BLACK_EU", "label": "Noir" },
      "qty": 10,
      "limits": { "minQty": 1, "qtyStep": 1, "maxQtyPerOrder": 20 },
      "price": { "ht": 209244, "ttc": 249000, "vatRate": 1900 },
      "promotionApplied": null,
      "lineTotals": { "subtotalHt": 2092440, "discountHt": 0, "totalHt": 2092440, "vat": 397560, "totalTtc": 2490000 },
      "availability": "IN_STOCK",
      "addedAt": "2026-11-02T18:40:00Z",
      "issues": []
    },
    {
      "id": "01a19d90-9260-7dcb-a467-f758492648c1",
      "kind": "VARIANT",
      "product": { "id": "01a08ac7-1ce0-7b52-8b6a-3ec2aaccb8fb", "name": "HOCO X89 Câble USB-C", "brandName": "HOCO", "imageUrl": "https://cdn.hoco.dz/products/x89/x89-black-1-240.webp" },
      "variant": { "id": "01a08ac7-20c8-70bf-b753-d181dfe71ff6", "sku": "X89_BLACK_1M", "label": "Noir · 1 m" },
      "qty": 20,
      "limits": { "minQty": 5, "qtyStep": 5, "maxQtyPerOrder": 200 },
      "price": { "ht": 105042, "ttc": 125000, "vatRate": 1900 },
      "promotionApplied": null,
      "lineTotals": { "subtotalHt": 2100840, "discountHt": 0, "totalHt": 2100840, "vat": 399160, "totalTtc": 2500000 },
      "availability": "IN_STOCK",
      "addedAt": "2026-11-02T18:41:00Z",
      "issues": []
    },
    {
      "id": "01a1a070-a7e0-784a-b487-07849a4f0460",
      "kind": "VARIANT",
      "product": { "id": "01a15889-fa80-7ff3-b294-f863a2ee34a6", "name": "HOCO J86A Power Bank 10000mAh", "brandName": "HOCO", "imageUrl": "https://cdn.hoco.dz/products/j86a/j86a-black-1-240.webp" },
      "variant": { "id": "01a15889-fe68-733d-8056-75eec8b226f5", "sku": "J86A_BLACK", "label": "Noir" },
      "qty": 5,
      "limits": { "minQty": 1, "qtyStep": 1, "maxQtyPerOrder": 30 },
      "price": { "ht": 302521, "ttc": 360000, "vatRate": 1900 },
      "promotionApplied": {
        "promotionId": "01a19cfd-2c00-78b5-80c8-b4b9fb167f17",
        "label": "Offre flash -30 %",
        "type": "PERCENT_OFF",
        "value": 3000,
        "unitPrice": { "ht": 211765, "ttc": 252000, "vatRate": 1900 },
        "endsAt": "2026-11-03T22:59:59Z"
      },
      "lineTotals": { "subtotalHt": 1512605, "discountHt": 453780, "totalHt": 1058825, "vat": 201175, "totalTtc": 1260000 },
      "availability": "IN_STOCK",
      "addedAt": "2026-11-03T08:05:00Z",
      "issues": []
    }
  ],
  "totals": {
    "subtotalHt": 7208405,
    "discountTotal": 453780,
    "deliveryFee": 0,
    "vatTotal": 1283375,
    "totalTtc": 8038000,
    "currency": "DZD"
  },
  "displayTotals": { "itemsTtc": 8578000, "discountTtc": 540000, "deliveryFeeTtc": 0, "totalTtc": 8038000 },
  "appliedPromotions": [
    {
      "promotionId": "01a19cfd-2c00-78b5-80c8-b4b9fb167f17",
      "label": "Offre flash -30 %",
      "type": "PERCENT_OFF",
      "lineIds": ["01a1a070-a7e0-784a-b487-07849a4f0460"],
      "discountHt": 453780,
      "discountTtc": 540000
    }
  ],
  "delivery": { "addressId": "01a12f63-efa8-7f36-87fc-e386a4e58a22", "feePolicy": "FREE", "isFree": true, "freeThresholdTtc": null },
  "minimumOrder": { "amountTtc": 1000000, "met": true, "missingTtc": 0 },
  "issues": [],
  "canCheckout": true
}
FieldNotes
linesOldest first, so the list does not jump when a quantity changes.
lines[].priceCurrent price, not the one seen when the line was added — the change is reported by PRICE_CHANGED.
lines[].limitsDrive the stepper: − stops at minQty, + adds qtyStep, the total of the variant stops at maxQtyPerOrder.
totals vs displayTotalstotals is the accounting view (HT, VAT); displayTotals is what RA-17 shows: Sous-total 85 780,00 DA, Remises −5 400,00 DA, Frais de livraison Offerts, Total TTC 80 380,00 DA.
minimumOrderDrives the progress bar « Plus que … pour commander » and the disabled « Valider la commande ».

Errors — none specific.

Rules & side effects

  1. Read-only: re-validation never changes the stored lines — a line with a problem keeps its quantity and gets an issue; nothing is removed or clamped behind the reseller's back.
  2. The cart belongs to the reseller, not to a device: the app and the web read the same Cart. The web refreshes it when the tab regains focus; the app on resume and after every mutation.
  3. Lines untouched for 90 days are purged by a nightly job (assumption, keeps abandoned carts from re-pricing forever).

Used by — RA-17 (« Panier »), RW-06, the cart badge of every reseller screen (through catalog.home or this call).

POST /cart/items

cart.items.add · Access session — reseller (self) · Apps RA, RW · Idempotency-Key required · Release V1 · Requirements ORD-01, ORD-02

Purpose. Adds a variant to the cart, or increases its quantity when it is already there (« Ajouter au panier », the cart button of a product card, « Commander maintenant »).

Request

interface AddCartItemRequest {
  variantId: Uuid;
  qty?: number;                       // units to add; default = the variant's minQty
}
{ "variantId": "01a08ac2-8ce8-7069-8328-28fd7158a572", "qty": 10 }

Response — 200 OK: the whole CartView after the change (example: the cart was empty).

{
  "id": "01a1396c-e400-70fe-bda9-3dc62fcad676",
  "updatedAt": "2026-11-04T08:00:00Z",
  "itemsCount": 1,
  "unitsCount": 10,
  "lines": [
    {
      "id": "01a1a592-7000-7a48-9b51-c3aff054ef28",
      "kind": "VARIANT",
      "product": { "id": "01a08ac2-8900-7f6d-937c-c81b37e89a5b", "name": "HOCO N32 Chargeur rapide 30W (USB-C + USB-A)", "brandName": "HOCO", "imageUrl": "https://cdn.hoco.dz/products/n32/n32-black-1-240.webp" },
      "variant": { "id": "01a08ac2-8ce8-7069-8328-28fd7158a572", "sku": "N32_BLACK_EU", "label": "Noir" },
      "qty": 10,
      "limits": { "minQty": 1, "qtyStep": 1, "maxQtyPerOrder": 20 },
      "price": { "ht": 209244, "ttc": 249000, "vatRate": 1900 },
      "promotionApplied": null,
      "lineTotals": { "subtotalHt": 2092440, "discountHt": 0, "totalHt": 2092440, "vat": 397560, "totalTtc": 2490000 },
      "availability": "IN_STOCK",
      "addedAt": "2026-11-04T08:00:00Z",
      "issues": []
    }
  ],
  "totals": { "subtotalHt": 2092440, "discountTotal": 0, "deliveryFee": 0, "vatTotal": 397560, "totalTtc": 2490000, "currency": "DZD" },
  "displayTotals": { "itemsTtc": 2490000, "discountTtc": 0, "deliveryFeeTtc": 0, "totalTtc": 2490000 },
  "appliedPromotions": [],
  "delivery": { "addressId": "01a12f63-efa8-7f36-87fc-e386a4e58a22", "feePolicy": "FREE", "isFree": true, "freeThresholdTtc": null },
  "minimumOrder": { "amountTtc": 1000000, "met": true, "missingTtc": 0 },
  "issues": [],
  "canCheckout": true
}

Errors

StatusCodeWhen
400VALIDATION_FAILEDqty not a positive integer.
404NOT_FOUNDUnknown variant, or its product is not visible to the reseller.
422PRODUCT_UNAVAILABLEProduct visible but not orderable (variant switched off).
422STOCK_INSUFFICIENTThe variant is out of stock, or the resulting quantity is not available. details: { variantId, requestedQty, availability } — never the available quantity. Message: « Quantité non disponible pour HOCO N32. Réduisez la quantité. »
422QUANTITY_NOT_ALLOWEDThe resulting quantity breaks a limit: below minQty, not minQty + k × qtyStep, or above maxQtyPerOrder. details: { rule, minQty, qtyStep, maxQtyPerOrder, currentQty, suggestedQty }.
422CART_LINE_LIMIT_REACHEDThe cart already has 100 lines (details.limit).

Rules & side effects

  1. Merge. A variant appears once in the cart; adding it again adds to the existing line, and the limits and the stock check apply to the resulting quantity.
  2. No silent clamping. A request that cannot be satisfied in full is refused; the cart is unchanged. The client keeps the stepper value and shows the message (decision: rejecting is clearer than adjusting a quantity the reseller did not choose, and the adjusted value would reveal the stock level).
  3. Stock is checked against the available quantity (physical − reserved) of the sellable warehouses at the time of the call; it is not reserved — reservation happens at the back-office confirmation (stock.reserveOn = VALIDATED).
  4. The line records the unit price it was added at (CartItem.unitPriceSnapshot), used to detect PRICE_CHANGED later.
  5. A PENDING_VALIDATION reseller may fill the cart (prices per catalog.pricesVisibleBeforeValidation); the checkout waits for validation.

Used by — RA-12 (card cart button, adds minQty), RA-14 (« Ajouter au panier », « Commander maintenant »), RA-16, RA-25, RW-03, RW-04, RW-05.

PATCH /cart/items/{itemId}

cart.items.update · Access session — reseller (self) · Apps RA, RW · Idempotency-Key optional · Release V1 · Requirements ORD-01, ORD-02

Purpose. Sets the quantity of a line (the − / + stepper of the cart, or a typed quantity).

Request

interface UpdateCartItemRequest {
  qty: number;                        // the new absolute quantity (≥ 1; use DELETE to remove)
}
PATCH /v1/cart/items/01a1a594-44c0-7ae1-8bfd-7cd22b6eecc5
{ "qty": 20 }

Response — 200 OK: the CartView (example: the X89 line goes from 10 to 20).

{
  "id": "01a1396c-e400-70fe-bda9-3dc62fcad676",
  "updatedAt": "2026-11-04T08:02:30Z",
  "itemsCount": 2,
  "unitsCount": 30,
  "lines": [
    {
      "id": "01a1a592-7000-7a48-9b51-c3aff054ef28",
      "kind": "VARIANT",
      "product": { "id": "01a08ac2-8900-7f6d-937c-c81b37e89a5b", "name": "HOCO N32 Chargeur rapide 30W (USB-C + USB-A)", "brandName": "HOCO", "imageUrl": "https://cdn.hoco.dz/products/n32/n32-black-1-240.webp" },
      "variant": { "id": "01a08ac2-8ce8-7069-8328-28fd7158a572", "sku": "N32_BLACK_EU", "label": "Noir" },
      "qty": 10,
      "limits": { "minQty": 1, "qtyStep": 1, "maxQtyPerOrder": 20 },
      "price": { "ht": 209244, "ttc": 249000, "vatRate": 1900 },
      "promotionApplied": null,
      "lineTotals": { "subtotalHt": 2092440, "discountHt": 0, "totalHt": 2092440, "vat": 397560, "totalTtc": 2490000 },
      "availability": "IN_STOCK",
      "addedAt": "2026-11-04T08:00:00Z",
      "issues": []
    },
    {
      "id": "01a1a594-44c0-7ae1-8bfd-7cd22b6eecc5",
      "kind": "VARIANT",
      "product": { "id": "01a08ac7-1ce0-7b52-8b6a-3ec2aaccb8fb", "name": "HOCO X89 Câble USB-C", "brandName": "HOCO", "imageUrl": "https://cdn.hoco.dz/products/x89/x89-black-1-240.webp" },
      "variant": { "id": "01a08ac7-20c8-70bf-b753-d181dfe71ff6", "sku": "X89_BLACK_1M", "label": "Noir · 1 m" },
      "qty": 20,
      "limits": { "minQty": 5, "qtyStep": 5, "maxQtyPerOrder": 200 },
      "price": { "ht": 105042, "ttc": 125000, "vatRate": 1900 },
      "promotionApplied": null,
      "lineTotals": { "subtotalHt": 2100840, "discountHt": 0, "totalHt": 2100840, "vat": 399160, "totalTtc": 2500000 },
      "availability": "IN_STOCK",
      "addedAt": "2026-11-04T08:02:00Z",
      "issues": []
    }
  ],
  "totals": { "subtotalHt": 4193280, "discountTotal": 0, "deliveryFee": 0, "vatTotal": 796720, "totalTtc": 4990000, "currency": "DZD" },
  "displayTotals": { "itemsTtc": 4990000, "discountTtc": 0, "deliveryFeeTtc": 0, "totalTtc": 4990000 },
  "appliedPromotions": [],
  "delivery": { "addressId": "01a12f63-efa8-7f36-87fc-e386a4e58a22", "feePolicy": "FREE", "isFree": true, "freeThresholdTtc": null },
  "minimumOrder": { "amountTtc": 1000000, "met": true, "missingTtc": 0 },
  "issues": [],
  "canCheckout": true
}

Errors

StatusCodeWhen
400VALIDATION_FAILEDqty missing, 0 or negative.
404NOT_FOUNDNo such line in my cart (already removed from another device). The client reloads the cart.
422QUANTITY_NOT_ALLOWEDAs for cart.items.add (details.suggestedQty = nearest allowed quantity).
422STOCK_INSUFFICIENTIncreasing beyond what is available (details without quantity). Decreasing is always accepted.
422PRODUCT_UNAVAILABLEIncreasing a line whose product is no longer orderable (remove it instead).

Rules & side effects

  1. Decreasing a quantity never fails on stock; increasing re-checks availability.
  2. Any successful update refreshes the line's price snapshot, which clears its PRICE_CHANGED warning — sending the same quantity is the way to acknowledge a price change.
  3. Concurrent edits from the app and the web: last write wins per line; each response carries the full cart, so the client always renders the server state.

Used by — quantity steppers of RA-17 and RW-06.

DELETE /cart/items/{itemId}

cart.items.remove · Access session — reseller (self) · Apps RA, RW · Idempotency-Key optional · Release V1 · Requirements ORD-01

Purpose. Removes a line (the bin icon of reference screen 5).

Request — path itemId; no body.

Response — 200 OK: the CartView. Example: the N32 line is removed from a cart holding N32 × 10 and X89 × 5 — the rest is below the minimum.

{
  "id": "01a1396c-e400-70fe-bda9-3dc62fcad676",
  "updatedAt": "2026-11-04T09:15:00Z",
  "itemsCount": 1,
  "unitsCount": 5,
  "lines": [
    {
      "id": "01a1a594-44c0-7ae1-8bfd-7cd22b6eecc5",
      "kind": "VARIANT",
      "product": { "id": "01a08ac7-1ce0-7b52-8b6a-3ec2aaccb8fb", "name": "HOCO X89 Câble USB-C", "brandName": "HOCO", "imageUrl": "https://cdn.hoco.dz/products/x89/x89-black-1-240.webp" },
      "variant": { "id": "01a08ac7-20c8-70bf-b753-d181dfe71ff6", "sku": "X89_BLACK_1M", "label": "Noir · 1 m" },
      "qty": 5,
      "limits": { "minQty": 5, "qtyStep": 5, "maxQtyPerOrder": 200 },
      "price": { "ht": 105042, "ttc": 125000, "vatRate": 1900 },
      "promotionApplied": null,
      "lineTotals": { "subtotalHt": 525210, "discountHt": 0, "totalHt": 525210, "vat": 99790, "totalTtc": 625000 },
      "availability": "IN_STOCK",
      "addedAt": "2026-11-04T08:02:00Z",
      "issues": []
    }
  ],
  "totals": { "subtotalHt": 525210, "discountTotal": 0, "deliveryFee": 0, "vatTotal": 99790, "totalTtc": 625000, "currency": "DZD" },
  "displayTotals": { "itemsTtc": 625000, "discountTtc": 0, "deliveryFeeTtc": 0, "totalTtc": 625000 },
  "appliedPromotions": [],
  "delivery": { "addressId": "01a12f63-efa8-7f36-87fc-e386a4e58a22", "feePolicy": "FREE", "isFree": true, "freeThresholdTtc": null },
  "minimumOrder": { "amountTtc": 1000000, "met": false, "missingTtc": 375000 },
  "issues": [
    {
      "code": "ORDER_MIN_AMOUNT_NOT_MET",
      "severity": "BLOCKING",
      "message": "Plus que 3 750,00 DA pour atteindre le minimum de commande de 10 000,00 DA.",
      "details": { "minAmount": 1000000, "currentAmount": 625000, "missingAmount": 375000 }
    }
  ],
  "canCheckout": false
}

Errors

StatusCodeWhen
404NOT_FOUNDNo such line in my cart.

Rules & side effects — the CartItem is deleted; promotions and the minimum are re-evaluated for the remaining lines.

Used by — RA-17, RW-06.

DELETE /cart

cart.clear · Access session — reseller (self) · Apps RA, RW · Idempotency-Key optional · Release V1 · Requirements ORD-01

Purpose. Empties the cart (the bin in the cart header), after the client's confirmation dialog « Vider le panier ? ».

Request — no body.

Response — 200 OK: the empty CartView.

{
  "id": "01a1396c-e400-70fe-bda9-3dc62fcad676",
  "updatedAt": "2026-11-04T09:20:00Z",
  "itemsCount": 0,
  "unitsCount": 0,
  "lines": [],
  "totals": { "subtotalHt": 0, "discountTotal": 0, "deliveryFee": 0, "vatTotal": 0, "totalTtc": 0, "currency": "DZD" },
  "displayTotals": { "itemsTtc": 0, "discountTtc": 0, "deliveryFeeTtc": 0, "totalTtc": 0 },
  "appliedPromotions": [],
  "delivery": { "addressId": "01a12f63-efa8-7f36-87fc-e386a4e58a22", "feePolicy": "FREE", "isFree": true, "freeThresholdTtc": null },
  "minimumOrder": { "amountTtc": 1000000, "met": false, "missingTtc": 1000000 },
  "issues": [],
  "canCheckout": false
}

Errors — none specific (clearing an empty cart is a no-op that returns the same body).

Rules & side effects — all CartItem rows deleted; the Cart row is kept. No event.

Used by — RA-17, RW-06.

GET /cart/checkout-options

cart.checkoutOptions · Access session — reseller (self) · Apps RA, RW · Idempotency-Key — · Release V1 · Requirements FIN-02, FIN-05, DLV-06

Purpose. Everything the checkout step needs for the current cart: delivery addresses with their fee and resulting total, the payment methods with their availability (credit checked against the available credit), the minimum and the exact total to confirm.

Request — no parameters.

Response — 200 OK, Cache-Control: private, no-store.

interface CheckoutOptions {
  cartUpdatedAt: Iso;                 // the cart state these options were computed for
  addresses: CheckoutAddress[];       // default first
  paymentMethods: PaymentMethodOption[];
  credit: {
    limit: Money;
    used: Money;                      // exposure, see finance.summary
    available: Money;
    blocked: boolean;
    overLimitPolicy: "BLOCK" | "REQUIRE_APPROVAL";
  } | null;                           // null: no credit line (limit 0)
  minimumOrder: { amountTtc: Money; met: boolean; missingTtc: Money };
  noteMaxLength: number;              // 500
  canCheckout: boolean;               // same as cart.get
  issues: Issue[];                    // cart-level issues, as in cart.get
}

interface CheckoutAddress extends Address {      // shared Address
  deliverable: boolean;               // false: wilaya not served (`delivery.servedWilayas`)
  deliveryFee: Price;                 // 0 when free
  totalTtc: Money;                    // cart total with this address — send it as expectedTotalTtc
}

interface PaymentMethodOption {
  method: "CASH_ON_DELIVERY" | "BANK_TRANSFER" | "CCP" | "BARIDIMOB" | "CREDIT";
  enabled: boolean;
  disabledReason: "CREDIT_LIMIT_EXCEEDED" | "CREDIT_BLOCKED" | "NO_CREDIT_LINE" | null;
  requiresApproval: boolean;          // CREDIT over the limit under REQUIRE_APPROVAL: allowed, flagged for finance
  requiresDeclaration: boolean;       // BANK_TRANSFER, CCP, BARIDIMOB: pay, then declare with proof (finance page)
  instructions: string | null;        // localised text from settings (HOCO account to pay to)
}
{
  "cartUpdatedAt": "2026-11-03T08:05:00Z",
  "addresses": [
    {
      "id": "01a12f63-efa8-7f36-87fc-e386a4e58a22",
      "label": "Magasin",
      "recipientName": "Mohamed Benali",
      "phone": "+213555123456",
      "wilayaCode": "31",
      "wilayaName": "Oran",
      "communeId": "01a0c3e1-5d20-7a10-8b31-310300000003",
      "communeName": "Bir El Djir",
      "addressLine": "Cité USTO, local n° 14",
      "landmark": "En face de la pharmacie Benyahia",
      "location": { "lat": 35.7079, "lng": -0.5821, "accuracy": 18, "capturedAt": "2026-11-03T08:10:52Z" },
      "isDefault": true,
      "deliverable": true,
      "deliveryFee": { "ht": 0, "ttc": 0, "vatRate": 1900 },
      "totalTtc": 8038000
    },
    {
      "id": "01a159ef-08c0-7ea9-bb81-447b0125a0b9",
      "label": "Dépôt",
      "recipientName": "Samir Benali",
      "phone": "+213770456789",
      "wilayaCode": "31",
      "wilayaName": "Oran",
      "communeId": "01a0c3e1-5d20-7a10-8b31-310100000001",
      "communeName": "Oran",
      "addressLine": "12, rue Larbi Ben M'hidi",
      "isDefault": false,
      "deliverable": true,
      "deliveryFee": { "ht": 0, "ttc": 0, "vatRate": 1900 },
      "totalTtc": 8038000
    }
  ],
  "paymentMethods": [
    { "method": "CASH_ON_DELIVERY", "enabled": true, "disabledReason": null, "requiresApproval": false, "requiresDeclaration": false, "instructions": null },
    { "method": "BANK_TRANSFER", "enabled": true, "disabledReason": null, "requiresApproval": false, "requiresDeclaration": true, "instructions": "Virement au compte HOCO indiqué sur la facture pro forma, puis déclarez le paiement avec le reçu dans Compte › Paiements." },
    { "method": "CCP", "enabled": true, "disabledReason": null, "requiresApproval": false, "requiresDeclaration": true, "instructions": "Versement CCP au compte HOCO, puis déclarez le paiement avec le reçu." },
    { "method": "BARIDIMOB", "enabled": true, "disabledReason": null, "requiresApproval": false, "requiresDeclaration": true, "instructions": "Paiement BaridiMob vers le RIP HOCO, puis déclarez le paiement avec la capture d'écran." },
    { "method": "CREDIT", "enabled": true, "disabledReason": null, "requiresApproval": false, "requiresDeclaration": false, "instructions": "Montant ajouté à votre encours ; échéance indiquée sur la facture." }
  ],
  "credit": { "limit": 40000000, "used": 15000000, "available": 25000000, "blocked": false, "overLimitPolicy": "BLOCK" },
  "minimumOrder": { "amountTtc": 1000000, "met": true, "missingTtc": 0 },
  "noteMaxLength": 500,
  "canCheckout": true,
  "issues": []
}

With a cart of 300 000,00 DA and the default BLOCK policy, the credit option becomes:

{ "method": "CREDIT", "enabled": false, "disabledReason": "CREDIT_LIMIT_EXCEEDED", "requiresApproval": false, "requiresDeclaration": false, "instructions": "Crédit disponible : 250 000,00 DA. Réduisez la commande ou choisissez un autre mode de paiement." }
FieldNotes
addressesThe reseller's delivery addresses with the fee and total each one gives. A non-deliverable address is listed but cannot be chosen.
paymentMethodsOnly the methods enabled in the settings (D-03); CREDIT is listed for every reseller so the screen can explain why it is unavailable. NO_CREDIT_LINE: limit 0 (D-29); CREDIT_BLOCKED: credit account blocked by finance; CREDIT_LIMIT_EXCEEDED: used + totalTtc above limit under the BLOCK policy. Under REQUIRE_APPROVAL the option stays enabled with requiresApproval: true and the screen warns « Commande soumise à l'accord du service financier ».
creditSame values as finance.summary (creditLimit, creditUsed, creditAvailable, blocked).

Errors — none specific.

Rules & side effects — read-only. Totals here are only valid with the cart state of cartUpdatedAt; the checkout re-checks everything anyway.

Used by — RA-18 (« Validation de commande »), RA-19 (address choice), RW-07.

POST /cart/checkout

cart.checkout · Access session — reseller (self), AccountStatus ACTIVE · Apps RA, RW · Idempotency-Key required · Release V1 · Requirements ORD-03, ORD-02, ORD-14, FIN-05, STK-03

Purpose. « Valider la commande »: re-validates the whole cart, turns it into an order with a unique number, empties the cart and returns the order — in SUBMITTED, waiting for the back-office confirmation.

Request

HeaderRequiredNotes
Idempotency-KeyyesOne UUID per tap on « Valider la commande », reused on every retry of that tap
interface CheckoutRequest {
  addressId: Uuid;                    // one of my addresses, deliverable
  paymentMethod: "CASH_ON_DELIVERY" | "BANK_TRANSFER" | "CCP" | "BARIDIMOB" | "CREDIT";
  note?: string;                      // ≤ 500 characters — « Note pour la commande »
  expectedTotalTtc: Money;            // the total the reseller saw and accepted
}
{
  "addressId": "01a12f63-efa8-7f36-87fc-e386a4e58a22",
  "paymentMethod": "CASH_ON_DELIVERY",
  "note": "Merci d'appeler avant la livraison, le magasin ferme à 13h le vendredi.",
  "expectedTotalTtc": 8038000
}

Response — 201 Created, Location: /v1/orders/01a1a0c0-4e80-7095-bb23-dcbea2c9e1e5: the OrderDetail of the new order.

{
  "id": "01a1a0c0-4e80-7095-bb23-dcbea2c9e1e5",
  "number": "CMD-2026-000158",
  "version": 1,
  "status": "SUBMITTED",
  "progressStep": 0,
  "awaitingConfirmation": "BACK_OFFICE",
  "pendingReasons": [],
  "channel": "RESELLER_APP",
  "isFirstOrder": false,
  "createdAt": "2026-11-03T09:32:00Z",
  "submittedAt": "2026-11-03T09:32:00Z",
  "validatedAt": null,
  "updatedAt": "2026-11-03T09:32:00Z",
  "currency": "DZD",
  "itemsCount": 4,
  "unitsCount": 41,
  "lines": [
    {
      "id": "01a1a0c0-4e80-71dd-aedc-7a6b01b8a49d",
      "product": { "id": "01a0901f-d380-7a88-9746-4ec8936c3d68", "name": "BOROFONE BW06 Écouteurs TWS", "brandName": "BOROFONE", "imageUrl": "https://cdn.hoco.dz/products/bw06/bw06-white-1-240.webp" },
      "variant": { "id": "01a0901f-d768-77a1-94f2-1bca2251bdcd", "sku": "BW06_WHITE", "label": "Blanc" },
      "qty": 6,
      "price": { "ht": 250420, "ttc": 298000, "vatRate": 1900 },
      "promotionApplied": null,
      "lineTotals": { "subtotalHt": 1502520, "discountHt": 0, "totalHt": 1502520, "vat": 285480, "totalTtc": 1788000 },
      "originalQty": null,
      "returnedQty": 0,
      "claimedQty": 0,
      "returnableQty": 0
    },
    {
      "id": "01a1a0c0-4e80-7c52-8789-bdc2d7ba3b57",
      "product": { "id": "01a08ac2-8900-7f6d-937c-c81b37e89a5b", "name": "HOCO N32 Chargeur rapide 30W (USB-C + USB-A)", "brandName": "HOCO", "imageUrl": "https://cdn.hoco.dz/products/n32/n32-black-1-240.webp" },
      "variant": { "id": "01a08ac2-8ce8-7069-8328-28fd7158a572", "sku": "N32_BLACK_EU", "label": "Noir" },
      "qty": 10,
      "price": { "ht": 209244, "ttc": 249000, "vatRate": 1900 },
      "promotionApplied": null,
      "lineTotals": { "subtotalHt": 2092440, "discountHt": 0, "totalHt": 2092440, "vat": 397560, "totalTtc": 2490000 },
      "originalQty": null,
      "returnedQty": 0,
      "claimedQty": 0,
      "returnableQty": 0
    },
    {
      "id": "01a1a0c0-4e80-7e8e-8f06-3842302674ed",
      "product": { "id": "01a08ac7-1ce0-7b52-8b6a-3ec2aaccb8fb", "name": "HOCO X89 Câble USB-C", "brandName": "HOCO", "imageUrl": "https://cdn.hoco.dz/products/x89/x89-black-1-240.webp" },
      "variant": { "id": "01a08ac7-20c8-70bf-b753-d181dfe71ff6", "sku": "X89_BLACK_1M", "label": "Noir · 1 m" },
      "qty": 20,
      "price": { "ht": 105042, "ttc": 125000, "vatRate": 1900 },
      "promotionApplied": null,
      "lineTotals": { "subtotalHt": 2100840, "discountHt": 0, "totalHt": 2100840, "vat": 399160, "totalTtc": 2500000 },
      "originalQty": null,
      "returnedQty": 0,
      "claimedQty": 0,
      "returnableQty": 0
    },
    {
      "id": "01a1a0c0-4e80-75c9-a6c9-7ca88b6f19f9",
      "product": { "id": "01a15889-fa80-7ff3-b294-f863a2ee34a6", "name": "HOCO J86A Power Bank 10000mAh", "brandName": "HOCO", "imageUrl": "https://cdn.hoco.dz/products/j86a/j86a-black-1-240.webp" },
      "variant": { "id": "01a15889-fe68-733d-8056-75eec8b226f5", "sku": "J86A_BLACK", "label": "Noir" },
      "qty": 5,
      "price": { "ht": 302521, "ttc": 360000, "vatRate": 1900 },
      "promotionApplied": {
        "promotionId": "01a19cfd-2c00-78b5-80c8-b4b9fb167f17",
        "label": "Offre flash -30 %",
        "type": "PERCENT_OFF",
        "value": 3000,
        "unitPrice": { "ht": 211765, "ttc": 252000, "vatRate": 1900 },
        "endsAt": "2026-11-03T22:59:59Z"
      },
      "lineTotals": { "subtotalHt": 1512605, "discountHt": 453780, "totalHt": 1058825, "vat": 201175, "totalTtc": 1260000 },
      "originalQty": null,
      "returnedQty": 0,
      "claimedQty": 0,
      "returnableQty": 0
    }
  ],
  "totals": { "subtotalHt": 7208405, "discountTotal": 453780, "deliveryFee": 0, "vatTotal": 1283375, "totalTtc": 8038000, "currency": "DZD" },
  "displayTotals": { "itemsTtc": 8578000, "discountTtc": 540000, "deliveryFeeTtc": 0, "totalTtc": 8038000 },
  "appliedPromotions": [
    { "promotionId": "01a19cfd-2c00-78b5-80c8-b4b9fb167f17", "label": "Offre flash -30 %", "type": "PERCENT_OFF", "lineIds": ["01a1a0c0-4e80-75c9-a6c9-7ca88b6f19f9"], "discountHt": 453780, "discountTtc": 540000 }
  ],
  "address": {
    "id": "01a12f63-efa8-7f36-87fc-e386a4e58a22",
    "label": "Magasin",
    "recipientName": "Mohamed Benali",
    "phone": "+213555123456",
    "wilayaCode": "31",
    "wilayaName": "Oran",
    "communeId": "01a0c3e1-5d20-7a10-8b31-310300000003",
    "communeName": "Bir El Djir",
    "addressLine": "Cité USTO, local n° 14",
    "landmark": "En face de la pharmacie Benyahia",
    "location": { "lat": 35.7079, "lng": -0.5821, "accuracy": 18, "capturedAt": "2026-11-03T08:10:52Z" },
    "isDefault": true
  },
  "note": "Merci d'appeler avant la livraison, le magasin ferme à 13h le vendredi.",
  "paymentMethod": "CASH_ON_DELIVERY",
  "paymentStatus": {
    "invoiced": false,
    "invoiceId": null,
    "invoiceNumber": null,
    "invoiceStatus": null,
    "dueDate": null,
    "amountPaid": 0,
    "creditNotesTotal": 0,
    "balance": 8038000,
    "pendingVerificationAmount": 0
  },
  "delivery": {
    "status": null,
    "mode": null,
    "carrierName": null,
    "trackingNumber": null,
    "trackingUrl": null,
    "driverName": null,
    "driverPhone": null,
    "deliveryNoteNumber": null,
    "shippedAt": null,
    "deliveredAt": null,
    "recipientName": null
  },
  "statusHistory": [
    { "from": null, "to": "SUBMITTED", "at": "2026-11-03T09:32:00Z", "actor": { "id": "01a19c3e-5a10-7b21-8f4e-2c6d9a1b3e57", "displayName": "Mohamed Benali" } }
  ],
  "changes": [],
  "documents": [
    { "type": "INVOICE", "number": null, "available": false, "generatedAt": null },
    { "type": "DELIVERY_NOTE", "number": null, "available": false, "generatedAt": null }
  ],
  "assisted": { "isAssisted": false, "delegate": null, "confirmation": null, "expiresAt": null, "confirmedAt": null, "rejectionReason": null },
  "cancellation": null,
  "refusal": null,
  "returns": [],
  "claims": [],
  "returnableUntil": null,
  "claimableUntil": null,
  "allowedActions": ["CANCEL", "CONTACT_SUPPORT"]
}

The confirmation screen (RA-20) uses number, displayTotals.totalTtc and awaitingConfirmation for its copy: « Commande CMD-2026-000158 envoyée. Elle sera confirmée par l'équipe HOCO avant sa préparation ; vous serez notifié à chaque étape. »

Errors — checked in this order; the first failing check answers.

StatusCodeWhen
400IDEMPOTENCY_KEY_REQUIREDHeader missing.
400VALIDATION_FAILEDField codes: REQUIRED, TOO_LONG (note), INVALID_ENUM (paymentMethod), UNKNOWN_ADDRESS (addressId is not one of my addresses).
403ACCOUNT_PENDING_VALIDATIONAccountStatus PENDING_VALIDATION — the app shows RA-06.
403RESELLER_SUSPENDEDResellerStatus SUSPENDED (commercial block).
409IDEMPOTENCY_KEY_REUSED · IDEMPOTENCY_IN_PROGRESSSame key with another body · the first request is still running (retry after 1 s).
422CART_EMPTYNo line.
422PRODUCT_UNAVAILABLE · QUANTITY_NOT_ALLOWED · STOCK_INSUFFICIENT · ORDER_MIN_AMOUNT_NOT_METThe cart has a blocking issue; the code is that of the first blocking issue in this order. details: { issues: Issue[], cart: CartView } — the client renders details.cart at once.
422ADDRESS_NOT_DELIVERABLEThe chosen address is in a wilaya not served.
422PAYMENT_METHOD_NOT_ALLOWEDMethod disabled in the settings, or CREDIT without a credit line or with a blocked credit account (details.reason: METHOD_DISABLED, NO_CREDIT_LINE, CREDIT_BLOCKED).
409CART_CHANGEDThe re-validated total differs from expectedTotalTtc (price change, promotion ended, delivery fee). details: { expectedTotalTtc, actualTotalTtc, cart: CartView }; nothing is created. The client shows the new cart with « Votre panier a été mis à jour, vérifiez le nouveau total » and lets the reseller confirm again with a new Idempotency-Key.
422CREDIT_LIMIT_EXCEEDEDCREDIT chosen, used + totalTtc above the limit, policy BLOCK. details: { limit, used, available, required }.

Rules & side effects — all in one database transaction (cart row locked FOR UPDATE):

  1. Re-validation of every line exactly as in cart.get (visibility, customer-type price, promotions with their dates, targets and caps, quantity rules, availability), then the minimum, the address, the payment method and the credit.
  2. Order creation. Number CMD-YYYY-NNNNNN from NumberSequence (year in Africa/Algiers, no gaps); Order with channel RESELLER_APP (request with X-App-Platform) or RESELLER_WEB; OrderItem snapshots of product name, SKU, variant label, image, unit price HT/TTC, VAT rate, promotion and line totals; address snapshot; payment method and note; OrderStatusChange null → SUBMITTED with the reseller as actor and ClientApp.
  3. Always SUBMITTED. No order is confirmed automatically (ORD-14): the order enters the back-office queue « Commandes à confirmer » (admin.orders.toConfirm, BO-41) and only a user with orders.validate moves it on. Under the REQUIRE_APPROVAL credit policy an over-limit CREDIT order is still created SUBMITTED; its credit control is flagged for finance in the queue, and the back-office may put it on hold (PENDING), which the reseller then sees live.
  4. Promotions. Each used promotion's cap — and its per-reseller limit (perResellerCap, SRV-42) — is consumed atomically; if a cap runs out during the transaction the promotion is dropped, the total changes and the answer is 409 CART_CHANGED — the reseller never pays a price they did not see. A promotion whose cap reaches 0 moves to ENDED.
  5. Stock. Nothing is reserved at checkout with the default stock.reserveOn = VALIDATED (reservation happens at the back-office confirmation); with stock.reserveOn = SUBMITTED, RESERVATION movements are written here under row locks, and a shortage found at that moment answers 422 STOCK_INSUFFICIENT.
  6. Cart emptied (its lines deleted) in the same transaction.
  7. First order. When the reseller has no earlier order that is not REFUSED or CANCELLED: isFirstOrder = true, ResellerStatus → FIRST_ORDER, invitation step FIRST_ORDER, attribution to the source delegate (ORD-09).
  8. Events (outbox, after commit): order.submitted → back-office SSE order.created and the in-app alert to orders.validate holders, push and live event « Commande envoyée » to the reseller (its own action is notified so the push confirms reception), in-app to the assigned delegate — see the notifications matrix.
  9. Idempotency. A replay with the same key and body returns the original 201 and body with Idempotent-Replayed: true, even though the cart is now empty — a lost response never creates a second order.
  10. Prepaid methods. For BANK_TRANSFER, CCP and BARIDIMOB the reseller pays and declares the payment (payments.declare with orderId); whether HOCO waits for the verified payment before shipping is a back-office decision, not a checkout rule.

Used by — « Valider la commande » on RA-18, then RA-20 (« Commande envoyée »); RW-07.

POST /cart/packs

cart.packs.add · Access session — reseller (self) · Apps RA, RW · Idempotency-Key required · Release V2 · Requirements PRM-06

Purpose. Adds a profitable pack (« Packs rentables », cahier §5.5) to the cart as one unit at the pack price. Specified with the V2 contract (SRV-29); the baseline is below.

Request

interface AddPackRequest {
  packId: Uuid;
  qty?: number;                       // number of packs, default 1
}
{ "packId": "01a49fbf-a000-7f4e-99b1-a4afd2a3a603", "qty": 2 }

Response — 200 OK: the CartView, where a pack is one line of kind: "PACK":

interface PackCartLine extends Omit<CartLine, "variant" | "limits"> {
  kind: "PACK";
  variant: null;
  pack: { id: Uuid; name: string; items: { variantId: Uuid; sku: string; name: string; qtyPerPack: number }[] };
  limits: { minQty: 1; qtyStep: 1; maxQtyPerOrder: number | null };   // packs per order
  // price = pack price for the reseller's customer type; promotionApplied is always null
}
{
  "id": "01a572e4-5c00-7066-bf5a-8d3a378aa9a0",
  "updatedAt": "2027-05-12T09:01:30Z",
  "itemsCount": 2,
  "unitsCount": 12,
  "lines": [
    {
      "id": "01a5731b-4a80-7294-b27a-0030936c3e0e",
      "kind": "PACK",
      "product": { "id": "01a49fbf-a000-7f4e-99b1-a4afd2a3a603", "name": "Pack Charge & Câble", "brandName": "HOCO", "imageUrl": "https://cdn.hoco.dz/packs/charge-cable-240.webp" },
      "variant": null,
      "pack": {
        "id": "01a49fbf-a000-7f4e-99b1-a4afd2a3a603",
        "name": "Pack Charge & Câble",
        "items": [
          { "variantId": "01a08ac2-8ce8-7069-8328-28fd7158a572", "sku": "N32_BLACK_EU", "name": "HOCO N32 Chargeur rapide 30W (USB-C + USB-A)", "qtyPerPack": 6 },
          { "variantId": "01a08ac7-20c8-70bf-b753-d181dfe71ff6", "sku": "X89_BLACK_1M", "name": "HOCO X89 Câble USB-C", "qtyPerPack": 7 }
        ]
      },
      "qty": 2,
      "limits": { "minQty": 1, "qtyStep": 1, "maxQtyPerOrder": 5 },
      "price": { "ht": 1588235, "ttc": 1890000, "vatRate": 1900 },
      "promotionApplied": null,
      "lineTotals": { "subtotalHt": 3176470, "discountHt": 0, "totalHt": 3176470, "vat": 603530, "totalTtc": 3780000 },
      "availability": "IN_STOCK",
      "addedAt": "2027-05-12T09:00:00Z",
      "issues": []
    },
    {
      "id": "01a5731c-34e0-7ec2-9471-39e84c977fa7",
      "kind": "VARIANT",
      "product": { "id": "01a08ac2-8900-7f6d-937c-c81b37e89a5b", "name": "HOCO N32 Chargeur rapide 30W (USB-C + USB-A)", "brandName": "HOCO", "imageUrl": "https://cdn.hoco.dz/products/n32/n32-black-1-240.webp" },
      "variant": { "id": "01a08ac2-8ce8-7069-8328-28fd7158a572", "sku": "N32_BLACK_EU", "label": "Noir" },
      "qty": 10,
      "limits": { "minQty": 1, "qtyStep": 1, "maxQtyPerOrder": 20 },
      "price": { "ht": 209244, "ttc": 249000, "vatRate": 1900 },
      "promotionApplied": null,
      "lineTotals": { "subtotalHt": 2092440, "discountHt": 0, "totalHt": 2092440, "vat": 397560, "totalTtc": 2490000 },
      "availability": "IN_STOCK",
      "addedAt": "2027-05-12T09:01:00Z",
      "issues": []
    }
  ],
  "totals": { "subtotalHt": 5268910, "discountTotal": 0, "deliveryFee": 0, "vatTotal": 1001090, "totalTtc": 6270000, "currency": "DZD" },
  "displayTotals": { "itemsTtc": 6270000, "discountTtc": 0, "deliveryFeeTtc": 0, "totalTtc": 6270000 },
  "appliedPromotions": [],
  "delivery": { "addressId": "01a12f63-efa8-7f36-87fc-e386a4e58a22", "feePolicy": "FREE", "isFree": true, "freeThresholdTtc": null },
  "minimumOrder": { "amountTtc": 1000000, "met": true, "missingTtc": 0 },
  "issues": [],
  "canCheckout": true
}

For a pack line, product describes the pack (its card image and name) and unitsCount counts packs, not the units inside.

Errors

StatusCodeWhen
403FEATURE_DISABLEDPacks are switched off (packs.enabled = false).
404NOT_FOUNDUnknown pack or not targeted at the reseller.
422PACK_UNAVAILABLEPack outside its dates or one of its items not orderable (details.reason).
422STOCK_INSUFFICIENTThe packs requested, together with the same variants already in the cart, exceed what is available (no quantity given).
422QUANTITY_NOT_ALLOWEDMore packs than maxQtyPerOrder.

Rules & side effects — adding the same pack again increases the pack line; pack lines never receive another promotion (V2 assumption, to confirm with the stacking rules); at checkout the pack becomes one order line per item variant, all carrying the pack id, with the pack discount spread pro rata on the items (so invoices and stock stay per variant).

Used by — RA-37, RW-15.

On this page