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 UNAUTHENTICATEDhere (the app shows the sign-up prompt of RA-41). Delegates build assisted orders throughdelegate.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;canCheckoutsums 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.checkoutre-validates everything, creates the order inSUBMITTEDand 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 ».
| Method | Path | Operation | Access | Apps | Release |
|---|---|---|---|---|---|
| GET | /cart | cart.getMy cart with lines, re-validated prices/stock, applied promotions, totals and blocking issues | self | RA · RW | V1 |
| POST | /cart/items | cart.items.addAdd a variant (or increase its quantity) | selfIdempotency-Key | RA · RW | V1 |
| PATCH | /cart/items/{itemId} | cart.items.updateChange a line quantity | self | RA · RW | V1 |
| DELETE | /cart/items/{itemId} | cart.items.removeRemove a line | self | RA · RW | V1 |
| DELETE | /cart | cart.clearEmpty the cart | self | RA · RW | V1 |
| GET | /cart/checkout-options | cart.checkoutOptionsAllowed payment methods, delivery addresses, delivery fee and credit availability for this cart | self | RA · RW | V1 |
| POST | /cart/checkout | cart.checkoutSubmit the cart as an order (Idempotency-Key required); re-validates everything; returns the order | selfIdempotency-Key | RA · RW | V1 |
| POST | /cart/packs | cart.packs.addAdd a pack to the cart (V2) | selfIdempotency-Key | RA · RW | V2 |
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.

How the cart computes amounts
- Unit price. For each line, the variant's price for the reseller's
customerType(priceSuperGros,priceDemiGrosorpriceDetail, stored HT) — no quantity tiers in V1 (D-37).price.ttc = round(price.ht × (1 + vatRate / 10 000)). - 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_OFFandAMOUNT_OFFapply to the HT unit price;FIXED_PRICEreplaces it;FREE_DELIVERYsets the delivery fee to 0. - 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. - Cart totals (shared
Totals): sums of the lines, plus the delivery fee (HT, its VAT added tovatTotal);totalTtc = subtotalHt − discountTotal + deliveryFee + vatTotal.displayTotalsgives the same figures in TTC for the summary block (Sous-total, Remises, Frais de livraison, Total TTC). - Delivery fee (DLV-06, D-20): setting
delivery.feePolicy—FREE(default: every order that reaches the minimum is delivered free, shown « Offerts ») orWILAYA_TABLE(fee per wilayadelivery.feeByWilaya, HT, waived fromdelivery.freeThresholdTtcof merchandise). The cart uses the default address; checkout uses the chosen one. - 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.
| Code | Severity | Where | Raised when | What the reseller does |
|---|---|---|---|---|
PRICE_CHANGED | WARNING | line | The 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_ENDED | WARNING | line | A promotion the line had is over (details.reason: END_DATE, CAP_REACHED, PAUSED) — « Offre terminée », normal price applies | Nothing |
PRODUCT_UNAVAILABLE | BLOCKING | line | Product hidden, archived, no longer visible to the reseller, or variant switched off (details.reason: HIDDEN, ARCHIVED, NOT_VISIBLE, VARIANT_INACTIVE) | Remove the line |
STOCK_INSUFFICIENT | BLOCKING | line | The 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 given | Lower the quantity or remove the line |
QUANTITY_NOT_ALLOWED | BLOCKING | line | The 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_MET | BLOCKING | cart | Merchandise total below orders.minAmount (details: { minAmount, currentAmount, missingAmount }) — « Plus que 3 750,00 DA pour commander » | Add products |
ACCOUNT_PENDING_VALIDATION | BLOCKING | cart | AccountStatus PENDING_VALIDATION — the cart can be prepared, checkout waits for validation | Wait for validation (RA-06) |
RESELLER_SUSPENDED | BLOCKING | cart | ResellerStatus 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
}| Field | Notes |
|---|---|
lines | Oldest first, so the list does not jump when a quantity changes. |
lines[].price | Current price, not the one seen when the line was added — the change is reported by PRICE_CHANGED. |
lines[].limits | Drive the stepper: − stops at minQty, + adds qtyStep, the total of the variant stops at maxQtyPerOrder. |
totals vs displayTotals | totals 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. |
minimumOrder | Drives the progress bar « Plus que … pour commander » and the disabled « Valider la commande ». |
Errors — none specific.
Rules & side effects
- 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.
- 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. - 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
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | qty not a positive integer. |
| 404 | NOT_FOUND | Unknown variant, or its product is not visible to the reseller. |
| 422 | PRODUCT_UNAVAILABLE | Product visible but not orderable (variant switched off). |
| 422 | STOCK_INSUFFICIENT | The 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é. » |
| 422 | QUANTITY_NOT_ALLOWED | The resulting quantity breaks a limit: below minQty, not minQty + k × qtyStep, or above maxQtyPerOrder. details: { rule, minQty, qtyStep, maxQtyPerOrder, currentQty, suggestedQty }. |
| 422 | CART_LINE_LIMIT_REACHED | The cart already has 100 lines (details.limit). |
Rules & side effects
- 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.
- 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).
- 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). - The line records the unit price it was added at (
CartItem.unitPriceSnapshot), used to detectPRICE_CHANGEDlater. - A
PENDING_VALIDATIONreseller may fill the cart (prices percatalog.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
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | qty missing, 0 or negative. |
| 404 | NOT_FOUND | No such line in my cart (already removed from another device). The client reloads the cart. |
| 422 | QUANTITY_NOT_ALLOWED | As for cart.items.add (details.suggestedQty = nearest allowed quantity). |
| 422 | STOCK_INSUFFICIENT | Increasing beyond what is available (details without quantity). Decreasing is always accepted. |
| 422 | PRODUCT_UNAVAILABLE | Increasing a line whose product is no longer orderable (remove it instead). |
Rules & side effects
- Decreasing a quantity never fails on stock; increasing re-checks availability.
- Any successful update refreshes the line's price snapshot, which clears its
PRICE_CHANGEDwarning — sending the same quantity is the way to acknowledge a price change. - 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
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | No such line in my cart. |
Rules & side effects — the CartItem is deleted; promotions and the minimum are re-evaluated for the remaining lines.
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.
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." }| Field | Notes |
|---|---|
addresses | The reseller's delivery addresses with the fee and total each one gives. A non-deliverable address is listed but cannot be chosen. |
paymentMethods | Only 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 ». |
credit | Same 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
| Header | Required | Notes |
|---|---|---|
Idempotency-Key | yes | One 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.
| Status | Code | When |
|---|---|---|
| 400 | IDEMPOTENCY_KEY_REQUIRED | Header missing. |
| 400 | VALIDATION_FAILED | Field codes: REQUIRED, TOO_LONG (note), INVALID_ENUM (paymentMethod), UNKNOWN_ADDRESS (addressId is not one of my addresses). |
| 403 | ACCOUNT_PENDING_VALIDATION | AccountStatus PENDING_VALIDATION — the app shows RA-06. |
| 403 | RESELLER_SUSPENDED | ResellerStatus SUSPENDED (commercial block). |
| 409 | IDEMPOTENCY_KEY_REUSED · IDEMPOTENCY_IN_PROGRESS | Same key with another body · the first request is still running (retry after 1 s). |
| 422 | CART_EMPTY | No line. |
| 422 | PRODUCT_UNAVAILABLE · QUANTITY_NOT_ALLOWED · STOCK_INSUFFICIENT · ORDER_MIN_AMOUNT_NOT_MET | The 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. |
| 422 | ADDRESS_NOT_DELIVERABLE | The chosen address is in a wilaya not served. |
| 422 | PAYMENT_METHOD_NOT_ALLOWED | Method 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). |
| 409 | CART_CHANGED | The 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. |
| 422 | CREDIT_LIMIT_EXCEEDED | CREDIT 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):
- 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. - Order creation. Number
CMD-YYYY-NNNNNNfromNumberSequence(year inAfrica/Algiers, no gaps);Orderwith channelRESELLER_APP(request withX-App-Platform) orRESELLER_WEB;OrderItemsnapshots of product name, SKU, variant label, image, unit price HT/TTC, VAT rate, promotion and line totals; address snapshot; payment method and note;OrderStatusChangenull → SUBMITTEDwith the reseller as actor andClientApp. - 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 withorders.validatemoves it on. Under theREQUIRE_APPROVALcredit policy an over-limitCREDITorder is still createdSUBMITTED; 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. - 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 is409 CART_CHANGED— the reseller never pays a price they did not see. A promotion whose cap reaches 0 moves toENDED. - Stock. Nothing is reserved at checkout with the default
stock.reserveOn = VALIDATED(reservation happens at the back-office confirmation); withstock.reserveOn = SUBMITTED,RESERVATIONmovements are written here under row locks, and a shortage found at that moment answers422 STOCK_INSUFFICIENT. - Cart emptied (its lines deleted) in the same transaction.
- First order. When the reseller has no earlier order that is not
REFUSEDorCANCELLED:isFirstOrder = true, ResellerStatus →FIRST_ORDER, invitation stepFIRST_ORDER, attribution to the source delegate (ORD-09). - Events (outbox, after commit):
order.submitted→ back-office SSEorder.createdand the in-app alert toorders.validateholders, 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. - Idempotency. A replay with the same key and body returns the original
201and body withIdempotent-Replayed: true, even though the cart is now empty — a lost response never creates a second order. - Prepaid methods. For
BANK_TRANSFER,CCPandBARIDIMOBthe reseller pays and declares the payment (payments.declarewithorderId); 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
| Status | Code | When |
|---|---|---|
| 403 | FEATURE_DISABLED | Packs are switched off (packs.enabled = false). |
| 404 | NOT_FOUND | Unknown pack or not targeted at the reseller. |
| 422 | PACK_UNAVAILABLE | Pack outside its dates or one of its items not orderable (details.reason). |
| 422 | STOCK_INSUFFICIENT | The packs requested, together with the same variants already in the cart, exceed what is available (no quantity given). |
| 422 | QUANTITY_NOT_ALLOWED | More 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).
Catalog & favorites
Home aggregate, brands, categories, product search with advanced filters and facets, product detail, barcode lookup, similar products and favorites — for guests, resellers and delegates.
Reseller orders
The reseller's orders — list, status counters, detail with live tracking, cancellation, assisted-order confirmation or rejection, reorder, documents — and V1 return requests on delivered orders.