HOCO B2B
SRS

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:

ColumnMeaning
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).
GuardsPreconditions checked by the server. A failed guard returns the error shown; nothing changes.
Side effectsWhat happens in the same database transaction (stock, numbers, history, outbox event) and right after commit (notifications, SSE, analytics).
API operationThe operation id that performs the move (API reference). job … marks a scheduled job (jobs and scheduler).

Rules that hold for every lifecycle:

  1. 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 orders ORDER_TRANSITION_NOT_ALLOWED) and details: { from, to, allowed: [...] }.
  2. 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 OutboxEvent table, so a failed push never rolls back a confirmed order.
  3. The actor is always recorded: user id and ClientApp (DASHBOARD, RESELLER_APP, RESELLER_WEB, DELEGATE_APP, SYSTEM). Automatic moves record SYSTEM plus the job or event key that caused them.
  4. 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 item OTHER makes the free-text comment mandatory. A missing reason returns 422 REASON_REQUIRED.
  5. Idempotent and offline-safe. Every transition operation accepts an Idempotency-Key; a replay returns the original result. Delegate actions recorded offline carry occurredAt; time-based guards (such as "the tour date is today") use the Africa/Algiers date of occurredAt, not of receivedAt.
  6. Derived states are never stored. "Late" visits, "overdue" tasks and "not started" tours are computed at read time.
  7. 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.

ValueUI label (fr)Meaning
NEW_PROSPECTNouveau prospectJust created; no qualified contact yet.
TO_CONTACTÀ contacterPlanned for a call or a visit.
INTERESTEDIntéresséInterest confirmed during a visit or call.
APP_INSTALLEDApplication installéeInvitation attributed an install (automatic).
ACCOUNT_CREATEDCompte crééReseller account created and phone verified (automatic).
FIRST_ORDERPremière commandeFirst order submitted (automatic).
ACTIVERevendeur actifFirst order delivered, or a validated order within the activity window (automatic).
NOT_INTERESTEDNon intéresséDeclined; kept for later campaigns.
TO_FOLLOW_UPÀ relancerA follow-up is due (from a visit report or inactivity).
SUSPENDEDSuspenduCommercially suspended (unpaid, fraud, closure); ordering blocked.

The ten values form three families:

FamilyValuesWho moves them
Prospect qualificationNEW_PROSPECT, TO_CONTACT, INTERESTED, NOT_INTERESTED, TO_FOLLOW_UPPeople — 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 funnelAPP_INSTALLED, ACCOUNT_CREATED, FIRST_ORDER, ACTIVESYSTEM only, from events. Never set by hand.
Commercial blockSUSPENDEDBack-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 → ToTrigger · actor (app)GuardsSide effectsAPI operation
— → NEW_PROSPECTDelegate 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 wilayasource = 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 allocateddelegate.resellers.create · admin.resellers.create
— → ACCOUNT_CREATEDReseller signs up in RA or RW with a phone that is not in the CRMValid OTP verification token; customer type declared (AUTH-11)source = SELF_SIGNUP; AccountStatus PENDING_VALIDATION; ValidationStatus PENDING; invitation attributed when a code is given; validators notifiedauth.signup.reseller
NEW_PROSPECT → TO_CONTACTSYSTEM — the prospect is added as a stop of a tour, or an open CALL or VISIT task is created for itCurrent status is NEW_PROSPECT—side effect of admin.tours.create · admin.tours.setStops · admin.tasks.create
qualification statuses → APP_INSTALLEDSYSTEM — the invitation of this reseller is attributed to a first app openNot SUSPENDED; reseller not yet further in the funnelInvitationStep APP_INSTALLED; push to the inviting delegateinvitations.attribute
qualification statuses, APP_INSTALLED → ACCOUNT_CREATEDSYSTEM — the owner claims the record: sign-up with the same phone, OTP verified (AUTH-05)Not SUSPENDED; no DISABLED user on the recordUser created; AccountStatus ACTIVE if ValidationStatus is APPROVED, else PENDING_VALIDATION; InvitationStep ACCOUNT_CREATED and PHONE_VERIFIEDauth.signup.reseller
any status without a previous first order (except SUSPENDED) → FIRST_ORDERSYSTEM — the reseller's first order is submitted: checkout, confirmation of an assisted order, or back-office orderThe order is the first non-draft order that is not REFUSED or CANCELLEDOrder.isFirstOrder = true; InvitationStep FIRST_ORDER; push to the attributed delegate (BR-27)cart.checkout · orders.confirmAssisted · admin.orders.confirmAssisted · admin.orders.create
FIRST_ORDER → ACTIVESYSTEM — an order of the reseller reaches DELIVERED—Conversion counted for the attributed delegateadmin.deliveries.proof
FIRST_ORDER → the status before itSYSTEM — 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 reachedadmin.orders.refuse · orders.cancel · admin.orders.cancel · job orders.expireAssisted
ACTIVE, ACCOUNT_CREATED → TO_FOLLOW_UPSYSTEM — 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 SUSPENDEDCrmTask of type CALL « Relance inactivité » for the assigned delegate (commercial admins when unassigned), due the next working day; push in the FIELD categoryjob crm.inactivityScan
TO_FOLLOW_UP → ACTIVESYSTEM — an order of a reseller who already had a delivered order is confirmed (VALIDATED)At least one past DELIVERED orderOpen inactivity task closed (DONE, outcome « Commande reçue »)admin.orders.validate

Manual transitions

From → ToTrigger · actor (app)GuardsSide effectsAPI operation
NEW_PROSPECT, TO_CONTACT, INTERESTED, NOT_INTERESTED, TO_FOLLOW_UP → TO_CONTACT, INTERESTED, NOT_INTERESTED, TO_FOLLOW_UPDelegate 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 dateResellerStatusChange; Interaction of type STATUS_CHANGE on the timeline; TO_FOLLOW_UP creates the follow-up CrmTask; NOT_INTERESTED cancels the record's open tasksdelegate.resellers.update · delegate.visits.report · admin.resellers.setStatus · admin.resellers.bulk
any → SUSPENDEDBack-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 statusBack-office crm.updateReason requiredPrevious status read from the last ResellerStatusChange, then automation re-evaluated (an order delivered in the meantime makes it ACTIVE)admin.resellers.setStatus
— → any statusInitial load of existing customers (settings.administer, ADM-06)Only through the reseller import; customer type required per rowsource = IMPORT; ValidationStatus APPROVED; existing buyers loaded ACTIVEadmin.imports.create

Rules specific to resellers:

  1. Automatic events only move a reseller forward in the funnel order NEW_PROSPECT → APP_INSTALLED → ACCOUNT_CREATED → FIRST_ORDER → ACTIVE. They lift TO_CONTACT, INTERESTED, NOT_INTERESTED and TO_FOLLOW_UP; they never lift SUSPENDED — the event is recorded on the timeline and the status stays.
  2. The four funnel statuses cannot be set by hand: 409 RESELLER_TRANSITION_NOT_ALLOWED with details.reason = "AUTOMATIC_STATUS".
  3. Only two automatic moves go backwards: the first-order revert and the inactivity rule.
  4. 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 CONFLICT by sync.batch and 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.

ValueUI label (fr)Meaning
SUPER_GROSSuper-grosLarge wholesaler — largest volumes; lowest price.
DEMI_GROSDemi-grosSemi-wholesaler — intermediate volumes and price.
DETAILDétailRetail shop — smallest volumes; highest of the three prices.
From → ToTrigger · actor (app)GuardsSide effectsAPI operation
— → declared typeReseller at sign-up (RA, RW) · delegate at field creation (DA) · back-office creation or importRequired; one of the three valuesPrices of that type shown right away when catalog.pricesVisibleBeforeValidation is true (default); ordering still waits for validationauth.signup.reseller · delegate.resellers.create · admin.resellers.create
declared → confirmed or another typeValidator crm.validate while approving the account (BO-13)Account PENDING validationcustomerTypeConfirmedAt; when changed, the reseller is told the new tier in the approval message; AuditLog UPDATE with before and afteradmin.resellers.validate
any → another typeBack-office crm.update (BO-11)Reason requiredAuditLog 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).

ValueUI label (fr)Meaning
PENDING_VALIDATIONEn attente de validationReseller signed up; can browse the catalog (if allowed by settings) but cannot order until validated.
ACTIVEActifFull access for the user's role.
SUSPENDEDSuspenduTemporarily blocked; sign-in refused with an explicit message; sessions revoked.
DISABLEDDésactivéPermanently deactivated; history kept, sign-in impossible.
From → ToTrigger · actor (app)GuardsSide effectsAPI operation
— → PENDING_VALIDATIONReseller signs up or claims a record whose ValidationStatus is not APPROVED (RA, RW)OTP verified; password policy; customer type declaredSession opened; the app shows RA-06auth.signup.reseller
— → ACTIVEAdmin 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 recordPhone or email unique among usersWelcome message with the temporary passwordadmin.users.create · admin.delegates.crud · auth.signup.reseller
PENDING_VALIDATION → ACTIVESYSTEM when the account is approved by a crm.validate holderValidationStatus becomes APPROVEDSMS and push « Votre compte est validé »; checkout unlockedadmin.resellers.validate
PENDING_VALIDATION → DISABLEDBack-office users.updateReason requiredSessions revokedadmin.users.setStatus
ACTIVE → SUSPENDEDBack-office users.updateReason 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 → ACTIVEBack-office users.update—The user signs in againadmin.users.setStatus
ACTIVE, SUSPENDED → DISABLEDBack-office users.updateSame 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 accountadmin.users.setStatus

What a visitor or a reseller account can do (AUTH-10, BR-61):

CapabilityGuest (no account)PENDING_VALIDATIONACTIVESUSPENDED, 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.

ValueUI label (fr)Meaning
PENDINGEn attente
APPROVEDValidé
REJECTEDRefusé
From → ToTrigger · actor (app)GuardsSide effectsAPI operation
— → PENDINGField creation (DA), self sign-up (RA, RW)—Queue entry with duplicate hints (BR-05); SSE account.pending; validators reminded after crm.validationReminderHoursdelegate.resellers.create · auth.signup.reseller
— → APPROVEDRecord created by a back-office user or imported——admin.resellers.create · admin.imports.create
PENDING → APPROVEDBack-office crm.validateDuplicate 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 → REJECTEDBack-office crm.validateReason required (10–500 characters, shown to the reseller)Reseller sees the reason on RA-06; delegate notified; the record keeps its commercial statusadmin.resellers.validate
REJECTED → PENDINGSYSTEM when the reseller edits the store details, or the delegate corrects the point of saleAt least one field changedBack in the queue with the change historyprofile.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).

ValueUI label (fr)Meaning
DRAFTBrouillonBeing prepared; invisible to resellers.
ACTIVEActifVisible and orderable (subject to visibility rules).
HIDDENMasquéKept but invisible to resellers.
OUT_OF_STOCKEn ruptureSet automatically when every variant's available stock is 0; back to ACTIVE on restock.
ARCHIVEDArchivéRetired; remains referenced by past orders.
From → ToTrigger · actor (app)GuardsSide effectsAPI operation
— → DRAFTcatalog.create (BO) or product importReference unique per brandDefault variant createdadmin.products.create · admin.products.import
DRAFT → ACTIVEcatalog.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 missingpublishedAt set (drives the « Nouveautés » filter and sort); if every variant has 0 available the product goes straight to OUT_OF_STOCKadmin.products.setStatus
ACTIVE → HIDDEN, HIDDEN → ACTIVEcatalog.update—Hidden products disappear from catalog, search, facets, favorites and promotions at the next read; cart lines become blocking issues PRODUCT_UNAVAILABLEadmin.products.setStatus
ACTIVE → OUT_OF_STOCKSYSTEM — the sum of available stock of all variants in sellable warehouses reaches 0Status was ACTIVEProduct stays visible with « En rupture » and cannot be ordered; stock alert raisedstock movement handler
OUT_OF_STOCK → ACTIVESYSTEM — available stock of any variant becomes positive (receipt, cancellation, return reception)The product was put out of stock by the systemBack in the « En stock » filterstock movement handler
OUT_OF_STOCK → HIDDENcatalog.update—Showing it again recomputes ACTIVE or OUT_OF_STOCK from stockadmin.products.setStatus
any except ARCHIVED → ARCHIVEDcatalog.update—Removed from every reseller surface; still referenced by past orders, invoices, returns and price historyadmin.products.setStatus
ARCHIVED → HIDDENcatalog.administer—Must be shown explicitly afterwardsadmin.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.

ValueUI label (fr)Meaning
DRAFTBrouillonAssisted order being prepared by a delegate, or a back-office draft.
SUBMITTEDEnvoyéePlaced by the reseller (or an assisted order confirmed by the reseller); waits for the back-office. Orders are never confirmed automatically.
PENDINGEn attenteOn hold: needs information, a stock/credit decision, or the reseller's confirmation of an assisted order.
VALIDATEDValidéeConfirmed by a back-office user (orders.validate); stock reserved (default setting).
REFUSEDRefuséeRefused with a reason; terminal.
PREPARINGEn préparationPicking in progress.
READYPrêtePacked, waiting for the carrier.
SHIPPEDExpédiéeHanded to the carrier; stock deducted (default setting).
DELIVEREDLivréeProof of delivery recorded.
CANCELLEDAnnuléeCancelled by the reseller (before confirmation) or the back-office (with reason); reserved quantities return to stock automatically.
RETURNEDRetournéeReturned 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):

ChannelCreated byFirst statusNumber allocated
RESELLER_APP, RESELLER_WEBReseller checkout (the cart is the draft)SUBMITTED (or PENDING at once when a control needs a decision)At creation
DELEGATE_ASSISTEDDelegate's first-order assistance (DA-17)DRAFT, then PENDING awaiting the reseller's confirmationWhen sent for confirmation
BACK_OFFICEorders.create on behalf of a reseller (BO-40, phone orders), or a free replacement created by an after-sales resolutionSUBMITTED, or DRAFT when saved as a draftWhen 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 → ToTrigger · actor (app)GuardsSide effectsAPI operation
— → SUBMITTEDReseller (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 resellercart.checkout
— → SUBMITTEDBack-office orders.create (BO-40)Same controls as checkout except the reseller's AccountStatus (phone orders for prospects are allowed); no override at creationChannel BACK_OFFICE, placedByUserId; reseller notified « Commande créée pour vous »; enters the confirmation queue like any orderadmin.orders.create
— → SUBMITTEDSYSTEM — after-sales resolution REPLACEMENTThe claim's lines and quantitiesFree order (lines at price 0, replacementForTicketId); goes through confirmation, preparation and delivery like any orderadmin.tickets.resolve
— → DRAFTDelegate on a reseller in scope (DA) · back-office draft (BO)ResellerStatus not SUSPENDEDPriced with the reseller's customer-type prices; no number, invisible to the reseller; no stock effectdelegate.assisted.create · admin.orders.create
DRAFT → deletedThe delegate who created it (DA)Status DRAFTHard delete, audit entry keptdelegate.assisted.discard
DRAFT → PENDINGDelegate 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-41delegate.assisted.submit
DRAFT → SUBMITTEDBack-office submits its own draft (BO)Same controls as back-office creationAs back-office creationadmin.orders.transition
PENDING → SUBMITTEDReseller 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 confirmedAssistedConfirmation 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.validateorders.confirmAssisted · admin.orders.confirmAssisted
SUBMITTED → PENDINGSYSTEM right after submission or confirmation · back-office orders.validate puts the order on holdSystem: 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-apporder controls handler · admin.orders.transition
SUBMITTED → VALIDATEDBack-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 = VALIDATEDadmin.orders.validate
PENDING → VALIDATEDBack-office orders.validate; for CREDIT_OVER_LIMIT only, a finance.validate holderThe 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 aboveadmin.orders.validate
SUBMITTED, PENDING → REFUSEDBack-office orders.validateReason from ORDER_REFUSAL_REASONReservation 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 → CANCELLEDReseller 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_REASONAs refusal; back-office alerted in-app; the order leaves the confirmation queueorders.cancel
PENDING → CANCELLEDReseller rejects the assisted order (RA, RW) · SYSTEM when 72 h pass without an answerReject: reason required. Expiry: assistedExpiresAt passedAssistedConfirmation REJECTED_BY_RESELLER or EXPIRED; system reason « Refusée par le revendeur » or « Délai de confirmation expiré »; delegate notifiedorders.rejectAssisted · job orders.expireAssisted
SUBMITTED, PENDING, VALIDATED, PREPARING, READY → CANCELLEDBack-office orders.updateReason 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 notifiedadmin.orders.cancel
VALIDATED → PREPARINGDelivery manager or any orders.update / delivery.update holder (BO-42)—DeliveryStatus PREPARING; picking list available; reseller sees « En préparation » liveadmin.orders.transition · admin.deliveries.pickingList
PREPARING → READYSamePicking confirmed for every line (shortages handled by a line modification first)BL-YYYY-NNNNNN allocated and delivery note PDF generated; package count recorded; DeliveryStatus READYadmin.orders.transition
READY → SHIPPEDSameDelivery 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 referenceadmin.orders.transition · admin.deliveries.update
READY → DELIVEREDSame, for PICKUP orders handed over at the depotRecipient name, photo or signatureDeduction and invoice happen in the same transition (crossing rule below); then as deliveryadmin.deliveries.proof
SHIPPED → DELIVEREDdelivery.update holder (BO)Proof: recipient name or photo, at least one; delivery timedeliveredAt (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 = DELIVEREDadmin.deliveries.proof
SHIPPED → RETURNEDdelivery.update or orders.update holderReason (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 notifiedadmin.orders.transition
DELIVERED → RETURNEDSYSTEM when received returns cover every delivered unitSum of accepted returned quantities = delivered quantitiesPartial returns leave the order DELIVERED with a « Retour partiel » badgeadmin.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).

EventreserveOn = VALIDATED (default)reserveOn = SUBMITTED
Order submitted—RESERVATION +qty
Order confirmed (VALIDATED)RESERVATION +qty—
Refused, cancelled or expiredRESERVATION_RELEASE −qty for what is reserved; RETURN +qty for what was already deductedsame
Line modified while reservedRESERVATION or RESERVATION_RELEASE for the differencesame
deductOn status reached (default SHIPPED)SHIPMENT: physical −qty, reserved −qtysame
Return received (admin.returns.receive)RETURN +accepted qty, posted automaticallysame

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

ActorDRAFTSUBMITTEDPENDINGVALIDATEDPREPARINGREADYSHIPPED and after
Reseller (RA, RW)invisiblecancelcancel (reject if assisted)———return request after DELIVERED
Delegate (DA)discard own draft——————
Back-office orders.validate—confirm · refuse · holdconfirm · refuse————
Back-office orders.updatedeletecancelcancelcancelcancelcancelRETURNED (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

ActionAllowed inWhoRules and effectsAPI operation
Controlled modification of linesSUBMITTED, PENDING, VALIDATED: any change · PREPARING: decrease or remove only (picking shortage)orders.updateReason 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 draftDRAFTCreating delegateRe-priced on every change; returns totals and issuesdelegate.assisted.update
Regenerate documentsREADY and afterorders.updateSame content, new PDF version; never changes amountsadmin.orders.documents
Request a returnDELIVERED, within returns.windowDaysResellerCreates a ReturnRequest (ReturnStatus)returns.create
Open an after-sales claimDELIVERED, within the after-sales windowResellerCreates a ticket of type AFTER_SALES (TicketStatus)tickets.create
ReorderDELIVERED, CANCELLED, REFUSED, RETURNEDResellerCopies available lines into the cart at today's prices; unavailable lines reportedorders.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 counterOrder statuses
En attenteSUBMITTED, PENDING
ValidéeVALIDATED
En préparationPREPARING, READY
ExpédiéeSHIPPED
LivréeDELIVERED
Off the tracker, with the reasonREFUSED, 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.

ValueUI label (fr)Meaning
AWAITING_RESELLEREn attente du revendeur
CONFIRMED_BY_RESELLERConfirmée par le revendeur
CONFIRMED_BY_ADMINValidée par l'administration
REJECTED_BY_RESELLERRefusée par le revendeur
EXPIREDExpiréeNot confirmed within the configured delay (default 72 h).
From → ToTrigger · actor (app)GuardsSide effectsAPI operation
— → AWAITING_RESELLERDelegate (DA)See DRAFT → PENDINGOrder PENDING; assistedExpiresAt = now + orders.assistedConfirmationHours; reminder push to the reseller 24 h before expirydelegate.assisted.submit
AWAITING_RESELLER → CONFIRMED_BY_RESELLERReseller (RA RA-23, RW RW-09)Before expiry; AccountStatus ACTIVE; totals shown equal the re-priced totalsOrder SUBMITTED, enters the confirmation queueorders.confirmAssisted
AWAITING_RESELLER → CONFIRMED_BY_ADMINBack-office orders.validate (BO-41) — typically the reseller has no app yet and confirmed by phoneBefore expiry; confirmation note requiredOrder SUBMITTED; the note is kept in the order history; the same user may then confirm the order with a second explicit actionadmin.orders.confirmAssisted
AWAITING_RESELLER → REJECTED_BY_RESELLERReseller (RA, RW)Reason requiredOrder CANCELLED; delegate notified with the reasonorders.rejectAssisted
AWAITING_RESELLER → EXPIREDSYSTEM — job every 15 minutesassistedExpiresAt passedOrder CANCELLED; delegate and reseller notifiedjob 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.

ValueUI label (fr)Meaning
REQUESTEDDemandé
APPROVEDAccepté
IN_TRANSITEn retourApproved; goods on their way back.
RECEIVEDReçu
REFUNDEDRemboursé / avoir émis
REJECTEDRefusé
From → ToTrigger · actor (app)GuardsSide effectsAPI operation
— → REQUESTEDReseller (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 SUSPENDEDRET-YYYY-NNNNNN; SSE to the back-office; orders.validate holders notifiedreturns.create
— → APPROVEDSYSTEM — after-sales resolution RETURNThe claim's lines and quantitiesLinked to the ticket; return instructions sent to the reselleradmin.tickets.resolve
— → IN_TRANSITSYSTEM — order SHIPPED → RETURNED (refused at the door)—Covers every shipped lineadmin.orders.transition
REQUESTED → APPROVEDBack-office orders.validate—Instructions to the reseller (bring to the depot, or collection date); push and live eventadmin.returns.decide
REQUESTED → REJECTEDBack-office orders.validateReason requiredReseller notified with the reasonadmin.returns.decide
APPROVED → IN_TRANSITBack-office (delivery manager) when a driver or carrier collects the goodsCollection detailsReseller notifiedadmin.returns.decide (collection recorded)
APPROVED, IN_TRANSIT → RECEIVEDStock stock.update at the depotReceived and accepted quantities per line (accepted ≤ received ≤ requested); a reason for every unit not acceptedRETURN 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 backadmin.returns.receive
RECEIVED → REFUNDEDSYSTEM, in the same operation, when accepted lines are invoicedAccepted quantity > 0CreditNote for the accepted lines at the order's snapshot prices, applied to the invoice balance (BR-39); reseller notified; V2: loyalty REVERSALadmin.returns.receive
RECEIVED → REJECTEDSYSTEM, in the same operationNothing acceptedReseller notified with the inspection reasonsadmin.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.

ValueUI label (fr)Meaning
UNPAIDImpayée
PARTIALLY_PAIDPartiellement payée
PAIDPayée
OVERDUEEn retardSet by a daily job when dueDate passes with a balance.
CANCELLEDAnnulé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 → ToTrigger · actor (app)GuardsSide effectsAPI operation
— → UNPAIDSYSTEM when the order reaches billing.invoiceOnOrder not already invoicedFAC-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 invoiceside effect of admin.orders.transition
UNPAID, PARTIALLY_PAID, OVERDUE → PARTIALLY_PAID, PAIDSYSTEM when a confirmed payment is allocated or a credit note is appliedAllocation ≤ balancepaidAt when fully paid; available credit recomputed; V2: loyalty points when loyalty.earnOn = PAIDadmin.payments.verify · admin.payments.record · admin.payments.allocate · admin.returns.receive · admin.tickets.resolve
UNPAID, PARTIALLY_PAID → OVERDUESYSTEM — daily job at 01:00 Africa/AlgiersBalance > 0 and dueDate passedFinance dashboard « Impayés »; V2: reminders and blocking (FIN-07)job invoices.markOverdue
PAID → PARTIALLY_PAID, UNPAID, OVERDUESYSTEM when an allocation is removedfinance.update on the allocationAudit entry with the reasonadmin.payments.allocate
UNPAID, OVERDUE → CANCELLEDFinance finance.validate (invoice issued by mistake) · SYSTEM when the order is cancelled after invoicingNo confirmed allocation (409 INVOICE_HAS_PAYMENTS — deallocate first); reasonA full credit note is issued — the invoice number is kept and never reusedadmin.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.

ValueUI label (fr)Meaning
PENDING_VERIFICATIONÀ vérifier
CONFIRMEDConfirmé
REJECTEDRejeté
From → ToTrigger · actor (app)GuardsSide effectsAPI operation
— → PENDING_VERIFICATIONReseller 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 requiredPAY-YYYY-NNNNNN; finance notified in-app; available credit not changed yetpayments.declare
— → PENDING_VERIFICATIONCash collected on delivery of a CASH_ON_DELIVERY order (BO)Amount collected entered with the proof of deliveryPayment linked to the order and its invoiceadmin.deliveries.proof
— → CONFIRMEDFinance finance.create records a payment it has seen (BO-45)Allocations ≤ amountAllocations applied; invoices re-evaluated; reseller notifiedadmin.payments.record
PENDING_VERIFICATION → CONFIRMEDFinance finance.validateAllocations ≤ amount; by default the oldest due invoices firstAs above; unallocated remainder kept as an advance that lowers credit exposureadmin.payments.verify
PENDING_VERIFICATION → REJECTEDFinance finance.validateReason from PAYMENT_REJECT_REASONReseller notified with the reason; proof keptadmin.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.

ValueUI label (fr)Meaning
TO_PREPAREÀ préparer
PREPARINGEn préparation
READYPrête
SHIPPEDExpédiée
DELIVEREDLivrée
FAILEDÉchec de livraison
RETURNEDRetournée
Order statusDelivery statusWhat the delivery record holdsAPI operation
VALIDATEDTO_PREPAREMode (OWN_FLEET, CARRIER, PICKUP) proposed from the address and the delivery settingscreated by admin.orders.validate
PREPARINGPREPARINGPicker, picking list printedadmin.orders.transition
READYREADYPackages, delivery note BL-admin.orders.transition
SHIPPEDSHIPPEDCarrier or driver, tracking number, shippedAtadmin.orders.transition · admin.deliveries.update
SHIPPED (unchanged)FAILEDAttempt number, failure reason, next attempt dateadmin.deliveries.update
DELIVEREDDELIVEREDProof (photo DELIVERY_PROOF, recipient name), deliveredAt, cash collectedadmin.deliveries.proof
RETURNED (door refusal)RETURNEDReason, linked return requestadmin.orders.transition
CANCELLED (from VALIDATED, PREPARING, READY)last value kept, cancelledAt setLeaves the delivery boardadmin.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).

ValueUI label (fr)Meaning
DRAFTBrouillon
SCHEDULEDProgrammée
ACTIVEActive
PAUSEDEn pause
ENDEDTerminéePast its end date or stock cap reached.
ARCHIVEDArchivée
From → ToTrigger · actor (app)GuardsSide effectsAPI operation
— → DRAFTpromotions.create (BO-50)——admin.promotions.crud
DRAFT → SCHEDULEDpromotions.validateValue 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 validVisible in the back-office calendaradmin.promotions.setStatus
SCHEDULED → DRAFTpromotions.validateNot startedEditable againadmin.promotions.setStatus
DRAFT → ACTIVE, SCHEDULED → ACTIVEpromotions.validate (activate now) · SYSTEM at startsAt — job every minuteSame as schedulingApplied 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 → ACTIVEpromotions.validateResume only before endsAtCarts re-priced at the next read; banners hidden while pausedadmin.promotions.setStatus
ACTIVE, PAUSED → ENDEDSYSTEM at endsAt (job) · SYSTEM when the quantity cap is fully consumed (checkout)—Banners hidden; carts show the issue « Offre terminée » and the normal pricejob promotions.applySchedule · cart.checkout
DRAFT, ENDED → ARCHIVEDpromotions.validate—Kept for reports; never applied againadmin.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.

ValueUI label (fr)Meaning
PLANNEDPlanifiéeScheduled for a future day.
TO_STARTÀ démarrerDay of the tour has come (set by the 00:05 job); the start button is enabled.
IN_PROGRESSEn coursStarted by the assigned delegate (time + position recorded).
SUSPENDEDSuspenduePaused with a reason (break, incident); location sampling stops.
COMPLETEDTerminéeClosed with the final report; definitive unless reopened.
CANCELLEDAnnuléeCancelled by the back-office with a reason.
From → ToTrigger · actor (app)GuardsSide effectsAPI operation
— → PLANNEDBack-office field.create (planner BO-21, editor BO-22); a recurring series creates one tour per dayDelegate 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_CONTACTadmin.tours.create
— → TO_STARTSame, when the date is todaySameSame; available at once in delegate.tours.todayadmin.tours.create
PLANNED → TO_STARTSYSTEM — daily job at 00:05 Africa/Algiers for the tours of the dayStatus PLANNEDStart button enabled on DA-05 (cahier §6.1 « disponible uniquement pour une tournée planifiée »); the app pre-loads the offline snapshotjob tours.activateToday
TO_START (or PLANNED dated today, when the 00:05 job is late) → IN_PROGRESSOnly 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 mapdelegate.tours.start
IN_PROGRESS → SUSPENDEDAssigned delegateReason 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 durationdelegate.tours.suspend
IN_PROGRESS → SUSPENDEDSYSTEM at 21:00 — proposal of the jobs page, so that sampling never runs overnight when a delegate forgets to closeStatus still IN_PROGRESSReason AUTO_END_OF_DAY; sampling stops; the delegate closes the tour when next online (within field.reopenMaxDays)job field.autoSuspendOpenTours
SUSPENDED → IN_PROGRESSAssigned delegateTour not closedSampling resumesdelegate.tours.resume
IN_PROGRESS, SUSPENDED → COMPLETEDAssigned 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 recomputeddelegate.tours.closingPreview · delegate.tours.close
COMPLETED → IN_PROGRESSBack-office field.validate (BO-23)Reason required; within field.reopenMaxDays (default 7) of the tour dateDelegate notified; the previous final report is kept as a version; finished visits stay locked unless unlocked one by oneadmin.tours.reopen
PLANNED, TO_START → CANCELLEDBack-office field.updateReason requiredVisits PLANNED → CANCELLED; delegate notifiedadmin.tours.cancel
IN_PROGRESS, SUSPENDED → CANCELLEDBack-office field.update (delegate sick, vehicle breakdown)Reason requiredVisits not started → CANCELLED; finished visits kept; sampling stopsadmin.tours.cancel

Rules specific to tours:

  1. A PLANNED or TO_START tour 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 through sync.delta and a push (cahier §6.2 « Une visite ajoutée ou déplacée doit être synchronisée »).
  2. In RECOMMENDED ordering mode the delegate may reorder the remaining visits (delegate.tours.reorder); in IMPOSED mode the call returns 409 TOUR_ORDER_IMPOSED.
  3. Not started (derived): a tour still TO_START after its date. Not closed (derived): a tour IN_PROGRESS or SUSPENDED after 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).

ValueUI label (fr)Meaning
PLANNEDÀ visiter
CHECKED_INArrivéArrival recorded (position + time).
IN_PROGRESSEn cours
DONEVisitéFinished; locked unless unlocked by permission.
NOT_DONENon réaliséeNot carried out — a reason is mandatory.
CANCELLEDAnnulée

'Late' (En retard) is derived: PLANNED and its time slot has passed.

From → ToTrigger · actor (app)GuardsSide effectsAPI operation
— → PLANNEDBack-office planning (BO) · delegate adds an unplanned visit (DA, client-generated id, offline)Reseller in scope, or a prospect just createdVIS- number (allocated when the unplanned visit reaches the server)admin.tours.setStops · delegate.tours.addVisit
PLANNED, NOT_DONE → CHECKED_INAssigned 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_PROGRESScheckedInAt (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_PROGRESSAssigned delegate (« Commencer la visite »)—startedAt; timer showndelegate.visits.start
IN_PROGRESS → DONEAssigned 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.missingendedAt, 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_DONEAssigned delegate (DA-11)Reason from VISIT_NOT_DONE_REASON; comment required for OTHERnotDoneAt, position when available; listed in the closing summarydelegate.visits.notDone
PLANNED → CANCELLEDBack-office removes the stop or cancels the tourVisit not startedKept in history, excluded from the visit rateadmin.tours.setStops · admin.tours.cancel
DONE → DONE (unlocked)Back-office field.validate (BO-25)Reason requiredunlockedUntil = 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; auditedadmin.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.

ValueUI label (fr)Meaning
OPENÀ faire
DONEFaite
CANCELLEDAnnulée

'Overdue' is derived (OPEN and dueAt in the past), never stored.

From → ToTrigger · actor (app)GuardsSide effectsAPI operation
— → OPENVisit report with a next action other than NONE (DA) · back-office crm.update · SYSTEM inactivity scanDue date required; assignee is the delegate of the reseller by defaultNextAction 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 → DONEAssignee (DA) · back-office crm.updateOutcome text requiredcompletedAt; Interaction on the timelinedelegate.tasks.complete · admin.tasks.update
OPEN → CANCELLEDBack-office crm.update · SYSTEM when the reseller becomes NOT_INTERESTED——admin.tasks.update
DONE → OPENBack-office crm.updateReason—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.

ValueUI label (fr)Meaning
AFTER_SALESService après-vente (SAV)Claim on items of an order — faulty, damaged, wrong or missing product.
ORDER_ISSUEProblème de commande
DELIVERY_ISSUEProblème de livraison
BILLINGFacturation
QUESTIONQuestion
OTHERAutre
ValueUI label (fr)Meaning
OPENOuverte
IN_PROGRESSEn cours de traitement
WAITING_RESELLEREn attente de votre réponse
RESOLVEDRésolueA resolution was applied (for SAV: see AfterSalesResolution).
REJECTEDRejetéeClaim refused with a reason (e.g. out of warranty, damage by misuse).
CLOSEDFermée
From → ToTrigger · actor (app)GuardsSide effectsAPI operation
— → OPENReseller (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 photoTKT-YYYY-NNNNNN; support queue notified (BO-55); claims flagged « SAV »tickets.create
OPEN → IN_PROGRESSsupport.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_RESELLERsupport.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_PROGRESSSYSTEM when the reseller repliesTicket not CLOSED; a rejected claim can be contested onceAssignee notifiedtickets.reply
IN_PROGRESS → RESOLVED (non-SAV)support.update—resolvedAt; push to the reselleradmin.tickets.update
IN_PROGRESS → RESOLVED (SAV)support.validate applies a resolution REPLACEMENT, RETURN, CREDIT_NOTE or REPAIRResolution 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 resolutionadmin.tickets.resolve
IN_PROGRESS → REJECTED (SAV)support.validate — resolution REJECTEDReason required (out of warranty, damage by misuse, outside the window…)Reseller notified with the reasonadmin.tickets.resolve
OPEN, IN_PROGRESS, RESOLVED, REJECTED → CLOSEDReseller (optional 1–5 rating)—closedAttickets.close
RESOLVED, REJECTED, WAITING_RESELLER → CLOSEDSYSTEM after support.autoCloseDays (default 7) without reseller replyReminder sent at half the delay—job support.autoClose
ValueUI label (fr)Meaning
DEFECTIVEProduit défectueux
DAMAGEDProduit endommagé
WRONG_ITEMMauvais article
MISSING_ITEMArticle manquant
NOT_AS_DESCRIBEDNon conforme
OTHERAutre
ValueUI label (fr)Meaning
REPLACEMENTRemplacementA free replacement order is created and goes through the normal order flow.
RETURNRetourA return request is created; reception re-enters stock and issues a credit note.
CREDIT_NOTEAvoirA credit note without physical return.
REPAIRRéparationItem repaired under warranty.
REJECTEDRefuséeClaim 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.

ValueUI label (fr)Meaning
SENTInvitation envoyée
APP_INSTALLEDApplication téléchargée
ACCOUNT_CREATEDCompte créé
PHONE_VERIFIEDTéléphone vérifié
CATALOG_VIEWEDCatalogue consulté
FIRST_ORDERPremière commande
StepEvent that sets itAPI operationOther effects
SENTThe delegate shares the invitation through any channel (QR displayed, WHATSAPP, SMS, LINK copied)delegate.invitations.create then delegate.invitations.markSharedShare channel recorded
APP_INSTALLEDFirst 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-upinvitations.attributeResellerStatus APP_INSTALLED; push to the delegate
ACCOUNT_CREATEDAccount created or claimed with this invitationauth.signup.resellerResellerStatus ACCOUNT_CREATED
PHONE_VERIFIEDThe OTP of that sign-up was verified — recorded together with ACCOUNT_CREATED, because an account cannot exist without itauth.otp.verify + auth.signup.reseller—
CATALOG_VIEWEDFirst successful catalog read by the new account (guest browsing before sign-up does not count)catalog.home · catalog.products.list · catalog.products.get—
FIRST_ORDERFirst order submitted (including an assisted order confirmed)cart.checkout · orders.confirmAssisted · admin.orders.confirmAssistedResellerStatus 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.

ValueUI label (fr)Meaning
QUEUEDEn file
RUNNINGEn cours
SUCCEEDEDTerminé
FAILEDÉchec
EXPIREDExpiré
From → ToTrigger · actor (app)GuardsSide effectsAPI operation
— → QUEUEDBack-office user with the export or import permissionFilters validated; the export only contains what the requester may see (scope and masked fields)AuditLog EXPORT or IMPORTadmin.exports.create · admin.resellers.export · admin.products.export · admin.imports.create · admin.products.import
QUEUED → RUNNING → SUCCEEDEDSYSTEM (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 runadmin.exports.get · admin.imports.get
RUNNING → QUEUEDSYSTEMTransient error; attempts below 3; exponential backoff——
RUNNING → FAILEDSYSTEMThird failure or invalid fileError summary and row-level report for importsadmin.imports.get
SUCCEEDED → EXPIREDSYSTEM dailyexports.retentionDays (default 7) passedFile deleted; the job row stays for auditjob 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.

ValueUI label (fr)Meaning
EARNGain
BONUSBonus
REDEEMÉchange
EXPIREExpiration
ADJUSTAjustement
REVERSALAnnulation
TypeSignCreated whenRule
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 jobPoints 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 cancelledCancels 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 tableRecordsKey columnsRead by
OrderStatusChangeEvery order transition, creation includedorderId, fromStatus (null at creation), toStatus, reasonCode, comment, actorUserId, actorApp, trigger (user or job/event key), occurredAt, createdAtReseller timeline in orders.get (status, date, public reason only); admin.orders.history
OrderChangeLine modifications and other non-status editsorderId, changeType, before/after diff, reason, actoradmin.orders.history
ResellerStatusChangeEvery reseller status changeresellerId, from, to, trigger (MANUAL, AUTOMATIC, IMPORT), eventKey, reason, actor, occurredAtReseller 360° timeline (mirrored as an Interaction STATUS_CHANGE), conversion reports
TourStatusChangeEvery tour status change, suspensions and reopenings includedtourId, from, to, reasonCode, comment, position (lat, lng, accuracy), occurredAt, receivedAt, actorTour live detail, active time, field reports
InvitationEventEach funnel step reachedinvitationId, step, occurredAt, inferred, attribution sourceDA-16, invitation funnel report
ResellerAssignment · CreditLimitChange · PriceChange · StockMovement · V2 LoyaltyTransactionAssignment, credit limit, price (the three prices), stock and points histories—Their module screens
AuditLogAccount status, validation decisions, customer type changes, product, promotion, return, invoice, payment, ticket and task status changes, after-sales resolutions, overrides, unlocks, permission changesaction (AuditAction), entityType, entityId, before/after, actor, app, IP, requestIdBO-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

JobSchedule (Africa/Algiers)Moves
tours.activateTodayDaily 00:05Tour PLANNED → TO_START; lists tours not started or not closed the day before
invoices.markOverdueDaily 01:00Invoice → OVERDUE (acts only on invoices that carry a due date)
crm.inactivityScanDaily 02:00Reseller ACTIVE, ACCOUNT_CREATED → TO_FOLLOW_UP; creates follow-up tasks
orders.expireAssistedEvery 15 minutesAssistedConfirmation → EXPIRED, order → CANCELLED; sends the 24-hour reminder
promotions.applyScheduleEvery minutePromotion SCHEDULED → ACTIVE, ACTIVE or PAUSED → ENDED
support.autoCloseDaily 03:00Ticket RESOLVED, REJECTED, WAITING_RESELLER → CLOSED
exports.expireDaily 04:45Job SUCCEEDED → EXPIRED
field.autoSuspendOpenTours (proposal)Daily 21:00Tour 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.

On this page