Pricing
Three selling prices per variant chosen by the reseller's customer type, purchase and recommended prices with their history, VAT and totals, re-validation at cart and checkout, and the V2 per-reseller agreements.
This area defines what each reseller pays (cahier §4.3, §4.7, §5.1, §5.3, §8, reference screen 5, and the pricing decision of 17 Oct 2026). Every product variant carries three selling prices, one per customer type — Super-gros, Demi-gros, Détail — plus its VAT rate, a recommended retail price and the restricted purchase price. The reseller's customer type (AUTH-11) selects one of the three; promotions apply on top (PRM-04); guests see no price (AUTH-10). Segment price lists and quantity tiers are not part of V1: per-reseller agreements move to V2 (PRC-03) and tiers are dropped (D-37). The server's pricing module owns the resolver and PriceChange; the cart module applies it (API › Cart & checkout); prices are edited in the product editor (API › Catalog & pricing).
| ID | Requirement | Release | Priority | Source | Apps |
|---|---|---|---|---|---|
PRC-01 | Three selling prices per variant — super-gros, demi-gros, détail — plus purchase price and recommended retail price, with change history | V1 | MUST | §4.3, §8 | API · BO |
PRC-02 | Price resolution: the reseller's customer type selects one of the three prices; then promotions; guests see no price | V1 | MUST | Team decision (17 Oct 2026), §4.3, §8 | API |
PRC-03 | Per-reseller price agreements and level-based prices on top of the three customer-type prices | V2 | SHOULD | §8 | API · BO |
PRC-04 | Taxes: VAT rate per product, HT/VAT/TTC totals on documents | V1 | MUST | §4.7, reference screen 5 | API |
PRC-05 | Prices and stock re-validated at cart and at checkout; the reseller only ever sees their own price | V1 | MUST | §5.1, §5.3 | API · RA · RW |
Settings used in this area
| Setting | Default | Meaning |
|---|---|---|
pricing.defaultVatRate | 1900 | VAT rate (basis points) proposed for a new variant. |
pricing.priceDisplay | TTC | Which value the apps show large, TTC or HT (D-17). |
catalog.pricesVisibleBeforeValidation | true | Pending accounts see their declared type's prices (D-18). |
Requirements
PRC-01 — Three selling prices, purchase and recommended prices, with history
Statement — The system shall store, for every product variant, three selling prices HT — super-gros, demi-gros and détail — together with its VAT rate, the recommended retail price and the purchase price, and keep the history of every change.
Rules
- Price fields of
ProductVariant, all integer centimes:priceSuperGros,priceDemiGros,priceDetail(HT, required, greater than 0),vatRate(basis points),recommendedPrice(prix conseillé public, TTC, optional),purchasePrice(prix d'achat, HT, optional, restricted). - The three selling prices must be ordered
priceSuperGros ≤ priceDemiGros ≤ priceDetail(422 PRICE_ORDER_INVALID). A recommended price lower than the Détail TTC price is accepted with a warning in the editor. - Writing any price field needs
pricing.update(on top ofcatalog.update); readingpurchasePrice, margins and the history needspricing.view(ACL-04). The editor (BO-28) shows the three prices in HT and TTC and, withpricing.view, the margin of each type in % ((price − purchase) ÷ price). - Every change writes one
PriceChangerow per field (PRICE_SUPER_GROS,PRICE_DEMI_GROS,PRICE_DETAIL,RECOMMENDED_PRICE,PURCHASE_PRICE,VAT_RATE) with the old and new values, the actor, the source (MANUAL,IMPORT) and an optional reason. Rows are append-only and kept for archived variants (admin.variants.priceHistory). - A change applies to the next pricing: carts are re-priced at their next read (line issue
PRICE_CHANGED), catalog reads show the new price at once, orders already submitted keep their snapshot prices, and an order waiting for confirmation shows the difference in its price control as information. - Prices are entered one variant at a time in BO-28 or in bulk through the product import (CAT-06); effective-dated (scheduled) price changes are not in V1.
Acceptance criteria
- Saving 32 000 / 35 000 / 39 000 HT for super-gros / demi-gros / détail succeeds; 35 000 / 32 000 / 39 000 returns
422 PRICE_ORDER_INVALID. - A stock manager saving
priceDetailgets403 PERMISSION_DENIED; a finance manager succeeds and threePriceChangerows appear when the three prices change. admin.variants.priceHistoryanswers403 PERMISSION_DENIEDto a user withoutpricing.view.- After the Demi-gros price changes, a demi-gros reseller's cart line shows
PRICE_CHANGEDwith the old and new prices; an order submitted before keeps the old price.
Data — ProductVariant (price fields), PriceChange, AuditLog.
Interfaces
- Screens: BO-28 Product editor · BO-31 Price grid — three customer-type prices (bulk edit)
- API:
admin.variants.crudPOST · PATCH · DELETE /admin/products/{productId}/variants[/{variantId}]·admin.variants.priceHistoryGET /admin/variants/{variantId}/price-history
Notes / open points — Who sets the prices, whether HOCO's grid is HT or TTC, and whether price changes need scheduling are D-01.
PRC-02 — Price resolution by customer type, then promotions
Statement — The system shall give every signed-in reseller exactly one unit price per variant — the price of their customer type — then apply the eligible promotion, and give guests no price.
Rules
- Resolution, for caller C, variant V and quantity q at time t:
- C is a guest (or a pending account while
catalog.pricesVisibleBeforeValidationis false) →price: null,priceLocked: true. Stop. - Customer type: the reseller's
customerType(for a delegate, that of the reseller given inresellerId). A reseller without a type cannot be priced: catalog prices arenulland the cart cannot be used until the back-office sets one. - Base unit price HT
U=priceSuperGros,priceDemiGrosorpriceDetailof V. No quantity tier, no price list in V1. - Promotion: among the promotions active at t, targeted at C and covering V (PRM-02), the stacking rules (PRM-03) select the one to apply; its unit price HT
P=round(U × (10 000 − bps) ÷ 10 000)forPERCENT_OFF,U − amountforAMOUNT_OFF,min(U, fixed)forFIXED_PRICE; never below 0. Without a promotionP = U. - Unit TTC =
round(P × (10 000 + vatRate) ÷ 10 000), rounded half-up to the centime. - Line:
subtotalHt = U × q;discountHt = (U − P) × q;totalHt = P × q;totalTtc = unit TTC × q;vat = totalTtc − totalHt— so the lines add up to the total to the centime.
- C is a guest (or a pending account while
- The resolver is one function in the
pricingmodule used by the catalog, the cart, checkout, assisted orders, back-office orders on behalf and the promotion preview; no other code computes a price. - The other two prices, the purchase price and any other reseller's price never leave the server in a reseller or delegate response (PRC-05).
- A change of customer type (AUTH-11) or of a variant price applies to the next resolution; submitted orders keep their snapshot.
Worked example — variant HOCO X14 câble USB-C 1 m: priceSuperGros 32 000, priceDemiGros 35 000, priceDetail 39 000 (HT, centimes), vatRate 1900; a promotion -10 % câbles (PERCENT_OFF 1000) targets the reseller.
| Caller | Base U (HT) | Promotion P (HT) | Unit TTC | q | subtotalHt | discountHt | totalHt | totalTtc | vat |
|---|---|---|---|---|---|---|---|---|---|
| Guest | — | — | null (lock) | — | — | — | — | — | — |
| Super-gros reseller, no promotion | 32 000 | 32 000 | 38 080 | 24 | 768 000 | 0 | 768 000 | 913 920 | 145 920 |
| Demi-gros reseller with the promotion | 35 000 | 31 500 | 37 485 | 24 | 840 000 | 84 000 | 756 000 | 899 640 | 143 640 |
| Détail reseller, no promotion | 39 000 | 39 000 | 46 410 | 24 | 936 000 | 0 | 936 000 | 1 113 840 | 177 840 |
Rounding case: a Détail price of 41 990 HT gives a unit TTC of round(49 968.1) = 49 968; for 3 units totalHt = 125 970, totalTtc = 149 904 and vat = 23 934 (the exact 19 % would be 23 934.3 — the difference absorbs the rounding).
Acceptance criteria
- With the example data, a Demi-gros reseller's cart line of 24 cables shows unit TTC 37 485 and line total 899 640 TTC, and the same reseller's catalog card shows 41 650 with the promotion price 37 485.
- The same product for a guest returns
price: nullandpriceLocked: true. - A delegate preparing an assisted order for a Détail reseller sees 46 410 TTC per unit.
- After the back-office changes the reseller from Demi-gros to Super-gros, the next
cart.getprices the line at 38 080 TTC per unit withPRICE_CHANGED. - No reseller or delegate response contains
priceSuperGros,priceDemiGros,priceDetailorpurchasePrice(contract test on every reseller and delegate schema).
Data — ProductVariant, Reseller (customerType), Promotion, PromotionScope, PromotionAudience, CartItem, OrderItem (snapshots).
Interfaces
- Screens: RA-41 Guest mode — price lock and sign-up prompts · BO-31 Price grid — three customer-type prices (bulk edit)
- API:
catalog.products.listGET /catalog/products·catalog.products.getGET /catalog/products/{productId}·admin.variants.tiersPUT /admin/variants/{variantId}/quantity-tiers·admin.priceLists.crudGET · POST · PATCH · DELETE /admin/price-lists[/{priceListId}]·admin.priceLists.itemsPUT /admin/price-lists/{priceListId}/items
Notes / open points — BO-31 is the three-price grid of the catalog (the three prices of every variant side by side, edited with pricing.update). admin.priceLists.* and admin.variants.tiers are V2 operations — not mounted in V1 (tiers dropped, price lists reserved for PRC-03, D-37).
PRC-03 — Per-reseller agreements and level-based prices (V2)
| Release | Priority | Source | Apps | Owner tasks |
|---|---|---|---|---|
| V2 | SHOULD | cahier §8 | API · BO | SRV-30 |
Statement — The system should, in V2, let the back-office give a reseller negotiated prices and give loyalty levels their own prices, on top of the three customer-type prices.
Rules
- A
PriceListof kindAGREEMENTbelongs to one reseller, has validity dates andPriceListItemrows (variant → unit price HT); a kindLEVELlist belongs to a loyalty level (LOY-03). - Resolution in V2 inserts them before the customer-type price: agreement item valid at t → level price → customer-type price, then promotions as in PRC-02. At most one active agreement per reseller at a time.
- Agreement and level prices are maintained in BO-31 through
admin.priceLists.*(pricing.*), historised asPriceChangerows of fieldPRICE_LIST_ITEM; carts of affected resellers are re-priced at their next read. - Whether quantity tiers come back with agreements is decided with D-37.
Acceptance criteria
- With an agreement price of 30 000 HT on the X14 cable for reseller
REV-000123, that reseller pays 30 000 HT while other Demi-gros resellers pay 35 000. - When the agreement expires, the reseller's next cart read shows the customer-type price with
PRICE_CHANGED.
Data — V2 PriceList, PriceListItem; PriceChange.
Interfaces
- Screens: none dedicated
- API: no dedicated operation
Notes / open points — V2 (SRV-30). Nothing of it is mounted in V1.
PRC-04 — VAT and HT/VAT/TTC totals
| Release | Priority | Source | Apps | Owner tasks |
|---|---|---|---|---|
| V1 | MUST | cahier §4.7, reference screen 5 | API | SRV-12 |
Statement — The system shall apply a VAT rate per variant and compute HT, VAT and TTC amounts identically in the cart, orders, invoices and credit notes.
Rules
vatRateis set per variant in basis points:1900(19 %, defaultpricing.defaultVatRate),900(9 %) or0; another value returns400 VALIDATION_FAILED.- Amounts follow PRC-02 R1: unit TTC rounded half-up once per unit; line TTC = unit TTC × quantity; line VAT = line TTC − line HT. Totals are sums of lines plus the delivery fee:
totalTtc = subtotalHt − discountTotal + deliveryFee + vatTotal, the fee being HT with VAT at 19 % added tovatTotal(DLV-06). - Documents (invoice, credit note) show per line the unit price HT, quantity, discount, VAT rate and totals, and at the bottom HT, the VAT per rate and TTC, plus the amount in words (« Arrêtée la présente facture à la somme de … ») (FIN-01).
- The apps show TTC prices large and HT in the breakdown (
pricing.priceDisplay=TTC); the cart summary (Sous-total, Remises, Frais de livraison, Total TTC, reference screen 5) uses the same figures as the order. - Order and invoice lines store the VAT rate and amounts at submission; a later rate change never alters an existing document.
- Stamp duty on cash payments (droit de timbre) is not computed until HOCO's accountant confirms the rule (D-21); when confirmed it is a separate line on cash-on-delivery invoices.
Acceptance criteria
- A variant saved with
vatRate1500 returns400 VALIDATION_FAILED. - For the Demi-gros example of PRC-02, the invoice shows HT 7 560,00 DA, TVA 19 % 1 436,40 DA, TTC 8 996,40 DA, exactly the cart and order figures.
- An order mixing 19 % and 9 % products shows two VAT lines on its invoice, one per rate, whose sum equals
vatTotal. - Changing a variant's VAT rate after an order leaves that order's lines and invoice unchanged.
Data — ProductVariant (vatRate), CartItem, OrderItem, Order (totals), Invoice, InvoiceLine, CreditNote.
Interfaces
- Screens: none dedicated
- API: no dedicated operation
PRC-05 — Re-validation at cart and checkout; each reseller sees only their own price
Statement — The system shall re-validate prices and availability on every cart read and write and again at checkout, never charge a total the reseller has not seen, and show each reseller only the price of their own customer type.
Rules
- Every
cart.getand cart mutation recomputes each line with PRC-02, the reseller's current visibility, quantity rules and availability, and returns issues:PRICE_CHANGEDandPROMOTION_ENDED(warnings),PRODUCT_UNAVAILABLE,STOCK_INSUFFICIENT,QUANTITY_NOT_ALLOWED,ORDER_MIN_AMOUNT_NOT_MET(blocking) (ORD-02). A line stores the unit price it was added at to detectPRICE_CHANGED. cart.checkoutcarriesexpectedTotalTtc, the total the reseller saw. The server re-validates everything inside the checkout transaction; when its total differs, nothing is created and it answers409 CART_CHANGEDwith the fresh cart; the reseller confirms again with a newIdempotency-Key(ORD-03).- Order lines snapshot the unit price, promotion and totals at submission; later price changes do not touch them.
- A reseller or delegate response contains the caller's price only — never the two other customer-type prices, the purchase price, margins or another reseller's price. Priced responses are
Cache-Control: private, no-storeand are never cached across callers; guest responses carry no price. - Availability is re-checked the same way and is only ever
IN_STOCK/OUT_OF_STOCK(STK-07).
Acceptance criteria
- A promotion that ends while the reseller looks at the cart makes the next
cart.getshowPROMOTION_ENDEDand the normal price. - Checkout with an
expectedTotalTtc1 centime off returns409 CART_CHANGEDwith the new cart and creates no order. - A Détail reseller's catalog, cart and order responses contain no Super-gros or Demi-gros price (checked on the JSON).
- Two resellers of different types reading the same product page get their own prices, never each other's (no shared cache entry).
Data — Cart, CartItem (unit price snapshot), Order, OrderItem, ProductVariant, Reseller (customerType).
Interfaces
Error codes
| Code | HTTP | Raised when |
|---|---|---|
PRICE_ORDER_INVALID | 422 | Selling prices not ordered super-gros ≤ demi-gros ≤ détail |
PERMISSION_DENIED | 403 | Writing a price without pricing.update, reading purchase prices or history without pricing.view |
CART_CHANGED | 409 | The re-validated total differs from the total the reseller saw |
| Issue codes (cart) | — | PRICE_CHANGED, PROMOTION_ENDED (warning); PRODUCT_UNAVAILABLE, STOCK_INSUFFICIENT, QUANTITY_NOT_ALLOWED, ORDER_MIN_AMOUNT_NOT_MET (blocking) |
Field codes in 400 VALIDATION_FAILED | 400 | Invalid VAT rate, non-positive price |
Catalog
Brands, the category tree with sub-categories, products and variants with filterable attributes, lifecycle, visibility, import/export, search with advanced filters, product detail, favorites, barcode scan and home merchandising.
Stock
Global or per-warehouse stock, physical, reserved and available quantities, reservation and deduction by order status with automatic restock on cancellation and return, the movement ledger, alerts, inventories, availability-only display and overrides.