Lifecycles and statuses
Every status machine of the platform — values, diagram, allowed transitions with actors, guards, side effects and the API operation that performs them.
This page is the behavioural core of the SRS. For every entity that has a status it gives the canonical values (from content/spec/enums.json, rendered live), a diagram of the allowed moves, and a transition table that developers implement as a state machine and testers turn into cases. The cahier names the statuses (cahier §4.2–§4.7, §15.1–§15.3); this page adds who may move them, when, with which checks and with which consequences. It applies the product decisions of 17 October 2026: no order is confirmed automatically, cancellations and return receptions restock automatically, returns and after-sales claims are V1, and every reseller has a customer type that selects their price.
How to read this page
Each transition table has five columns:
| Column | Meaning |
|---|---|
| From → To | — as origin means creation. Several origins separated by commas share the same rule. |
| Trigger · actor (app) | Who causes the move: a role or permission (with the app code BO, RA, RW, DA) or SYSTEM (a scheduled job or an event handler in the worker). |
| Guards | Preconditions checked by the server. A failed guard returns the error shown; nothing changes. |
| Side effects | What happens in the same database transaction (stock, numbers, history, outbox event) and right after commit (notifications, SSE, analytics). |
| API operation | The operation id that performs the move (API reference). job … marks a scheduled job (jobs and scheduler). |
Rules that hold for every lifecycle:
- The server owns statuses. Clients never write a status field; they call the operation of a transition. A move that is not in a table is refused with
409 <ENTITY>_TRANSITION_NOT_ALLOWED(for ordersORDER_TRANSITION_NOT_ALLOWED) anddetails: { from, to, allowed: [...] }. - One transaction. Status, side effects on owned data (stock movements, numbers, history rows) and the outbox event are committed together. Notifications, SSE events, analytics aggregates and CRM automation run after commit from the
OutboxEventtable, so a failed push never rolls back a confirmed order. - The actor is always recorded: user id and
ClientApp(DASHBOARD,RESELLER_APP,RESELLER_WEB,DELEGATE_APP,SYSTEM). Automatic moves recordSYSTEMplus the job or event key that caused them. - Reasons come from reference lists (
ReferenceItem.listKey:ORDER_REFUSAL_REASON,ORDER_CANCEL_REASON,TOUR_SUSPEND_REASON,VISIT_NOT_DONE_REASON,PAYMENT_REJECT_REASON). Items used by the system (for example « Délai de confirmation expiré ») are locked. The itemOTHERmakes the free-text comment mandatory. A missing reason returns422 REASON_REQUIRED. - Idempotent and offline-safe. Every transition operation accepts an
Idempotency-Key; a replay returns the original result. Delegate actions recorded offline carryoccurredAt; time-based guards (such as "the tour date is today") use theAfrica/Algiersdate ofoccurredAt, not ofreceivedAt. - Derived states are never stored. "Late" visits, "overdue" tasks and "not started" tours are computed at read time.
- Error codes follow the error registry. Codes introduced on this page (for example
TOUR_CLOSING_BLOCKED) belong to that registry.
Reseller — ResellerStatus
The commercial lifecycle of a point of sale (cahier §4.2 and §15.1, CRM-04). It is independent of the login account (AccountStatus), of the back-office check (ValidationStatus) and of the pricing tier (CustomerType): a prospect created in the field has a commercial status long before its owner has an account.
| Value | UI label (fr) | Meaning |
|---|---|---|
NEW_PROSPECT | Nouveau prospect | Just created; no qualified contact yet. |
TO_CONTACT | À contacter | Planned for a call or a visit. |
INTERESTED | Intéressé | Interest confirmed during a visit or call. |
APP_INSTALLED | Application installée | Invitation attributed an install (automatic). |
ACCOUNT_CREATED | Compte créé | Reseller account created and phone verified (automatic). |
FIRST_ORDER | Première commande | First order submitted (automatic). |
ACTIVE | Revendeur actif | First order delivered, or a validated order within the activity window (automatic). |
NOT_INTERESTED | Non intéressé | Declined; kept for later campaigns. |
TO_FOLLOW_UP | À relancer | A follow-up is due (from a visit report or inactivity). |
SUSPENDED | Suspendu | Commercially suspended (unpaid, fraud, closure); ordering blocked. |
The ten values form three families:
| Family | Values | Who moves them |
|---|---|---|
| Prospect qualification | NEW_PROSPECT, TO_CONTACT, INTERESTED, NOT_INTERESTED, TO_FOLLOW_UP | People — the delegate on resellers in scope, the back-office with crm.update — plus two system rules (planning sets TO_CONTACT, inactivity sets TO_FOLLOW_UP) |
| Activation funnel | APP_INSTALLED, ACCOUNT_CREATED, FIRST_ORDER, ACTIVE | SYSTEM only, from events. Never set by hand. |
| Commercial block | SUSPENDED | Back-office only, with a reason |
The diagram shows the usual path. Automatic events can also jump forward from any qualification status (a NOT_INTERESTED prospect who installs the app becomes APP_INSTALLED), and SUSPENDED can be reached from every status.
Automatic transitions
| From → To | Trigger · actor (app) | Guards | Side effects | API operation |
|---|---|---|---|---|
— → NEW_PROSPECT | Delegate creates a point of sale in the field (DA, offline, client-generated UUIDv7) · back-office user with crm.create (BO) | Main phone normalised and unique (409 PHONE_ALREADY_EXISTS); required fields of cahier §15.5 and the customer type present; commune belongs to the wilaya | source = DELEGATE_APP or BACK_OFFICE; createdByDelegateId; delegate and zone assigned (BR-47); ValidationStatus PENDING (field) or APPROVED (back-office); SSE account.pending for field creations; REV- number allocated | delegate.resellers.create · admin.resellers.create |
— → ACCOUNT_CREATED | Reseller signs up in RA or RW with a phone that is not in the CRM | Valid OTP verification token; customer type declared (AUTH-11) | source = SELF_SIGNUP; AccountStatus PENDING_VALIDATION; ValidationStatus PENDING; invitation attributed when a code is given; validators notified | auth.signup.reseller |
NEW_PROSPECT → TO_CONTACT | SYSTEM — the prospect is added as a stop of a tour, or an open CALL or VISIT task is created for it | Current status is NEW_PROSPECT | — | side effect of admin.tours.create · admin.tours.setStops · admin.tasks.create |
qualification statuses → APP_INSTALLED | SYSTEM — the invitation of this reseller is attributed to a first app open | Not SUSPENDED; reseller not yet further in the funnel | InvitationStep APP_INSTALLED; push to the inviting delegate | invitations.attribute |
qualification statuses, APP_INSTALLED → ACCOUNT_CREATED | SYSTEM — the owner claims the record: sign-up with the same phone, OTP verified (AUTH-05) | Not SUSPENDED; no DISABLED user on the record | User created; AccountStatus ACTIVE if ValidationStatus is APPROVED, else PENDING_VALIDATION; InvitationStep ACCOUNT_CREATED and PHONE_VERIFIED | auth.signup.reseller |
any status without a previous first order (except SUSPENDED) → FIRST_ORDER | SYSTEM — the reseller's first order is submitted: checkout, confirmation of an assisted order, or back-office order | The order is the first non-draft order that is not REFUSED or CANCELLED | Order.isFirstOrder = true; InvitationStep FIRST_ORDER; push to the attributed delegate (BR-27) | cart.checkout · orders.confirmAssisted · admin.orders.confirmAssisted · admin.orders.create |
FIRST_ORDER → ACTIVE | SYSTEM — an order of the reseller reaches DELIVERED | — | Conversion counted for the attributed delegate | admin.deliveries.proof |
FIRST_ORDER → the status before it | SYSTEM — the first order ends REFUSED or CANCELLED and the reseller has no other open order | — | isFirstOrder moves to the next submitted order, if any; funnel steps already reached stay reached | admin.orders.refuse · orders.cancel · admin.orders.cancel · job orders.expireAssisted |
ACTIVE, ACCOUNT_CREATED → TO_FOLLOW_UP | SYSTEM — daily inactivity scan at 02:00 Africa/Algiers: no order confirmed (VALIDATED) during crm.inactivityDays (default 60) since the last one (ACTIVE) or since the account was created (ACCOUNT_CREATED) | Not SUSPENDED | CrmTask of type CALL « Relance inactivité » for the assigned delegate (commercial admins when unassigned), due the next working day; push in the FIELD category | job crm.inactivityScan |
TO_FOLLOW_UP → ACTIVE | SYSTEM — an order of a reseller who already had a delivered order is confirmed (VALIDATED) | At least one past DELIVERED order | Open inactivity task closed (DONE, outcome « Commande reçue ») | admin.orders.validate |
Manual transitions
| From → To | Trigger · actor (app) | Guards | Side effects | API operation |
|---|---|---|---|---|
NEW_PROSPECT, TO_CONTACT, INTERESTED, NOT_INTERESTED, TO_FOLLOW_UP → TO_CONTACT, INTERESTED, NOT_INTERESTED, TO_FOLLOW_UP | Delegate on a reseller in scope (DA — point-of-sale sheet or visit report) · back-office crm.update (BO, also in bulk) | Target differs from current; NOT_INTERESTED needs a reason (3–300 characters); TO_FOLLOW_UP needs a next action and a follow-up date | ResellerStatusChange; Interaction of type STATUS_CHANGE on the timeline; TO_FOLLOW_UP creates the follow-up CrmTask; NOT_INTERESTED cancels the record's open tasks | delegate.resellers.update · delegate.visits.report · admin.resellers.setStatus · admin.resellers.bulk |
any → SUSPENDED | Back-office crm.update (BO-11) | Reason required (unpaid, fraud, closure…) | Checkout, assisted orders and return requests refused with 403 RESELLER_SUSPENDED; open orders are not cancelled but flagged in the orders list and the confirmation queue; assigned delegate notified; the login still works (orders, invoices, payments, tickets stay readable) | admin.resellers.setStatus |
SUSPENDED → previous status | Back-office crm.update | Reason required | Previous status read from the last ResellerStatusChange, then automation re-evaluated (an order delivered in the meantime makes it ACTIVE) | admin.resellers.setStatus |
| — → any status | Initial load of existing customers (settings.administer, ADM-06) | Only through the reseller import; customer type required per row | source = IMPORT; ValidationStatus APPROVED; existing buyers loaded ACTIVE | admin.imports.create |
Rules specific to resellers:
- Automatic events only move a reseller forward in the funnel order
NEW_PROSPECT→APP_INSTALLED→ACCOUNT_CREATED→FIRST_ORDER→ACTIVE. They liftTO_CONTACT,INTERESTED,NOT_INTERESTEDandTO_FOLLOW_UP; they never liftSUSPENDED— the event is recorded on the timeline and the status stays. - The four funnel statuses cannot be set by hand:
409 RESELLER_TRANSITION_NOT_ALLOWEDwithdetails.reason = "AUTOMATIC_STATUS". - Only two automatic moves go backwards: the first-order revert and the inactivity rule.
- A delegate may only move a reseller whose current status is a qualification status. A delegate change replayed from the offline queue after an automatic move (the prospect installed the app in the meantime) is answered
CONFLICTbysync.batchand shown in the sync centre (DA-27).
Customer type — CustomerType
The pricing tier of a reseller: each product variant has one selling price per type and the reseller's type selects it (BR-07, AUTH-11, PRC-02). It is not a lifecycle — a type does not progress — but who may change it, and when, is controlled like one.
| Value | UI label (fr) | Meaning |
|---|---|---|
SUPER_GROS | Super-gros | Large wholesaler — largest volumes; lowest price. |
DEMI_GROS | Demi-gros | Semi-wholesaler — intermediate volumes and price. |
DETAIL | Détail | Retail shop — smallest volumes; highest of the three prices. |
| From → To | Trigger · actor (app) | Guards | Side effects | API operation |
|---|---|---|---|---|
| — → declared type | Reseller at sign-up (RA, RW) · delegate at field creation (DA) · back-office creation or import | Required; one of the three values | Prices of that type shown right away when catalog.pricesVisibleBeforeValidation is true (default); ordering still waits for validation | auth.signup.reseller · delegate.resellers.create · admin.resellers.create |
| declared → confirmed or another type | Validator crm.validate while approving the account (BO-13) | Account PENDING validation | customerTypeConfirmedAt; when changed, the reseller is told the new tier in the approval message; AuditLog UPDATE with before and after | admin.resellers.validate |
| any → another type | Back-office crm.update (BO-11) | Reason required | AuditLog UPDATE (before, after, reason); Interaction NOTE on the timeline; cart re-priced at its next read (issue PRICE_CHANGED); submitted orders keep their snapshot prices; reseller notified (TRANSACTIONAL) | admin.resellers.update |
The reseller can never change their own type (profile.update rejects the field with 403 FIELD_NOT_EDITABLE); the delegate sets it only when creating a prospect.
User account — AccountStatus
The status of a login (User), for every user type: staff, delegate, reseller (cahier §6 and §10, AUTH-08, SEC-06).
| Value | UI label (fr) | Meaning |
|---|---|---|
PENDING_VALIDATION | En attente de validation | Reseller signed up; can browse the catalog (if allowed by settings) but cannot order until validated. |
ACTIVE | Actif | Full access for the user's role. |
SUSPENDED | Suspendu | Temporarily blocked; sign-in refused with an explicit message; sessions revoked. |
DISABLED | Désactivé | Permanently deactivated; history kept, sign-in impossible. |
| From → To | Trigger · actor (app) | Guards | Side effects | API operation |
|---|---|---|---|---|
— → PENDING_VALIDATION | Reseller signs up or claims a record whose ValidationStatus is not APPROVED (RA, RW) | OTP verified; password policy; customer type declared | Session opened; the app shows RA-06 | auth.signup.reseller |
— → ACTIVE | Admin creates a staff user (users.create) or a delegate (field.create) — temporary password by SMS or email, change forced at first sign-in · reseller claims an APPROVED record | Phone or email unique among users | Welcome message with the temporary password | admin.users.create · admin.delegates.crud · auth.signup.reseller |
PENDING_VALIDATION → ACTIVE | SYSTEM when the account is approved by a crm.validate holder | ValidationStatus becomes APPROVED | SMS and push « Votre compte est validé »; checkout unlocked | admin.resellers.validate |
PENDING_VALIDATION → DISABLED | Back-office users.update | Reason required | Sessions revoked | admin.users.setStatus |
ACTIVE → SUSPENDED | Back-office users.update | Reason required; not oneself (403 SELF_MODIFICATION_FORBIDDEN); not the last active SUPER_ADMIN (409 LAST_SUPER_ADMIN) | All sessions revoked, SSE streams closed, push tokens removed; next call answers 403 ACCOUNT_SUSPENDED with the explicit message of cahier §6; a delegate's tour IN_PROGRESS moves to SUSPENDED (reason « Compte suspendu ») | admin.users.setStatus |
SUSPENDED → ACTIVE | Back-office users.update | — | The user signs in again | admin.users.setStatus |
ACTIVE, SUSPENDED → DISABLED | Back-office users.update | Same as suspension; a delegate must have no tour PLANNED, TO_START, IN_PROGRESS or SUSPENDED (409 DELEGATE_HAS_OPEN_TOURS) | Terminal. History kept (cahier §10); the phone stays bound to its record and cannot be reused for another account | admin.users.setStatus |
What a visitor or a reseller account can do (AUTH-10, BR-61):
| Capability | Guest (no account) | PENDING_VALIDATION | ACTIVE | SUSPENDED, DISABLED |
|---|---|---|---|---|
| Browse catalog, product pages, promotions, WhatsApp support button | ✓ | ✓ | ✓ | sign-in refused (403 ACCOUNT_SUSPENDED); browsing as a guest after sign-out |
| See prices | — lock « Créez un compte pour voir les prix » (price: null) | Per catalog.pricesVisibleBeforeValidation (default ✓, prices of the declared type) | ✓ | — |
| Cart | — | ✓ when prices are visible | ✓ | — |
| Checkout, assisted-order confirmation, return request | — | — 403 ACCOUNT_PENDING_VALIDATION | ✓ | — |
| Store profile, documents, tickets | — | ✓ | ✓ | — |
Five failed sign-ins lock the phone for 15 minutes; that lock is not an account status. A reseller's commercial suspension (ResellerStatus.SUSPENDED) keeps the login open and only blocks ordering; an account suspension blocks the login itself.
Account validation — ValidationStatus
The back-office check of a reseller account or of a new point of sale created in the field (cahier §4.2 « La validation du compte est réalisée par un utilisateur autorisé », §4.5, §6.5 — CRM-09, FLD-14). It lives on the Reseller record; the queue is BO-13.
| Value | UI label (fr) | Meaning |
|---|---|---|
PENDING | En attente | |
APPROVED | Validé | |
REJECTED | Refusé |
| From → To | Trigger · actor (app) | Guards | Side effects | API operation |
|---|---|---|---|---|
— → PENDING | Field creation (DA), self sign-up (RA, RW) | — | Queue entry with duplicate hints (BR-05); SSE account.pending; validators reminded after crm.validationReminderHours | delegate.resellers.create · auth.signup.reseller |
— → APPROVED | Record created by a back-office user or imported | — | — | admin.resellers.create · admin.imports.create |
PENDING → APPROVED | Back-office crm.validate | Duplicate hints reviewed; required data of cahier §15.5 complete; a field-created point of sale has a GPS position; customer type confirmed or changed (BR-76) | AccountStatus PENDING_VALIDATION → ACTIVE when an account exists; SMS and push to the reseller (with the confirmed tier); push to the creating delegate; orders PENDING for RESELLER_NOT_VALIDATED are re-checked and move back to SUBMITTED (still waiting for confirmation) | admin.resellers.validate |
PENDING → REJECTED | Back-office crm.validate | Reason required (10–500 characters, shown to the reseller) | Reseller sees the reason on RA-06; delegate notified; the record keeps its commercial status | admin.resellers.validate |
REJECTED → PENDING | SYSTEM when the reseller edits the store details, or the delegate corrects the point of sale | At least one field changed | Back in the queue with the change history | profile.update · delegate.resellers.update |
An APPROVED record never goes back to PENDING: once approved, the identity fields a reseller cannot edit (store name, main phone, legal identifiers, customer type) change only through support and the back-office.
Product — ProductStatus
Catalog lifecycle (cahier §4.3, CAT-04). The status is on Product; what resellers see per variant is only an availability, IN_STOCK or OUT_OF_STOCK (BR-16).
| Value | UI label (fr) | Meaning |
|---|---|---|
DRAFT | Brouillon | Being prepared; invisible to resellers. |
ACTIVE | Actif | Visible and orderable (subject to visibility rules). |
HIDDEN | Masqué | Kept but invisible to resellers. |
OUT_OF_STOCK | En rupture | Set automatically when every variant's available stock is 0; back to ACTIVE on restock. |
ARCHIVED | Archivé | Retired; remains referenced by past orders. |
| From → To | Trigger · actor (app) | Guards | Side effects | API operation |
|---|---|---|---|---|
— → DRAFT | catalog.create (BO) or product import | Reference unique per brand | Default variant created | admin.products.create · admin.products.import |
DRAFT → ACTIVE | catalog.update (BO) | Minimum data of cahier §15.4: brand, reference, name, category, description, at least one image, VAT rate, at least one variant with SKU and its three prices (super-gros, demi-gros, détail); attributes marked required for the category filled (CAT-13) — 422 PRODUCT_INCOMPLETE lists what is missing | publishedAt set (drives the « Nouveautés » filter and sort); if every variant has 0 available the product goes straight to OUT_OF_STOCK | admin.products.setStatus |
ACTIVE → HIDDEN, HIDDEN → ACTIVE | catalog.update | — | Hidden products disappear from catalog, search, facets, favorites and promotions at the next read; cart lines become blocking issues PRODUCT_UNAVAILABLE | admin.products.setStatus |
ACTIVE → OUT_OF_STOCK | SYSTEM — the sum of available stock of all variants in sellable warehouses reaches 0 | Status was ACTIVE | Product stays visible with « En rupture » and cannot be ordered; stock alert raised | stock movement handler |
OUT_OF_STOCK → ACTIVE | SYSTEM — available stock of any variant becomes positive (receipt, cancellation, return reception) | The product was put out of stock by the system | Back in the « En stock » filter | stock movement handler |
OUT_OF_STOCK → HIDDEN | catalog.update | — | Showing it again recomputes ACTIVE or OUT_OF_STOCK from stock | admin.products.setStatus |
any except ARCHIVED → ARCHIVED | catalog.update | — | Removed from every reseller surface; still referenced by past orders, invoices, returns and price history | admin.products.setStatus |
ARCHIVED → HIDDEN | catalog.administer | — | Must be shown explicitly afterwards | admin.products.setStatus |
A DRAFT that was never published can be deleted (catalog.delete); any other product can only be archived.
Order — OrderStatus
The eleven statuses of cahier §4.4 and §15.2 (ORD-04). The state machine lives in the orders module; every other module reacts to its events. No order is ever confirmed automatically (ORD-14): whatever its channel, an order waits in SUBMITTED (or PENDING when on hold) until a back-office user with orders.validate confirms it from the queue BO-41.
| Value | UI label (fr) | Meaning |
|---|---|---|
DRAFT | Brouillon | Assisted order being prepared by a delegate, or a back-office draft. |
SUBMITTED | Envoyée | Placed by the reseller (or an assisted order confirmed by the reseller); waits for the back-office. Orders are never confirmed automatically. |
PENDING | En attente | On hold: needs information, a stock/credit decision, or the reseller's confirmation of an assisted order. |
VALIDATED | Validée | Confirmed by a back-office user (orders.validate); stock reserved (default setting). |
REFUSED | Refusée | Refused with a reason; terminal. |
PREPARING | En préparation | Picking in progress. |
READY | Prête | Packed, waiting for the carrier. |
SHIPPED | Expédiée | Handed to the carrier; stock deducted (default setting). |
DELIVERED | Livrée | Proof of delivery recorded. |
CANCELLED | Annulée | Cancelled by the reseller (before confirmation) or the back-office (with reason); reserved quantities return to stock automatically. |
RETURNED | Retournée | Returned after delivery (accepted return or refused at the door); quantities re-enter stock automatically on reception. |
Defaults used on this page (all tunable — see the settings catalogue): stock.reserveOn = VALIDATED, stock.deductOn = SHIPPED, billing.invoiceOn = SHIPPED, orders.cancelAllowedStatuses = [SUBMITTED, PENDING], orders.assistedConfirmationHours = 72, returns.windowDays = 7, credit.overLimitPolicy = BLOCK.
Where an order starts depends on the channel (OrderChannel):
| Channel | Created by | First status | Number allocated |
|---|---|---|---|
RESELLER_APP, RESELLER_WEB | Reseller checkout (the cart is the draft) | SUBMITTED (or PENDING at once when a control needs a decision) | At creation |
DELEGATE_ASSISTED | Delegate's first-order assistance (DA-17) | DRAFT, then PENDING awaiting the reseller's confirmation | When sent for confirmation |
BACK_OFFICE | orders.create on behalf of a reseller (BO-40, phone orders), or a free replacement created by an after-sales resolution | SUBMITTED, or DRAFT when saved as a draft | When submitted |
In every case the next step is the same: the order appears in admin.orders.toConfirm (oldest first, with its control results) and only admin.orders.validate moves it on.
Transition table
| From → To | Trigger · actor (app) | Guards | Side effects | API operation |
|---|---|---|---|---|
— → SUBMITTED | Reseller (RA, RW) | AccountStatus ACTIVE (403 ACCOUNT_PENDING_VALIDATION); ResellerStatus not SUSPENDED (403 RESELLER_SUSPENDED); Idempotency-Key present; every line visible and orderable (422 PRODUCT_UNAVAILABLE); quantity rules (422 QUANTITY_NOT_ALLOWED); every line available (422 STOCK_INSUFFICIENT); orders.minAmount (422 ORDER_MIN_AMOUNT_NOT_MET); payment method enabled; credit under BLOCK (422 CREDIT_LIMIT_EXCEEDED); address belongs to the reseller; totals equal to the totals the reseller saw (409 CART_CHANGED with the re-validated cart) | CMD-YYYY-NNNNNN allocated; lines, customer-type prices, discounts, promotions and address snapshotted; promotion caps consumed; cart emptied; first-order flag and ResellerStatus FIRST_ORDER when first; reservation only if stock.reserveOn = SUBMITTED; outbox order.created → SSE to the back-office and entry in the confirmation queue, in-app alert to orders.validate holders, push and live event « Commande envoyée » to the reseller | cart.checkout |
— → SUBMITTED | Back-office orders.create (BO-40) | Same controls as checkout except the reseller's AccountStatus (phone orders for prospects are allowed); no override at creation | Channel BACK_OFFICE, placedByUserId; reseller notified « Commande créée pour vous »; enters the confirmation queue like any order | admin.orders.create |
— → SUBMITTED | SYSTEM — after-sales resolution REPLACEMENT | The claim's lines and quantities | Free order (lines at price 0, replacementForTicketId); goes through confirmation, preparation and delivery like any order | admin.tickets.resolve |
— → DRAFT | Delegate on a reseller in scope (DA) · back-office draft (BO) | ResellerStatus not SUSPENDED | Priced with the reseller's customer-type prices; no number, invisible to the reseller; no stock effect | delegate.assisted.create · admin.orders.create |
DRAFT → deleted | The delegate who created it (DA) | Status DRAFT | Hard delete, audit entry kept | delegate.assisted.discard |
DRAFT → PENDING | Delegate sends the assisted order for confirmation (DA) | Same controls as checkout, except that a reseller pending validation is allowed; at most one assisted order awaiting confirmation per reseller (409 ASSISTED_ORDER_ALREADY_PENDING) | Number allocated; sourceDelegateId; AssistedConfirmation AWAITING_RESELLER; assistedExpiresAt = now + 72 h; push, live event and SMS to the reseller with a deep link to RA-23; listed in BO-41 | delegate.assisted.submit |
DRAFT → SUBMITTED | Back-office submits its own draft (BO) | Same controls as back-office creation | As back-office creation | admin.orders.transition |
PENDING → SUBMITTED | Reseller confirms the assisted order (RA, RW) · administration confirms it, for example after a phone call (BO, orders.validate) | AssistedConfirmation AWAITING_RESELLER and now before assistedExpiresAt (409 ASSISTED_ORDER_EXPIRED); order re-priced and re-controlled; reseller confirmation carries the totals shown (409 CART_CHANGED); admin confirmation needs a note saying how the reseller confirmed | AssistedConfirmation CONFIRMED_BY_RESELLER or CONFIRMED_BY_ADMIN; then the side effects of a checkout (caps, first order, events); delegate notified. The order still waits for admin.orders.validate | orders.confirmAssisted · admin.orders.confirmAssisted |
SUBMITTED → PENDING | SYSTEM right after submission or confirmation · back-office orders.validate puts the order on hold | System: a control needs a human decision — CREDIT_OVER_LIMIT (policy REQUIRE_APPROVAL) or RESELLER_NOT_VALIDATED (point of sale still PENDING validation). Manual hold: reason required (missing information, call the reseller…) | pendingReasons stored; reseller sees « En attente » live and by push; finance (credit) or validators (account) notified in-app | order controls handler · admin.orders.transition |
SUBMITTED → VALIDATED | Back-office orders.validate only (BO-41, BO-39) | Reseller ValidationStatus APPROVED; ResellerStatus not SUSPENDED; every line available, or a stock override by a stock.validate holder with a reason (422 STOCK_INSUFFICIENT otherwise); credit within limit for CREDIT orders, or a credit override by a finance.validate holder; a price changed since submission is shown as information only (the order keeps its snapshot) | Reservation (RESERVATION movements, reserved += qty); Delivery created TO_PREPARE; validatedAt, validatedBy; push and live event « Commande validée »; delegate notified for assisted orders; ResellerStatus TO_FOLLOW_UP → ACTIVE when applicable; objectives ORDERS and REVENUE counted; AuditLog VALIDATE plus one OVERRIDE entry per override; invoice when billing.invoiceOn = VALIDATED | admin.orders.validate |
PENDING → VALIDATED | Back-office orders.validate; for CREDIT_OVER_LIMIT only, a finance.validate holder | The pending reason is resolved or overridden; RESELLER_NOT_VALIDATED cannot be overridden — approve the account first; an assisted order still AWAITING_RESELLER must be confirmed first (409 ORDER_TRANSITION_NOT_ALLOWED) | As above | admin.orders.validate |
SUBMITTED, PENDING → REFUSED | Back-office orders.validate | Reason from ORDER_REFUSAL_REASON | Reservation released if any; promotion caps released; first-order revert; credit exposure released; reseller notified with the reason (push, live event, in-app); delegate notified (assisted or assigned) | admin.orders.refuse |
SUBMITTED, PENDING → CANCELLED | Reseller on their own order (RA, RW) | Status in orders.cancelAllowedStatuses (before confirmation only); not an assisted order awaiting confirmation (that one is rejected, not cancelled); reason from the reseller subset of ORDER_CANCEL_REASON | As refusal; back-office alerted in-app; the order leaves the confirmation queue | orders.cancel |
PENDING → CANCELLED | Reseller rejects the assisted order (RA, RW) · SYSTEM when 72 h pass without an answer | Reject: reason required. Expiry: assistedExpiresAt passed | AssistedConfirmation REJECTED_BY_RESELLER or EXPIRED; system reason « Refusée par le revendeur » or « Délai de confirmation expiré »; delegate notified | orders.rejectAssisted · job orders.expireAssisted |
SUBMITTED, PENDING, VALIDATED, PREPARING, READY → CANCELLED | Back-office orders.update | Reason from ORDER_CANCEL_REASON; never after SHIPPED (the path is then RETURNED); an issued invoice with confirmed payments blocks (409 INVOICE_HAS_PAYMENTS) | Stock put back automatically: RESERVATION_RELEASE for reserved quantities and RETURN for quantities already deducted (possible only when stock.deductOn = READY); delivery record closed (cancelledAt); an issued invoice is cancelled with a full credit note; caps released; first-order revert; reseller (live and push) and delegate notified | admin.orders.cancel |
VALIDATED → PREPARING | Delivery manager or any orders.update / delivery.update holder (BO-42) | — | DeliveryStatus PREPARING; picking list available; reseller sees « En préparation » live | admin.orders.transition · admin.deliveries.pickingList |
PREPARING → READY | Same | Picking confirmed for every line (shortages handled by a line modification first) | BL-YYYY-NNNNNN allocated and delivery note PDF generated; package count recorded; DeliveryStatus READY | admin.orders.transition |
READY → SHIPPED | Same | Delivery mode set: OWN_FLEET needs a driver name and phone, CARRIER a carrier and a tracking number; mode PICKUP cannot ship (409 ORDER_TRANSITION_NOT_ALLOWED) | Stock deducted (SHIPMENT: physical and reserved decrease); invoice FAC-YYYY-NNNNNN issued with its due date; DeliveryStatus SHIPPED; push and live event « Commande expédiée » with the tracking reference | admin.orders.transition · admin.deliveries.update |
READY → DELIVERED | Same, for PICKUP orders handed over at the depot | Recipient name, photo or signature | Deduction and invoice happen in the same transition (crossing rule below); then as delivery | admin.deliveries.proof |
SHIPPED → DELIVERED | delivery.update holder (BO) | Proof: recipient name or photo, at least one; delivery time | deliveredAt (starts the return and after-sales windows); cash collected on a CASH_ON_DELIVERY order creates a Payment PENDING_VERIFICATION; ResellerStatus FIRST_ORDER → ACTIVE; push and live event « Commande livrée »; V2: loyalty points when loyalty.earnOn = DELIVERED | admin.deliveries.proof |
SHIPPED → RETURNED | delivery.update or orders.update holder | Reason (refused at the door, unreachable after the last failed attempt…) | DeliveryStatus RETURNED; a ReturnRequest for every line is created by the system directly in IN_TRANSIT; its reception re-enters the goods and issues the credit note automatically (ReturnStatus); reseller notified | admin.orders.transition |
DELIVERED → RETURNED | SYSTEM when received returns cover every delivered unit | Sum of accepted returned quantities = delivered quantities | Partial returns leave the order DELIVERED with a « Retour partiel » badge | admin.returns.receive |
Every transition above emits order.status_changed to the outbox; the worker forwards it to the back-office stream, to the reseller's live stream events.stream while their app or page is open, and as a push plus an inbox notification (ORD-15, BR-30).
Stock effects and documents
Stock moves when an order reaches or passes the configured status — skipping a status (a pickup order goes from READY to DELIVERED) cannot skip its stock movement or its document. Everything that leaves the warehouse comes back automatically when the order is cancelled or when returned goods are received (STK-03, BR-13).
| Event | reserveOn = VALIDATED (default) | reserveOn = SUBMITTED |
|---|---|---|
| Order submitted | — | RESERVATION +qty |
Order confirmed (VALIDATED) | RESERVATION +qty | — |
| Refused, cancelled or expired | RESERVATION_RELEASE −qty for what is reserved; RETURN +qty for what was already deducted | same |
| Line modified while reserved | RESERVATION or RESERVATION_RELEASE for the difference | same |
deductOn status reached (default SHIPPED) | SHIPMENT: physical −qty, reserved −qty | same |
Return received (admin.returns.receive) | RETURN +accepted qty, posted automatically | same |
Example with the defaults — variant HOCO X89 câble USB-C, physical 100, reserved 20, available 80: an order of 30 is confirmed → reserved 50, available 50; it ships → physical 70, reserved 20, available 50. Had it been cancelled while READY, reserved would return to 20 and available to 80. If 10 cables come back later and 8 are accepted at reception → physical 78, available 58.
Documents follow the same crossing rule: the delivery note (BL-) is generated at READY; the invoice (FAC-) at billing.invoiceOn (default SHIPPED). An issued invoice is never edited: regenerating the PDF reproduces the same amounts; correcting it means a credit note (BR-39).
Cancellation and refusal windows
| Actor | DRAFT | SUBMITTED | PENDING | VALIDATED | PREPARING | READY | SHIPPED and after |
|---|---|---|---|---|---|---|---|
| Reseller (RA, RW) | invisible | cancel | cancel (reject if assisted) | — | — | — | return request after DELIVERED |
| Delegate (DA) | discard own draft | — | — | — | — | — | — |
Back-office orders.validate | — | confirm · refuse · hold | confirm · refuse | — | — | — | — |
Back-office orders.update | delete | cancel | cancel | cancel | cancel | cancel | RETURNED (door refusal) |
SYSTEM | — | — | expire (assisted) | — | — | — | RETURNED when a full return is received |
Refusal is a decision on an order that was never confirmed; cancellation withdraws an order (confirmed or not). Both are terminal. orders.cancelAllowedStatuses only accepts SUBMITTED and PENDING: once the back-office has confirmed an order, the reseller goes through support.
Changes that are not transitions
| Action | Allowed in | Who | Rules and effects | API operation |
|---|---|---|---|---|
| Controlled modification of lines | SUBMITTED, PENDING, VALIDATED: any change · PREPARING: decrease or remove only (picking shortage) | orders.update | Reason required; controls re-run (stock, credit, minimum amount); reservation and promotion caps adjusted by the difference; unchanged lines keep their snapshot price, new lines take today's price for the reseller's type; OrderChange row with the diff; reseller notified live and by push with the new totals (order.modified) | admin.orders.modify |
| Edit an assisted draft | DRAFT | Creating delegate | Re-priced on every change; returns totals and issues | delegate.assisted.update |
| Regenerate documents | READY and after | orders.update | Same content, new PDF version; never changes amounts | admin.orders.documents |
| Request a return | DELIVERED, within returns.windowDays | Reseller | Creates a ReturnRequest (ReturnStatus) | returns.create |
| Open an after-sales claim | DELIVERED, within the after-sales window | Reseller | Creates a ticket of type AFTER_SALES (TicketStatus) | tickets.create |
| Reorder | DELIVERED, CANCELLED, REFUSED, RETURNED | Reseller | Copies available lines into the cart at today's prices; unavailable lines reported | orders.reorder |
What the reseller sees
The reseller apps show a five-step tracker and five counters (reference screen 6, RA-21, RA-22, RW-08) that update live while the app or page is open; the status pill always shows the exact French label of the status.
| Tracker step and counter | Order statuses |
|---|---|
| En attente | SUBMITTED, PENDING |
| Validée | VALIDATED |
| En préparation | PREPARING, READY |
| Expédiée | SHIPPED |
| Livrée | DELIVERED |
| Off the tracker, with the reason | REFUSED, CANCELLED, RETURNED |
DRAFT orders are never visible to the reseller. Assisted orders awaiting confirmation are pinned at the top of the orders screen with « À confirmer ». orders.get returns allowedActions (CANCEL, CONFIRM, REJECT, REORDER, REQUEST_RETURN, OPEN_CLAIM, DOWNLOAD_INVOICE, DOWNLOAD_DELIVERY_NOTE) so the apps never re-implement these rules.
Assisted order confirmation — AssistedConfirmation
The confirmation state of an order prepared by a delegate (cahier §4.4 « Une commande assistée doit être confirmée par le revendeur ou validée par l'administration », §6.7 — ORD-08). It exists only on orders with channel DELEGATE_ASSISTED. Confirming the assisted order (this lifecycle) and confirming the order for preparation (admin.orders.validate) are two distinct, explicit acts.
| Value | UI label (fr) | Meaning |
|---|---|---|
AWAITING_RESELLER | En attente du revendeur | |
CONFIRMED_BY_RESELLER | Confirmée par le revendeur | |
CONFIRMED_BY_ADMIN | Validée par l'administration | |
REJECTED_BY_RESELLER | Refusée par le revendeur | |
EXPIRED | Expirée | Not confirmed within the configured delay (default 72 h). |
| From → To | Trigger · actor (app) | Guards | Side effects | API operation |
|---|---|---|---|---|
— → AWAITING_RESELLER | Delegate (DA) | See DRAFT → PENDING | Order PENDING; assistedExpiresAt = now + orders.assistedConfirmationHours; reminder push to the reseller 24 h before expiry | delegate.assisted.submit |
AWAITING_RESELLER → CONFIRMED_BY_RESELLER | Reseller (RA RA-23, RW RW-09) | Before expiry; AccountStatus ACTIVE; totals shown equal the re-priced totals | Order SUBMITTED, enters the confirmation queue | orders.confirmAssisted |
AWAITING_RESELLER → CONFIRMED_BY_ADMIN | Back-office orders.validate (BO-41) — typically the reseller has no app yet and confirmed by phone | Before expiry; confirmation note required | Order SUBMITTED; the note is kept in the order history; the same user may then confirm the order with a second explicit action | admin.orders.confirmAssisted |
AWAITING_RESELLER → REJECTED_BY_RESELLER | Reseller (RA, RW) | Reason required | Order CANCELLED; delegate notified with the reason | orders.rejectAssisted |
AWAITING_RESELLER → EXPIRED | SYSTEM — job every 15 minutes | assistedExpiresAt passed | Order CANCELLED; delegate and reseller notified | job orders.expireAssisted |
A confirmation that arrives after assistedExpiresAt is refused even if the job has not run yet. After a rejection or an expiry the delegate starts a new assisted order (the old one stays in history).
Return — ReturnStatus
Returns are V1 (ORD-13, FIN-08). A return covers some or all lines of a delivered order; it starts from a reseller request (RA-40, RW-14), from an after-sales resolution RETURN, or from a door refusal. The back-office queue is BO-69.
| Value | UI label (fr) | Meaning |
|---|---|---|
REQUESTED | Demandé | |
APPROVED | Accepté | |
IN_TRANSIT | En retour | Approved; goods on their way back. |
RECEIVED | Reçu | |
REFUNDED | Remboursé / avoir émis | |
REJECTED | Refusé |
| From → To | Trigger · actor (app) | Guards | Side effects | API operation |
|---|---|---|---|---|
— → REQUESTED | Reseller (RA, RW) | Order DELIVERED and within returns.windowDays of deliveredAt (default 7, 422 RETURN_WINDOW_EXPIRED); per line, quantity ≤ delivered − already returned or claimed (422 QUANTITY_NOT_ALLOWED); reason and at least one photo; ResellerStatus not SUSPENDED | RET-YYYY-NNNNNN; SSE to the back-office; orders.validate holders notified | returns.create |
— → APPROVED | SYSTEM — after-sales resolution RETURN | The claim's lines and quantities | Linked to the ticket; return instructions sent to the reseller | admin.tickets.resolve |
— → IN_TRANSIT | SYSTEM — order SHIPPED → RETURNED (refused at the door) | — | Covers every shipped line | admin.orders.transition |
REQUESTED → APPROVED | Back-office orders.validate | — | Instructions to the reseller (bring to the depot, or collection date); push and live event | admin.returns.decide |
REQUESTED → REJECTED | Back-office orders.validate | Reason required | Reseller notified with the reason | admin.returns.decide |
APPROVED → IN_TRANSIT | Back-office (delivery manager) when a driver or carrier collects the goods | Collection details | Reseller notified | admin.returns.decide (collection recorded) |
APPROVED, IN_TRANSIT → RECEIVED | Stock stock.update at the depot | Received and accepted quantities per line (accepted ≤ received ≤ requested); a reason for every unit not accepted | RETURN stock movements posted automatically for accepted quantities; refused units stay with the reseller's file (not re-entered); order becomes RETURNED when every delivered unit is back | admin.returns.receive |
RECEIVED → REFUNDED | SYSTEM, in the same operation, when accepted lines are invoiced | Accepted quantity > 0 | CreditNote for the accepted lines at the order's snapshot prices, applied to the invoice balance (BR-39); reseller notified; V2: loyalty REVERSAL | admin.returns.receive |
RECEIVED → REJECTED | SYSTEM, in the same operation | Nothing accepted | Reseller notified with the inspection reasons | admin.returns.receive |
A return that is accepted but concerns lines never invoiced (a pickup order paid in cash before invoicing, for example) stays RECEIVED: there is nothing to credit. Money already paid is not given back in cash by the platform: the credit note reduces what the reseller owes, and a cash refund, when HOCO grants one, is recorded by finance against the credit note.
Invoice — InvoiceStatus
Invoice lifecycle (cahier §4.7, FIN-01). Invoices are generated from orders; the status is derived from the balance and the due date, then stored for querying. The balance is the total minus confirmed payment allocations minus credit notes.
| Value | UI label (fr) | Meaning |
|---|---|---|
UNPAID | Impayée | |
PARTIALLY_PAID | Partiellement payée | |
PAID | Payée | |
OVERDUE | En retard | Set by a daily job when dueDate passes with a balance. |
CANCELLED | Annulée |
Status rule, evaluated on every allocation or credit note and by the daily job: CANCELLED if credit notes cover the whole total and nothing was paid; else PAID if the balance is 0; else OVERDUE if dueDate is before today (Africa/Algiers); else PARTIALLY_PAID if something is paid or credited; else UNPAID. An overdue invoice that receives a partial payment stays OVERDUE and shows the paid amount.
| From → To | Trigger · actor (app) | Guards | Side effects | API operation |
|---|---|---|---|---|
— → UNPAID | SYSTEM when the order reaches billing.invoiceOn | Order not already invoiced | FAC-YYYY-NNNNNN; legal mentions (RC, NIF, NIS, AI, VAT per rate, stamp duty when applicable); dueDate per BR-36; PDF stored as GENERATED_DOCUMENT; credit exposure moves from the order to the invoice | side effect of admin.orders.transition |
UNPAID, PARTIALLY_PAID, OVERDUE → PARTIALLY_PAID, PAID | SYSTEM when a confirmed payment is allocated or a credit note is applied | Allocation ≤ balance | paidAt when fully paid; available credit recomputed; V2: loyalty points when loyalty.earnOn = PAID | admin.payments.verify · admin.payments.record · admin.payments.allocate · admin.returns.receive · admin.tickets.resolve |
UNPAID, PARTIALLY_PAID → OVERDUE | SYSTEM — daily job at 01:00 Africa/Algiers | Balance > 0 and dueDate passed | Finance dashboard « Impayés »; V2: reminders and blocking (FIN-07) | job invoices.markOverdue |
PAID → PARTIALLY_PAID, UNPAID, OVERDUE | SYSTEM when an allocation is removed | finance.update on the allocation | Audit entry with the reason | admin.payments.allocate |
UNPAID, OVERDUE → CANCELLED | Finance finance.validate (invoice issued by mistake) · SYSTEM when the order is cancelled after invoicing | No confirmed allocation (409 INVOICE_HAS_PAYMENTS — deallocate first); reason | A full credit note is issued — the invoice number is kept and never reused | admin.invoices.cancel · admin.orders.cancel |
Payment — PaymentStatus
Recorded payments and reseller declarations (cahier §4.7, FIN-03, FIN-04). Online card payment is out of scope (scope): transfers, CCP and BaridiMob are declared with a proof and verified by finance.
| Value | UI label (fr) | Meaning |
|---|---|---|
PENDING_VERIFICATION | À vérifier | |
CONFIRMED | Confirmé | |
REJECTED | Rejeté |
| From → To | Trigger · actor (app) | Guards | Side effects | API operation |
|---|---|---|---|---|
— → PENDING_VERIFICATION | Reseller declares a BANK_TRANSFER, CCP or BARIDIMOB payment (RA RA-31, RW) | Amount > 0; payment date not in the future; proof file required (PAYMENT_PROOF); reference required | PAY-YYYY-NNNNNN; finance notified in-app; available credit not changed yet | payments.declare |
— → PENDING_VERIFICATION | Cash collected on delivery of a CASH_ON_DELIVERY order (BO) | Amount collected entered with the proof of delivery | Payment linked to the order and its invoice | admin.deliveries.proof |
— → CONFIRMED | Finance finance.create records a payment it has seen (BO-45) | Allocations ≤ amount | Allocations applied; invoices re-evaluated; reseller notified | admin.payments.record |
PENDING_VERIFICATION → CONFIRMED | Finance finance.validate | Allocations ≤ amount; by default the oldest due invoices first | As above; unallocated remainder kept as an advance that lowers credit exposure | admin.payments.verify |
PENDING_VERIFICATION → REJECTED | Finance finance.validate | Reason from PAYMENT_REJECT_REASON | Reseller notified with the reason; proof kept | admin.payments.verify |
A confirmed payment is never deleted or edited; a mistake is corrected by changing its allocations (audited).
Delivery — DeliveryStatus
The fulfilment record created when an order is confirmed (DLV-01 to DLV-03). It mirrors the order: the order state machine is the single source of truth and updates the delivery in the same transaction; the only status of its own is FAILED.
| Value | UI label (fr) | Meaning |
|---|---|---|
TO_PREPARE | À préparer | |
PREPARING | En préparation | |
READY | Prête | |
SHIPPED | Expédiée | |
DELIVERED | Livrée | |
FAILED | Échec de livraison | |
RETURNED | Retournée |
| Order status | Delivery status | What the delivery record holds | API operation |
|---|---|---|---|
VALIDATED | TO_PREPARE | Mode (OWN_FLEET, CARRIER, PICKUP) proposed from the address and the delivery settings | created by admin.orders.validate |
PREPARING | PREPARING | Picker, picking list printed | admin.orders.transition |
READY | READY | Packages, delivery note BL- | admin.orders.transition |
SHIPPED | SHIPPED | Carrier or driver, tracking number, shippedAt | admin.orders.transition · admin.deliveries.update |
SHIPPED (unchanged) | FAILED | Attempt number, failure reason, next attempt date | admin.deliveries.update |
DELIVERED | DELIVERED | Proof (photo DELIVERY_PROOF, recipient name), deliveredAt, cash collected | admin.deliveries.proof |
RETURNED (door refusal) | RETURNED | Reason, linked return request | admin.orders.transition |
CANCELLED (from VALIDATED, PREPARING, READY) | last value kept, cancelledAt set | Leaves the delivery board | admin.orders.cancel |
The delivery board BO-42 shows one column per delivery status; moving a card calls the order transition, never a delivery-only update. Returns after delivery do not change the delivery record; they live in their own lifecycle.
Promotion — PromotionStatus
Promotions, daily offers and their banners (cahier §4.6, §5.5 — PRM-01, PRM-04).
| Value | UI label (fr) | Meaning |
|---|---|---|
DRAFT | Brouillon | |
SCHEDULED | Programmée | |
ACTIVE | Active | |
PAUSED | En pause | |
ENDED | Terminée | Past its end date or stock cap reached. |
ARCHIVED | Archivée |
| From → To | Trigger · actor (app) | Guards | Side effects | API operation |
|---|---|---|---|---|
— → DRAFT | promotions.create (BO-50) | — | — | admin.promotions.crud |
DRAFT → SCHEDULED | promotions.validate | Value valid for the type (PERCENT_OFF 1–90 %, AMOUNT_OFF positive and below the lowest of the three prices in scope, FIXED_PRICE positive, FREE_DELIVERY no value); startsAt before endsAt, endsAt in the future; scope not empty; targeting valid | Visible in the back-office calendar | admin.promotions.setStatus |
SCHEDULED → DRAFT | promotions.validate | Not started | Editable again | admin.promotions.setStatus |
DRAFT → ACTIVE, SCHEDULED → ACTIVE | promotions.validate (activate now) · SYSTEM at startsAt — job every minute | Same as scheduling | Applied by the cart engine from the next cart read; linked banners shown; « Promotion » filter updated; optional PROMOTIONAL push to the target (opt-outs and quiet hours respected) | admin.promotions.setStatus · job promotions.applySchedule |
ACTIVE → PAUSED, PAUSED → ACTIVE | promotions.validate | Resume only before endsAt | Carts re-priced at the next read; banners hidden while paused | admin.promotions.setStatus |
ACTIVE, PAUSED → ENDED | SYSTEM at endsAt (job) · SYSTEM when the quantity cap is fully consumed (checkout) | — | Banners hidden; carts show the issue « Offre terminée » and the normal price | job promotions.applySchedule · cart.checkout |
DRAFT, ENDED → ARCHIVED | promotions.validate | — | Kept for reports; never applied again | admin.promotions.setStatus |
While ACTIVE, only the end date (later), the cap (higher) and the banner can change; any other change requires duplicating the promotion. The engine also checks the dates itself at every pricing, so a job running late never applies an expired promotion. Submitted orders keep the promotion they were priced with. Guests see that a product is on promotion (badge and percentage) but never a price.
Tour — TourStatus
A delegate's day in the field (cahier §4.5, §6.1, §6.9, §15.3 — FLD-04, FLD-12). One tour = one delegate × one date; weekly or monthly planning creates one tour per day.
| Value | UI label (fr) | Meaning |
|---|---|---|
PLANNED | Planifiée | Scheduled for a future day. |
TO_START | À démarrer | Day of the tour has come (set by the 00:05 job); the start button is enabled. |
IN_PROGRESS | En cours | Started by the assigned delegate (time + position recorded). |
SUSPENDED | Suspendue | Paused with a reason (break, incident); location sampling stops. |
COMPLETED | Terminée | Closed with the final report; definitive unless reopened. |
CANCELLED | Annulée | Cancelled by the back-office with a reason. |
| From → To | Trigger · actor (app) | Guards | Side effects | API operation |
|---|---|---|---|---|
— → PLANNED | Back-office field.create (planner BO-21, editor BO-22); a recurring series creates one tour per day | Delegate ACTIVE; date today or later; one tour per delegate per date (409 TOUR_DATE_TAKEN); stops are resellers in the delegate's scope or explicitly added (which grants the delegate access to them, see the permissions matrix) | TRN-YYYY-NNNNNN; visits created PLANNED with VIS- numbers; push « Nouvelle tournée » (FIELD); prospects among the stops move to TO_CONTACT | admin.tours.create |
— → TO_START | Same, when the date is today | Same | Same; available at once in delegate.tours.today | admin.tours.create |
PLANNED → TO_START | SYSTEM — daily job at 00:05 Africa/Algiers for the tours of the day | Status PLANNED | Start button enabled on DA-05 (cahier §6.1 « disponible uniquement pour une tournée planifiée »); the app pre-loads the offline snapshot | job tours.activateToday |
TO_START (or PLANNED dated today, when the 00:05 job is late) → IN_PROGRESS | Only the assigned delegate (DA) | Tour belongs to the caller (others get 404 NOT_FOUND); the Africa/Algiers date of occurredAt is the tour date (409 TOUR_NOT_TODAY); no other tour of the delegate IN_PROGRESS or SUSPENDED (409 TOUR_ALREADY_IN_PROGRESS) | startedAt; start position and accuracy when location permission is granted, otherwise startPositionMissing = true (cahier §6.1 « selon les permissions GPS »); location sampling starts; SSE tour.started; live map | delegate.tours.start |
IN_PROGRESS → SUSPENDED | Assigned delegate | Reason from TOUR_SUSPEND_REASON; no visit CHECKED_IN or IN_PROGRESS (409 VISIT_IN_PROGRESS) | Location sampling stops; the suspension interval is excluded from tour duration | delegate.tours.suspend |
IN_PROGRESS → SUSPENDED | SYSTEM at 21:00 — proposal of the jobs page, so that sampling never runs overnight when a delegate forgets to close | Status still IN_PROGRESS | Reason AUTO_END_OF_DAY; sampling stops; the delegate closes the tour when next online (within field.reopenMaxDays) | job field.autoSuspendOpenTours |
SUSPENDED → IN_PROGRESS | Assigned delegate | Tour not closed | Sampling resumes | delegate.tours.resume |
IN_PROGRESS, SUSPENDED → COMPLETED | Assigned delegate (DA-20) | No blocking item in the closing preview: every visit is DONE, NOT_DONE with a reason, or CANCELLED (409 TOUR_CLOSING_BLOCKED with details.blockingItems); with field.closingRequiresSync = true the app's offline queue for this tour is empty (the app does not offer « Terminer la tournée » otherwise) | completedAt, end position; kilometres computed from location pings (BR-54); final report generated (visits done/planned, sign-ups, installs, first orders, revenue, km, objectives, conversion) with the end-of-day comment; sampling stops; SSE tour.closed; objectives and dashboards recomputed | delegate.tours.closingPreview · delegate.tours.close |
COMPLETED → IN_PROGRESS | Back-office field.validate (BO-23) | Reason required; within field.reopenMaxDays (default 7) of the tour date | Delegate notified; the previous final report is kept as a version; finished visits stay locked unless unlocked one by one | admin.tours.reopen |
PLANNED, TO_START → CANCELLED | Back-office field.update | Reason required | Visits PLANNED → CANCELLED; delegate notified | admin.tours.cancel |
IN_PROGRESS, SUSPENDED → CANCELLED | Back-office field.update (delegate sick, vehicle breakdown) | Reason required | Visits not started → CANCELLED; finished visits kept; sampling stops | admin.tours.cancel |
Rules specific to tours:
- A
PLANNEDorTO_STARTtour can be edited (admin.tours.update: date, ordering mode, notes). Stops can be changed until closing (admin.tours.setStops), but only visits that have not started can be removed or moved; every change reaches the delegate throughsync.deltaand a push (cahier §6.2 « Une visite ajoutée ou déplacée doit être synchronisée »). - In
RECOMMENDEDordering mode the delegate may reorder the remaining visits (delegate.tours.reorder); inIMPOSEDmode the call returns409 TOUR_ORDER_IMPOSED. - Not started (derived): a tour still
TO_STARTafter its date. Not closed (derived): a tourIN_PROGRESSorSUSPENDEDafter its date. The 00:05 job lists both to the commercial admins. No tour is ever closed or cancelled automatically, because closing requires the delegate's report (the proposed 21:00 job only suspends).
Visit — VisitStatus
A stop of a tour, planned or added on the spot (cahier §6.3, §6.4, §6.8 — FLD-07 to FLD-10).
| Value | UI label (fr) | Meaning |
|---|---|---|
PLANNED | À visiter | |
CHECKED_IN | Arrivé | Arrival recorded (position + time). |
IN_PROGRESS | En cours | |
DONE | Visité | Finished; locked unless unlocked by permission. |
NOT_DONE | Non réalisée | Not carried out — a reason is mandatory. |
CANCELLED | Annulée |
'Late' (En retard) is derived: PLANNED and its time slot has passed.
| From → To | Trigger · actor (app) | Guards | Side effects | API operation |
|---|---|---|---|---|
— → PLANNED | Back-office planning (BO) · delegate adds an unplanned visit (DA, client-generated id, offline) | Reseller in scope, or a prospect just created | VIS- number (allocated when the unplanned visit reaches the server) | admin.tours.setStops · delegate.tours.addVisit |
PLANNED, NOT_DONE → CHECKED_IN | Assigned delegate taps « Arrivé » (DA DA-08, DA-10) | Tour IN_PROGRESS (409 TOUR_NOT_IN_PROGRESS); no other visit of the tour CHECKED_IN or IN_PROGRESS (409 VISIT_IN_PROGRESS); accuracy ≤ field.gpsMaxAccuracyMeters (422 GPS_ACCURACY_TOO_LOW); distance to the point of sale ≤ field.gpsToleranceMeters (422 GPS_OUT_OF_TOLERANCE, unless field.allowOutOfToleranceCheckIn and a justification is given); not a mock location (422 GPS_MOCK_LOCATION); NOT_DONE → CHECKED_IN only while the same tour is IN_PROGRESS | checkedInAt (occurredAt), position, accuracy, distance, outOfTolerance flag; a point of sale without coordinates gets this position as a candidate for back-office validation; SSE visit.checked_in (cahier §6.4 « en temps réel ») | delegate.visits.arrive |
CHECKED_IN → IN_PROGRESS | Assigned delegate (« Commencer la visite ») | — | startedAt; timer shown | delegate.visits.start |
IN_PROGRESS → DONE | Assigned delegate (« Terminer la visite ») | Visit type requirements met: mandatory checklist items checked, at least VisitType.minPhotos photos, report saved when the visit type requires it (default field.reportRequiredToEndVisit) — 422 VISIT_REQUIREMENTS_NOT_MET with details.missing | endedAt, duration; record locked; SSE visit.completed; Interaction VISIT on the reseller timeline; the report's next action becomes a CrmTask and may change the ResellerStatus; home counters updated (cahier §6.1) | delegate.visits.end |
PLANNED, CHECKED_IN, IN_PROGRESS → NOT_DONE | Assigned delegate (DA-11) | Reason from VISIT_NOT_DONE_REASON; comment required for OTHER | notDoneAt, position when available; listed in the closing summary | delegate.visits.notDone |
PLANNED → CANCELLED | Back-office removes the stop or cancels the tour | Visit not started | Kept in history, excluded from the visit rate | admin.tours.setStops · admin.tours.cancel |
DONE → DONE (unlocked) | Back-office field.validate (BO-25) | Reason required | unlockedUntil = now + field.visitUnlockHours (default 24); the delegate may edit checklist, note, photos and report; the record locks again at the next delegate.visits.end or when the window closes; audited | admin.visits.unlock |
While a visit is CHECKED_IN or IN_PROGRESS, the checklist, the quick note (≤ 300 characters), photos and the report are saved with delegate.visits.checklist, delegate.visits.note, delegate.visits.addPhoto and delegate.visits.report. On a locked visit these calls return 409 VISIT_LOCKED; an offline edit replayed after the end is rejected with that code and shown in the sync centre. Late (« En retard ») is derived: PLANNED, tour TO_START or IN_PROGRESS, and the end of its time slot has passed (Africa/Algiers); visits without a time slot are never late.
Follow-up task — TaskStatus
Follow-ups (relances) and next actions on a reseller (cahier §4.2, §6.8 — CRM-08). Stored as CrmTask.
| Value | UI label (fr) | Meaning |
|---|---|---|
OPEN | À faire | |
DONE | Faite | |
CANCELLED | Annulée |
'Overdue' is derived (OPEN and dueAt in the past), never stored.
| From → To | Trigger · actor (app) | Guards | Side effects | API operation |
|---|---|---|---|---|
— → OPEN | Visit report with a next action other than NONE (DA) · back-office crm.update · SYSTEM inactivity scan | Due date required; assignee is the delegate of the reseller by default | NextAction maps to TaskType: PHONE_FOLLOW_UP → CALL, NEW_VISIT → VISIT, SEND_CATALOG → SEND_CATALOG, WHATSAPP_MESSAGE → WHATSAPP; one open follow-up per report (saving the report again updates it); due tasks are announced in a morning digest at 07:30 Africa/Algiers (job crm.followUpReminders) | delegate.visits.report · admin.tasks.create · job crm.inactivityScan |
OPEN → DONE | Assignee (DA) · back-office crm.update | Outcome text required | completedAt; Interaction on the timeline | delegate.tasks.complete · admin.tasks.update |
OPEN → CANCELLED | Back-office crm.update · SYSTEM when the reseller becomes NOT_INTERESTED | — | — | admin.tasks.update |
DONE → OPEN | Back-office crm.update | Reason | — | admin.tasks.update |
Rescheduling or reassigning does not change the status (delegate.tasks.update, admin.tasks.update). Overdue is derived: OPEN and dueAt in the past.
Support ticket and after-sales claim — TicketStatus
Reseller requests, claims and after-sales service (cahier §2.1 « demandes SAV et réclamations » — SUP-01, SUP-02, SUP-04). A ticket of type AFTER_SALES is a claim on items of a delivered order: it carries the order, the claimed lines and quantities (TicketItem), the issue and photos, and it ends with a resolution.
| Value | UI label (fr) | Meaning |
|---|---|---|
AFTER_SALES | Service après-vente (SAV) | Claim on items of an order — faulty, damaged, wrong or missing product. |
ORDER_ISSUE | Problème de commande | |
DELIVERY_ISSUE | Problème de livraison | |
BILLING | Facturation | |
QUESTION | Question | |
OTHER | Autre |
| Value | UI label (fr) | Meaning |
|---|---|---|
OPEN | Ouverte | |
IN_PROGRESS | En cours de traitement | |
WAITING_RESELLER | En attente de votre réponse | |
RESOLVED | Résolue | A resolution was applied (for SAV: see AfterSalesResolution). |
REJECTED | Rejetée | Claim refused with a reason (e.g. out of warranty, damage by misuse). |
CLOSED | Fermée |
| From → To | Trigger · actor (app) | Guards | Side effects | API operation |
|---|---|---|---|---|
— → OPEN | Reseller (RA RA-33, RW RW-14) | Type, subject, message; a related order must be the reseller's. For AFTER_SALES: order DELIVERED, within the after-sales window (BR-81, 422 AFTER_SALES_WINDOW_CLOSED), at least one line with quantity ≤ delivered − already claimed or returned, an AfterSalesIssue, at least one photo | TKT-YYYY-NNNNNN; support queue notified (BO-55); claims flagged « SAV » | tickets.create |
OPEN → IN_PROGRESS | support.update assigns, or posts the first public reply | — | firstResponseAt (support KPI); reseller sees « En cours de traitement » | admin.tickets.update · admin.tickets.reply |
IN_PROGRESS → WAITING_RESELLER | support.update — reply flagged as a question (for claims: more photos, serial number…) | — | Push and live event to the reseller; label « En attente de votre réponse » | admin.tickets.reply · admin.tickets.update |
WAITING_RESELLER, RESOLVED, REJECTED → IN_PROGRESS | SYSTEM when the reseller replies | Ticket not CLOSED; a rejected claim can be contested once | Assignee notified | tickets.reply |
IN_PROGRESS → RESOLVED (non-SAV) | support.update | — | resolvedAt; push to the reseller | admin.tickets.update |
IN_PROGRESS → RESOLVED (SAV) | support.validate applies a resolution REPLACEMENT, RETURN, CREDIT_NOTE or REPAIR | Resolution details: lines and quantities (≤ claimed); for CREDIT_NOTE the amount (≤ claimed lines at snapshot prices) | REPLACEMENT: free order created in SUBMITTED (waits for confirmation like any order) · RETURN: return created in APPROVED · CREDIT_NOTE: credit note issued against the order's invoice · REPAIR: repair note and expected date; resolvedAt; push and live event with the resolution | admin.tickets.resolve |
IN_PROGRESS → REJECTED (SAV) | support.validate — resolution REJECTED | Reason required (out of warranty, damage by misuse, outside the window…) | Reseller notified with the reason | admin.tickets.resolve |
OPEN, IN_PROGRESS, RESOLVED, REJECTED → CLOSED | Reseller (optional 1–5 rating) | — | closedAt | tickets.close |
RESOLVED, REJECTED, WAITING_RESELLER → CLOSED | SYSTEM after support.autoCloseDays (default 7) without reseller reply | Reminder sent at half the delay | — | job support.autoClose |
| Value | UI label (fr) | Meaning |
|---|---|---|
DEFECTIVE | Produit défectueux | |
DAMAGED | Produit endommagé | |
WRONG_ITEM | Mauvais article | |
MISSING_ITEM | Article manquant | |
NOT_AS_DESCRIBED | Non conforme | |
OTHER | Autre |
| Value | UI label (fr) | Meaning |
|---|---|---|
REPLACEMENT | Remplacement | A free replacement order is created and goes through the normal order flow. |
RETURN | Retour | A return request is created; reception re-enters stock and issues a credit note. |
CREDIT_NOTE | Avoir | A credit note without physical return. |
REPAIR | Réparation | Item repaired under warranty. |
REJECTED | Refusée | Claim refused with a reason. |
Internal notes never change the status and are never shown to the reseller. A reply to a CLOSED ticket is refused (409 TICKET_TRANSITION_NOT_ALLOWED); the reseller opens a new ticket that references the old one. A REPLACEMENT order and a RETURN created by a resolution carry the ticket id, so the claim, the replacement and the stock movements can be followed from BO-56 and BO-69.
Invitation funnel — InvitationStep
The activation steps followed for each invitation (cahier §6.6 « Suivre : application téléchargée, compte créé, téléphone vérifié et catalogue consulté » — IVT-03). Steps are events, not a status: each reached step is an InvitationEvent; the funnel shows them in enum order.
| Value | UI label (fr) | Meaning |
|---|---|---|
SENT | Invitation envoyée | |
APP_INSTALLED | Application téléchargée | |
ACCOUNT_CREATED | Compte créé | |
PHONE_VERIFIED | Téléphone vérifié | |
CATALOG_VIEWED | Catalogue consulté | |
FIRST_ORDER | Première commande |
| Step | Event that sets it | API operation | Other effects |
|---|---|---|---|
SENT | The delegate shares the invitation through any channel (QR displayed, WHATSAPP, SMS, LINK copied) | delegate.invitations.create then delegate.invitations.markShared | Share channel recorded |
APP_INSTALLED | First open of the reseller app attributed by the Play install referrer, the app link https://app.hoco.dz/i/… or the code typed at sign-up | invitations.attribute | ResellerStatus APP_INSTALLED; push to the delegate |
ACCOUNT_CREATED | Account created or claimed with this invitation | auth.signup.reseller | ResellerStatus ACCOUNT_CREATED |
PHONE_VERIFIED | The OTP of that sign-up was verified — recorded together with ACCOUNT_CREATED, because an account cannot exist without it | auth.otp.verify + auth.signup.reseller | — |
CATALOG_VIEWED | First successful catalog read by the new account (guest browsing before sign-up does not count) | catalog.home · catalog.products.list · catalog.products.get | — |
FIRST_ORDER | First order submitted (including an assisted order confirmed) | cart.checkout · orders.confirmAssisted · admin.orders.confirmAssisted | ResellerStatus FIRST_ORDER |
Rules: the funnel is monotonic — a later step implies the earlier ones, which are back-filled with inferred = true (a reseller who installed from the store without the link and typed the code at sign-up gets APP_INSTALLED inferred). A reseller has at most one active invitation; asking again returns it while it is valid (invitations.validityDays, default 30). The sign-up is attributed to the invitation's delegate (cahier §12 « attribuent l'installation au bon délégué »).
Export and import jobs — JobStatus
Asynchronous exports (PDF, XLSX, CSV — cahier §9) and imports (cahier §13.2) — ExportJob, ImportJob.
| Value | UI label (fr) | Meaning |
|---|---|---|
QUEUED | En file | |
RUNNING | En cours | |
SUCCEEDED | Terminé | |
FAILED | Échec | |
EXPIRED | Expiré |
| From → To | Trigger · actor (app) | Guards | Side effects | API operation |
|---|---|---|---|---|
— → QUEUED | Back-office user with the export or import permission | Filters validated; the export only contains what the requester may see (scope and masked fields) | AuditLog EXPORT or IMPORT | admin.exports.create · admin.resellers.export · admin.products.export · admin.imports.create · admin.products.import |
QUEUED → RUNNING → SUCCEEDED | SYSTEM (worker) | — | File stored (GENERATED_DOCUMENT or IMPORT_FILE report); in-app notification and SSE to the requester; imports: dry-run report, then apply as a second run | admin.exports.get · admin.imports.get |
RUNNING → QUEUED | SYSTEM | Transient error; attempts below 3; exponential backoff | — | — |
RUNNING → FAILED | SYSTEM | Third failure or invalid file | Error summary and row-level report for imports | admin.imports.get |
SUCCEEDED → EXPIRED | SYSTEM daily | exports.retentionDays (default 7) passed | File deleted; the job row stays for audit | job exports.expire |
V2 — loyalty points
Loyalty is V2 (cahier §14.2); its operations come with the V2 contract (SRV-29). The points ledger is append-only: the balance is the sum of LoyaltyTransaction rows; nothing is edited.
| Value | UI label (fr) | Meaning |
|---|---|---|
EARN | Gain | |
BONUS | Bonus | |
REDEEM | Échange | |
EXPIRE | Expiration | |
ADJUST | Ajustement | |
REVERSAL | Annulation |
| Type | Sign | Created when | Rule |
|---|---|---|---|
EARN | + | Order reaches loyalty.earnOn (DELIVERED or invoice PAID) | Points on the order total TTC without delivery fee (BR-40) |
BONUS | + | Campaign rule (first order, target product) | Linked to the campaign |
REDEEM | − | Reward redeemed (loyalty.redeem) | Refused if the balance is insufficient or the reward unavailable (cahier §5.5) |
EXPIRE | − | Expiry job | Points older than loyalty.pointsValidityMonths, oldest first |
ADJUST | ± | Manual adjustment (loyalty.update) | Reason required; audited |
REVERSAL | − or + | Order cancelled, return refunded or after-sales credit note after EARN; redemption cancelled | Cancels exactly the original amount, pro rata for partial returns (cahier §4.6 « Les points annulés ou remboursés doivent être régularisés ») |
Timestamps and history
Every status change is dated and kept (cahier §4.4 « Toute modification d'une commande doit être historisée », §5.4 « Chaque changement de statut doit être daté », AUD-02). Four lifecycles have their own history table; the others are traced in the AuditLog and in business timestamps.
| History table | Records | Key columns | Read by |
|---|---|---|---|
OrderStatusChange | Every order transition, creation included | orderId, fromStatus (null at creation), toStatus, reasonCode, comment, actorUserId, actorApp, trigger (user or job/event key), occurredAt, createdAt | Reseller timeline in orders.get (status, date, public reason only); admin.orders.history |
OrderChange | Line modifications and other non-status edits | orderId, changeType, before/after diff, reason, actor | admin.orders.history |
ResellerStatusChange | Every reseller status change | resellerId, from, to, trigger (MANUAL, AUTOMATIC, IMPORT), eventKey, reason, actor, occurredAt | Reseller 360° timeline (mirrored as an Interaction STATUS_CHANGE), conversion reports |
TourStatusChange | Every tour status change, suspensions and reopenings included | tourId, from, to, reasonCode, comment, position (lat, lng, accuracy), occurredAt, receivedAt, actor | Tour live detail, active time, field reports |
InvitationEvent | Each funnel step reached | invitationId, step, occurredAt, inferred, attribution source | DA-16, invitation funnel report |
ResellerAssignment · CreditLimitChange · PriceChange · StockMovement · V2 LoyaltyTransaction | Assignment, credit limit, price (the three prices), stock and points histories | — | Their module screens |
AuditLog | Account status, validation decisions, customer type changes, product, promotion, return, invoice, payment, ticket and task status changes, after-sales resolutions, overrides, unlocks, permission changes | action (AuditAction), entityType, entityId, before/after, actor, app, IP, requestId | BO-63 |
Each entity also stores the timestamp of its current status (statusChangedAt) and its business dates, so lists can sort and filter without reading history: orders (submittedAt, validatedAt, shippedAt, deliveredAt, cancelledAt, assistedExpiresAt…), returns (requestedAt, decidedAt, receivedAt, refundedAt), tickets (firstResponseAt, resolvedAt, closedAt, resolution), visits (checkedInAt, startedAt, endedAt, notDoneAt, lockedAt, unlockedUntil), tours (startedAt, completedAt), invoices (issuedAt, dueDate, paidAt), payments (declaredAt, verifiedAt). Offline actions keep both occurredAt (device) and receivedAt (server).
Scheduled jobs that change statuses
| Job | Schedule (Africa/Algiers) | Moves |
|---|---|---|
tours.activateToday | Daily 00:05 | Tour PLANNED → TO_START; lists tours not started or not closed the day before |
invoices.markOverdue | Daily 01:00 | Invoice → OVERDUE (acts only on invoices that carry a due date) |
crm.inactivityScan | Daily 02:00 | Reseller ACTIVE, ACCOUNT_CREATED → TO_FOLLOW_UP; creates follow-up tasks |
orders.expireAssisted | Every 15 minutes | AssistedConfirmation → EXPIRED, order → CANCELLED; sends the 24-hour reminder |
promotions.applySchedule | Every minute | Promotion SCHEDULED → ACTIVE, ACTIVE or PAUSED → ENDED |
support.autoClose | Daily 03:00 | Ticket RESOLVED, REJECTED, WAITING_RESELLER → CLOSED |
exports.expire | Daily 04:45 | Job SUCCEEDED → EXPIRED |
field.autoSuspendOpenTours (proposal) | Daily 21:00 | Tour IN_PROGRESS → SUSPENDED |
No job ever confirms an order: orders.staleConfirmationAlert (proposal) only alerts validators when the confirmation queue waits too long. The jobs and scheduler page owns their definition, retries and monitoring.
Source traceability
Every section of the cahier des charges mapped to requirement ids, screens and the pages that cover it, the list of team additions with their rationale, and how traceability is kept when the cahier is revised.
Business rules
Numbered rules BR-01 to BR-86 grouped like cahier §8, with source, tuning setting, default and worked examples, followed by the catalogue of every business setting.