HOCO B2B
SRSRequirements

Authentication & accounts

Sign-in for staff, delegates and resellers, guest browsing, reseller sign-up with a declared customer type, account claim, one-time codes, sessions, account statuses, devices and app versions.

This area covers how each kind of user obtains an account and signs in, how a visitor browses the reseller app or web as a guest, how a shop owner signs up with a declared customer type — or activates the prospect record a délégué (delegate) already created — and how passwords, one-time codes (OTP), sessions, account statuses, devices and minimum app versions are governed (cahier §3, §5, §6, §6.5, §6.6, §9, §10, §11, §12, and the team decisions of 17 Oct 2026). It concerns the four clients: back-office (BO), reseller app (RA), reseller web (RW) and delegate app (DA). The server's identity module owns it, built on Better Auth (User, Session, Account, Verification) with HOCO's rules on top; SMS goes through the notifications module, invitation codes are resolved by invitations, and the customer type lives on the CRM record (Reseller.customerType). The operations are specified on API › Authentication & sessions; the account statuses on Lifecycles › AccountStatus.

Three user types exist (UserType): STAFF (back-office only), DELEGATE (delegate app only) and RESELLER (reseller app and web). An account never crosses types. A fourth kind of visitor has no account at all: the guest, who browses the reseller app or web without prices (AUTH-10).

IDRequirementReleasePrioritySourceApps
AUTH-01Back-office staff sign-in (email or phone + password)V1MUST§3, §10BO
AUTH-02Delegate sign-in with phone + password; suspended accounts blocked with an explicit messageV1MUST§6DA
AUTH-03Reseller self sign-up: phone verified by OTP, store details, declared customer type (super-gros, demi-gros, détail), password, optional invitation codeV1MUST§5, §6.6, §12RA · RW
AUTH-04Reseller sign-in (phone + password) and password reset by OTPV1MUST§5, §11RA · RW
AUTH-05Account claim: a prospect created in the field is activated by the reseller without creating a duplicateV1MUST§4.2, §6.5, §6.6RA · RW · API
AUTH-06Sessions: expiry, multi-device, listing and revocation, sign-outV1MUST§10BO · RA · RW · DA
AUTH-07Password policy, hashing, OTP expiry/resend cooldown and attempt limitingV1MUST§10API
AUTH-08Account statuses (pending validation, active, suspended, disabled) gate what each user can doV1MUST§6, §10API · BO · RA · RW · DA
AUTH-09Device registration for push and minimum-app-version enforcementV1MUST§9, §11RA · DA · API
AUTH-10Guest mode: the reseller app and web can be browsed without an account; prices stay hidden until the visitor signs up and declares a customer typeV1MUSTTeam decision (17 Oct 2026)RA · RW · API
AUTH-11Customer type declared at sign-up, reviewed at account validation; any later change is made by the back-office and auditedV1MUSTTeam decision (17 Oct 2026)RA · RW · BO · API

Settings used in this area

Setting or constantDefaultMeaning
catalog.pricesVisibleBeforeValidation (setting)trueA reseller whose account is PENDING_VALIDATION sees the prices of their declared customer type; ordering still waits for validation (D-18).
signup.customerTypes (setting)all threeCustomer types offered on the sign-up form (RA-05, RW-01).
legal.termsVersion (setting)—Version of the terms the reseller must accept at sign-up; a new version triggers re-acceptance.
OTP (constant)6 digits · 5 min · 5 attemptsCode length, validity and wrong attempts allowed per code.
OTP sending (constant)1 per 60 s · 5 per hour per phone · 20 per hour per IPResend cooldown and hourly caps.
Verification token (constant)10 min, single useProof of a verified phone, bound to the destination and the OTP purpose.
Sign-in throttling (constant)5 per minute per identifier + IP · 15-min lock after 5 consecutive failuresBrute-force protection.
Session lifetime (constant)30 days reseller and delegate · 7 days back-office, slidingExpiry without activity.
Fresh-session window (constant)15 minBack-office actions marked sensitive need a sign-in this recent (D-33).

Requirements

AUTH-01 — Back-office staff sign-in

ReleasePrioritySourceAppsOwner tasks
V1MUSTcahier §3, §10BOSRV-05 · DSH-01

Statement — The system shall let back-office staff sign in to the dashboard with their email address or phone number and their password, and shall open a dashboard session only for STAFF users whose account allows it.

Rules

  1. One identifier field accepts an email (trimmed, lower-cased, looked up among STAFF users only) or a phone in any common format, normalised to E.164 (CRM-03); the value is an email when it contains @.
  2. The request declares the app (app: "DASHBOARD"). The password is checked first: an unknown identifier and a wrong password both answer 401 INVALID_CREDENTIALS with the same message (« Numéro ou mot de passe incorrect. »), and the server runs a dummy hash comparison for unknown identifiers so response times reveal nothing.
  3. Only after a correct password: a user type other than STAFF → 403 WRONG_APP; SUSPENDED or DISABLED → 403 ACCOUNT_SUSPENDED with details.status; ACTIVE → session.
  4. Throttling and lockout follow AUTH-07 R7.
  5. Accounts created by an administrator (ADM-01) carry a temporary password and mustChangePassword = true: until the password is changed, every operation except auth.password.change, auth.logout, auth.session, me.get and appConfig.get answers 403 PASSWORD_CHANGE_REQUIRED. Staff passwords are at least 10 characters.
  6. The session is the httpOnly cookie hoco.session_token on .hoco.dz (Secure, SameSite=Lax), valid 7 days and sliding (AUTH-06); cookie-authenticated mutations are accepted only from the allowed origins.
  7. Sensitive actions — role and permission changes, password resets of other users, user status changes, exports of personal data — need a session younger than 15 minutes; otherwise 403 REAUTH_REQUIRED and the dashboard asks for the password again (a new auth.login replaces the session).
  8. Every success writes AuditLog LOGIN (app DASHBOARD, IP, user agent) and updates lastLoginAt; every failure writes LOGIN_FAILED with the reason (INVALID_PASSWORD, UNKNOWN_IDENTIFIER, WRONG_APP, SUSPENDED, LOCKED), never the password. A sign-in from a browser not seen for this user in 90 days sends the security notification security.new_device by email.
  9. The sign-in page (BO-01) offers « Mot de passe oublié ? » and no self-registration: staff accounts exist only through ADM-01.

Acceptance criteria

  • Given an active staff user nadia@hoco.dz, when she signs in with NADIA@hoco.dz and the right password, then the cookie is set and the response carries her roles and permissions in user.
  • Given her phone +213555123456, signing in with 0555 12 34 56 also succeeds.
  • A wrong password and an unknown email return identical 401 INVALID_CREDENTIALS bodies.
  • A delegate's correct phone and password with app: "DASHBOARD" return 403 WRONG_APP and no Set-Cookie.
  • A suspended staff user gets 403 ACCOUNT_SUSPENDED with the right password and 401 INVALID_CREDENTIALS with a wrong one.
  • After 5 consecutive failures from one IP, a 6th attempt within 15 minutes returns 429 TOO_MANY_ATTEMPTS with Retry-After, even with the right password; the same account from another IP is not locked.
  • With a temporary password, admin.orders.list returns 403 PASSWORD_CHANGE_REQUIRED until auth.password.change succeeds.
  • A permission change attempted 20 minutes after sign-in returns 403 REAUTH_REQUIRED; after signing in again it succeeds.

Data — User (type STAFF, email, phone, status, mustChangePassword, lastLoginAt), Account (password hash), Session, UserRole, AuditLog.

Interfaces

Notes / open points — Session length, the list of sensitive actions and two-factor authentication for super administrators are D-33.

AUTH-02 — Delegate sign-in and suspended accounts

ReleasePrioritySourceAppsOwner tasks
V1MUSTcahier §6DASRV-05 · MOB-06 · DES-11

Statement — The system shall let a delegate sign in to the delegate app with their phone number and password, and shall refuse a suspended or disabled delegate with an explicit message.

Rules

  1. Delegate accounts are created only from the back-office (ADM-01, FLD-01); the delegate app has no sign-up. The temporary password is sent by SMS and must be changed at first sign-in (AUTH-01 R5).
  2. Identifier: the phone, normalised to E.164; app: "DELEGATE". Password first, then 403 WRONG_APP for another user type, then the status (AUTH-01 R2–R3).
  3. ACTIVE → bearer token (30 days, sliding) stored in secure storage. SUSPENDED → 403 ACCOUNT_SUSPENDED and DA-02 shows « Votre compte est suspendu. Contactez votre responsable. » with the support contacts of appConfig.get; DISABLED → the same code with details.status = "DISABLED" and « Ce compte est désactivé. ». Nothing is stored in either case.
  4. A delegate suspended while signed in loses every session at once; their next call — including sync.batch — answers 403 ACCOUNT_SUSPENDED, so the app shows the suspension message rather than "session expired".
  5. A new sign-in on the same device (X-Device-Id) and app replaces the previous session of that device.
  6. The app refuses to sign out while its offline queue holds unsent actions and opens the sync centre instead (DA-27, SYN-05): signing out would lose them.
  7. After sign-in the app reads me.get (profile, zones, permissions), registers the device (AUTH-09) and only then asks for location consent (DA-04, SEC-07).
  8. Throttling and audit as AUTH-01 R4 and R8 (app DELEGATE_APP).

Acceptance criteria

  • Given active delegate Karim (+213661234567), signing in with 0661 23 45 67 returns a token and user.delegate.zones.
  • Given a suspended delegate and the right password, the API returns 403 ACCOUNT_SUSPENDED, DA-02 shows the suspension message, and secure storage stays empty.
  • A reseller's credentials in the delegate app return 403 WRONG_APP with details.userType = "RESELLER".
  • Given Karim signed in, when an administrator suspends him, his next call returns 403 ACCOUNT_SUSPENDED and the app returns to DA-02 with the message.
  • Given 3 actions queued offline, « Se déconnecter » opens the sync centre and the session stays valid.
  • Signing in again on the same phone revokes the previous session of that phone.

Data — User (type DELEGATE), DelegateProfile, Session, Device, AuditLog.

Interfaces

AUTH-03 — Reseller self sign-up with OTP and customer type

ReleasePrioritySourceAppsOwner tasks
V1MUSTcahier §5, §6.6, §12RA · RWSRV-05 · WEB-01 · MOB-06 · DES-06

Statement — The system shall let a shop owner create a reseller account from the reseller app or web by verifying their phone with a one-time code, entering their store details, declaring their customer type (Super-gros, Demi-gros or Détail) and a password, and optionally an invitation code.

Rules

  1. Steps: phone and optional invitation code (RA-03) → auth.otp.send purpose SIGNUP → code (RA-04) and auth.otp.verify → store details, customer type and password (RA-05) → auth.signup.reseller. The web runs the same steps on RW-01. Guests reach them from every price lock and cart button (AUTH-10).
  2. Phones must be Algerian mobiles: after normalisation +213 followed by 9 digits starting with 5, 6 or 7 (field code INVALID_PHONE).
  3. auth.otp.send for SIGNUP refuses a phone that already has a user account with 409 PHONE_ALREADY_EXISTS (details.canSignIn: true — the app offers « Se connecter »). A phone that matches a point of sale without an account is accepted: the claim happens at sign-up (AUTH-05).
  4. A verified code returns a verification token (10 minutes, single use) and, when the phone matches a claimable point of sale, the record's details to pre-fill RA-05 — only after the code is verified.
  5. Required at sign-up: owner name (2–80), store name (2–120), business type (BUSINESS_TYPE list), wilaya and commune (the commune must belong to the wilaya — GEO-01), address line (5–200), customer type (one of signup.customerTypes, with the description of each type shown under the choice — AUTH-11), password (AUTH-07) and acceptance of the current terms (legal.termsVersion; field code TERMS_NOT_ACCEPTED). Optional: landmark, GPS position (with permission), WhatsApp (« Même numéro » switch), email, invitation code.
  6. Invitation code: 8 characters, case-insensitive, with or without the dash (ab12-cd34 = AB12CD34), pre-filled from the link https://app.hoco.dz/i/AB12CD34 or the install referrer (IVT-04). An unknown code answers 404 INVITATION_NOT_FOUND and the app offers « Continuer sans code »; an expired or replaced one 410 INVITATION_EXPIRED.
  7. Record resolution, in one transaction: (a) a point of sale has this main phone and no account → claim (AUTH-05); (b) no phone match but a valid invitation whose point of sale has no account → claim of the invited record; (c) otherwise a new Reseller (source SELF_SIGNUP, number REV-…, status ACCOUNT_CREATED, validation PENDING) with its store address as default delivery address. The User (type RESELLER, role RESELLER, phone verified, status PENDING_VALIDATION) is created in the same transaction.
  8. Assignment of a new record: to the attributed invitation's delegate; without invitation it stays unassigned and the validator assigns it (CRM-05).
  9. The response carries a session (token for the app, cookie for the web): the owner lands on the catalog with the prices of the declared type (while catalog.pricesVisibleBeforeValidation is true) and on the pending state of RA-06 for ordering.
  10. Side effects: invitation steps ACCOUNT_CREATED and PHONE_VERIFIED on the attributed invitation and the SIGNUPS objective credit to its delegate; account.pending_validation to users holding crm.validate (in-app and back-office live event) and to the assigned delegate (push); AuditLog CREATE; CRM timeline STATUS_CHANGE. At most 10 sign-ups per hour per IP (429 RATE_LIMITED).
  11. A retry after a lost response fails with 422 VERIFICATION_TOKEN_INVALID (USED) or 409 PHONE_ALREADY_EXISTS; the app then tries auth.login with the phone and password it just sent before showing an error.

Acceptance criteria

  • Given a phone unknown to HOCO, sending a code, verifying it and submitting valid details with customerType: "DEMI_GROS" returns 201 with a session; the account is PENDING_VALIDATION, the reseller ACCOUNT_CREATED, source SELF_SIGNUP, customerType DEMI_GROS, and it appears in BO-13 within 2 seconds.
  • Right after sign-up, catalog.products.list returns the Demi-gros prices and cart.checkout returns 403 ACCOUNT_PENDING_VALIDATION.
  • auth.otp.send with the phone of an existing account returns 409 PHONE_ALREADY_EXISTS with canSignIn: true.
  • Omitting customerType returns 400 VALIDATION_FAILED; a commune of another wilaya returns field code COMMUNE_NOT_IN_WILAYA; terms not accepted return TERMS_NOT_ACCEPTED.
  • An expired or reused verification token returns 422 VERIFICATION_TOKEN_INVALID.
  • With code ab12-cd34 from Karim's invitation, the new reseller is assigned to Karim and the invitation shows Compte créé and Téléphone vérifié; an unknown code returns 404 INVITATION_NOT_FOUND.
  • An eleventh sign-up from the same IP within an hour returns 429 RATE_LIMITED.

Data — User, Account, Session, Verification, Reseller (customerType, source, validationStatus), ResellerAddress, ResellerStatusChange, ResellerAssignment, Invitation, InvitationEvent, AuditLog, OutboxEvent.

Interfaces

  • Screens: RA-02 Welcome (sign in, sign up, browse as guest) · RA-03 Sign-up — phone & invitation code · RA-04 OTP verification · RA-05 Sign-up — store details, customer type & password · RW-01 Sign in, sign-up (with customer type), OTP, password reset — and guest browsing
  • API: auth.otp.send POST /auth/otp/send · auth.otp.verify POST /auth/otp/verify · auth.signup.reseller POST /auth/signup/reseller

Notes / open points — Whether anyone may sign up or only invited shops is D-28 (default: open sign-up). Whether prices of the declared type are shown before validation is D-18.

AUTH-04 — Reseller sign-in and password reset

ReleasePrioritySourceAppsOwner tasks
V1MUSTcahier §5, §11RA · RWSRV-05 · WEB-01 · MOB-06 · DES-06

Statement — The system shall let a reseller sign in to the app or the web with their phone number and password, and reset a forgotten password with a one-time code sent by SMS.

Rules

  1. Sign-in uses the phone in any common format and the password with app: "RESELLER"; the server records the client as RESELLER_APP (mobile headers present) or RESELLER_WEB. Password first, then 403 WRONG_APP, then the status (AUTH-01 R2–R3). Throttling and audit as AUTH-01.
  2. ACTIVE and PENDING_VALIDATION sign in (pending users get the access of AUTH-08); SUSPENDED and DISABLED get 403 ACCOUNT_SUSPENDED with the support contacts.
  3. One account serves both clients (RWA-02); sessions are independent — signing out on the web keeps the phone signed in. After a mobile sign-in the app registers its device and opens its live stream events.stream (ORD-15).
  4. Reset (RA-08, RW-01): auth.otp.send purpose PASSWORD_RESET answers the same 202 whether or not an account exists — and whether it is suspended — but sends an SMS only to an existing, non-suspended account, so the operation reveals nothing.
  5. auth.otp.verify returns a verification token; auth.password.reset with the token and a new password answers 204. The new password follows AUTH-07 and must differ from the current one (field code PASSWORD_REUSED).
  6. A reset revokes every session of the user and clears the sign-in lock for that phone; the app then signs in with the new password. A security notification (« Votre mot de passe HOCO vient d'être modifié ») is sent and cannot be disabled.
  7. Signing out of the reseller app returns it to guest browsing (AUTH-10) and keeps the catalog cache.
  8. The delegate app (DA-03) and the back-office (BO-02) use the same reset operations; staff who type an email receive the code by email.

Acceptance criteria

  • Given an active reseller, signing in on the app and the web creates two sessions; signing out on the web leaves the app session valid.
  • A wrong password and an unknown phone return the same 401 INVALID_CREDENTIALS.
  • A reset requested for an unknown phone returns 202 and the SMS gateway log shows no message.
  • With the correct code, reset returns 204; the old password fails; every previous session returns 401 UNAUTHENTICATED; the security notification is sent.
  • Five wrong codes destroy the code: the fifth answer is 429 TOO_MANY_ATTEMPTS.
  • Reusing a verification token returns 422 VERIFICATION_TOKEN_INVALID; reusing the current password returns field code PASSWORD_REUSED.
  • A pending reseller signs in; cart.checkout then returns 403 ACCOUNT_PENDING_VALIDATION.

Data — User, Account, Session, Verification, Notification, AuditLog.

Interfaces

  • Screens: RA-02 Welcome (sign in, sign up, browse as guest) · RA-04 OTP verification · RA-07 Sign in · RA-08 Forgot password · RW-01 Sign in, sign-up (with customer type), OTP, password reset — and guest browsing
  • API: auth.login POST /auth/login · auth.otp.send POST /auth/otp/send · auth.password.reset POST /auth/password/reset

AUTH-05 — Account claim without duplicate

ReleasePrioritySourceAppsOwner tasks
V1MUSTcahier §4.2, §6.5, §6.6RA · RW · APISRV-05 · WEB-01 · MOB-06 · DES-06

Statement — The system shall let a shop owner whose point of sale already exists in the CRM — created by a delegate, the back-office or an import — activate it by verifying the record's main phone (or by using its invitation), linking a new user account to the existing Reseller instead of creating a second record, and keeping the attribution to the delegates who created and invited it.

Rules

  1. Eligibility. A claim applies to a Reseller without a linked user, whatever its status or source (a NEW_PROSPECT from the field, a NOT_INTERESTED record, an ACTIVE customer imported at go-live).
  2. Two ways to prove the link. (a) Phone match — the verified phone equals the record's normalised main phone; (b) invitation — no record has the verified phone, and a valid invitation code (8 characters, delivered by the delegate in person or by link) points to a record without an account: the record's main phone is replaced by the verified phone and the old number is kept in the history and shown to the validator. A code whose point of sale already has an account answers 409 INVITATION_ALREADY_USED.
  3. Flow. auth.otp.send (SIGNUP) → auth.otp.verify returns signup.claimable = true and the record's details (store and owner names, business type, customer type if the delegate recorded one, wilaya, commune, address, the delegate's first name) — never before the code is verified → RA-05 shows « Nous avons trouvé votre magasin, enregistré par Yacine. Vérifiez vos informations. » → the owner corrects, chooses the customer type and a password → auth.signup.reseller.
  4. No duplicate. In one transaction the server locks the matching Reseller row, checks that no user is linked (otherwise 409 PHONE_ALREADY_EXISTS), creates the User and sets the link. No second Reseller is ever created for the phone; the unique index on the normalised main phone is the final guarantee under concurrency (CRM-03).
  5. Who owns which field. The submitted owner name, store name, business type, wilaya, commune, address line and landmark replace the record's values; GPS position and photos captured in the field are kept (a submitted position only fills an empty one); source, the creating delegate, the assigned delegate, segments, notes and history are unchanged. Every replaced value is written to AuditLog (UPDATE, actor = the new user, context signup.claim) so the validator sees what changed.
  6. Customer type. The declared type is stored as declared; when the delegate had recorded another value, the difference is kept in the audit entry and highlighted in the validation queue (AUTH-11). A back-office-created or imported record keeps the type the back-office set.
  7. Attribution kept. The creating delegate (createdByDelegateId) and the current assignment do not change. The sign-up is attributed, in this order, to the explicit invitation code, then the device attribution recorded at first app open (invitations.attribute), then the latest open invitation of the claimed record (IVT-04); that invitation records ACCOUNT_CREATED and PHONE_VERIFIED and its delegate gets the SIGNUPS credit.
  8. Lifecycle. The reseller moves to ACCOUNT_CREATED unless it is already further (FIRST_ORDER, ACTIVE); SUSPENDED stays SUSPENDED (Lifecycles › Reseller).
  9. Account status. A record already APPROVED (a point of sale validated by the back-office, an imported customer) gets an ACTIVE account at once and no validation is requested; otherwise the account is PENDING_VALIDATION and the record enters the validation queue as kind CLAIM (CRM-09).
  10. Notifications. The creating and assigned delegates receive « Le revendeur Magasin El Wafa a activé son compte » (push and in-app); validators receive account.pending_validation when validation is needed.

Acceptance criteria

  • Given prospect REV-000123 created by delegate Yacine with phone +213555123456 and no user, when the owner signs up with 0555 12 34 56, then verification returns claimable: true with the record's details, and after sign-up exactly one reseller has that phone — REV-000123 — with a linked user, the creating delegate still Yacine, the same assignment and status ACCOUNT_CREATED; the response has claimed: true.
  • A failed auth.otp.verify (422 OTP_INVALID) returns no field of the record.
  • Two devices completing the claim at the same time: exactly one gets 201, the other 409 PHONE_ALREADY_EXISTS.
  • Given a record whose phone was mistyped by the delegate, the owner signing up with the correct phone and the record's invitation code claims that record; its main phone becomes the verified phone and the old one appears in the history.
  • The owner's new address line replaces the record's; the delegate's GPS position and façade photo are unchanged; the audit entry lists each replaced value.
  • A prospect already APPROVED gets an ACTIVE account; a prospect still PENDING appears in BO-13 with kind CLAIM.
  • An imported ACTIVE customer who claims stays ACTIVE and can order at once with the customer type set at import.
  • Yacine receives the push « … a activé son compte » within 1 minute.

Data — Reseller, User, Account, Verification, Session, Invitation, InvitationEvent, ResellerStatusChange, ResellerAssignment (read only), AuditLog, OutboxEvent.

Interfaces

  • Screens: RA-03 Sign-up — phone & invitation code · RA-05 Sign-up — store details, customer type & password · RW-01 Sign in, sign-up (with customer type), OTP, password reset — and guest browsing
  • API: auth.signup.reseller POST /auth/signup/reseller

AUTH-06 — Sessions, devices and sign-out

ReleasePrioritySourceAppsOwner tasks
V1MUSTcahier §10BO · RA · RW · DASRV-05 · MOB-02

Statement — The system shall issue expiring sessions per device and client, let every user list and revoke their own sessions, let administrators revoke another user's sessions, and sign users out cleanly.

Rules

  1. Lifetime: 30 days for reseller and delegate sessions, 7 days for back-office sessions, sliding — an authenticated request extends the expiry by the full lifetime, at most once per 24 hours. No absolute maximum in V1.
  2. Transport: a request carrying X-App-Platform is mobile — the token is returned in the body (hs_…, kept in secure storage, sent as Authorization: Bearer); otherwise the API sets the httpOnly cookie hoco.session_token and never returns the token. Tokens are opaque; the database stores only their hash.
  3. One session per device and app: a new sign-in from the same X-Device-Id for the same app revokes the previous one. One browser holds one HOCO session; each web app ignores a session of another client and offers to sign out.
  4. Each session records the client (ClientApp), device id, platform, app version, device name, user agent, IP, createdAt and lastActiveAt (refreshed at most every 5 minutes).
  5. auth.sessions.list returns the caller's active sessions with a current flag; auth.sessions.revoke revokes one of them — an unknown, already revoked or foreign session id answers 404 NOT_FOUND; revoking the current one is a sign-out. Resellers see them under Paramètres (RA-35), staff under Mon profil (BO-68).
  6. auth.logout revokes the current session, detaches the device bound to it and deletes its push token, and closes its events.stream connection; it answers 204 even for an expired token so clients can always clear their state.
  7. Forced revocation: password change (other sessions), password reset, suspension and disabling (all sessions). Administrators holding users.administer list and revoke any user's sessions with admin.users.sessions; each revocation writes AuditLog LOGOUT with the administrator as actor.
  8. An expired or revoked token answers 401 UNAUTHENTICATED with details.reason (MISSING, EXPIRED, REVOKED); the reseller app drops it and continues as a guest, the delegate app returns to sign-in and keeps its offline queue for the same delegate.

Acceptance criteria

  • A reseller signed in on phone A, phone B and the web sees 3 sessions in auth.sessions.list, with current on the caller's.
  • When phone A revokes phone B's session, B's next request returns 401 UNAUTHENTICATED with reason: "REVOKED".
  • Revoking another user's session id returns 404 NOT_FOUND.
  • A session unused for 31 days returns 401; one used daily for 60 days is still valid.
  • After auth.logout on a phone, a push sent to that user is not delivered to that phone.
  • When an administrator revokes all of delegate Karim's sessions, his next call returns 401 and the activity log shows LOGOUT by that administrator.

Data — Session, Device, User, AuditLog.

Interfaces

AUTH-07 — Password policy, hashing, OTP and attempt limits

ReleasePrioritySourceAppsOwner tasks
V1MUSTcahier §10APISRV-05 · MOB-06

Statement — The system shall enforce a password policy, store passwords only as salted hashes, and protect one-time codes and sign-in with expiry, resend cooldown and attempt limits.

Rules

  1. Passwords: 8 to 128 characters (10 minimum for staff), at least one letter and one digit, not containing the last 6 digits of the user's phone, not in the server's list of the 10 000 most common passwords; no forced periodic change. A violation returns 400 VALIDATION_FAILED with field code PASSWORD_TOO_WEAK and details.minLength; the same password as the current one returns PASSWORD_REUSED.
  2. Hashing: Better Auth's scrypt with a random salt per password. Passwords, codes, verification tokens and session tokens are never logged, returned or stored in clear; auth.* request bodies are excluded from request logs and idempotency records.
  3. One-time codes: 6 digits from a cryptographic random source, stored hashed in Verification with destination, purpose (OtpPurpose: SIGNUP, PASSWORD_RESET, PHONE_CHANGE) and expiry; valid 5 minutes; 5 attempts; one pending code per destination and purpose (a new code invalidates the previous one); constant-time comparison.
  4. Sending limits: 1 code per 60 seconds per destination and purpose (429 OTP_RESEND_TOO_SOON with details.retryAfterSeconds), 5 per hour per phone and 20 per hour per IP (429 RATE_LIMITED). A gateway failure answers 503 OTP_DELIVERY_FAILED and is not counted. RA-04 shows the cooldown (« Renvoyer le code dans 0:42 »).
  5. A wrong code returns 422 OTP_INVALID with details.attemptsRemaining; the fifth wrong attempt destroys the code (429 TOO_MANY_ATTEMPTS); an expired, replaced or used code returns 422 OTP_EXPIRED.
  6. A verified code yields a verification token bound to the destination and purpose, valid 10 minutes, consumed once by the operation of its purpose only (422 VERIFICATION_TOKEN_INVALID with details.reason: EXPIRED, USED, PURPOSE_MISMATCH).
  7. Sign-in: more than 5 attempts per minute per identifier and IP → 429 RATE_LIMITED; 5 consecutive failures lock that identifier and IP for 15 minutes (429 TOO_MANY_ATTEMPTS, details.lockedUntil); a success or a password reset clears the counter. Counters live in Redis.
  8. SMS text: « HOCO : votre code est 482913. Il expire dans 5 minutes. Ne le communiquez à personne. », followed on Android by the app hash used by the SMS Retriever so the app fills the code by itself; no link. Every send is logged as a NotificationDelivery for cost follow-up (INT-07).
  9. auth.password.change requires the current password (422 INVALID_CURRENT_PASSWORD, blocked 15 minutes after 5 wrong attempts) and revokes the user's other sessions.
  10. Non-production environments send no SMS: the code is written to the server log and Mailpit (staging test numbers may use a fixed code).

Acceptance criteria

  • azerty12 is accepted for a reseller and refused for staff (PASSWORD_TOO_WEAK, minLength: 10); 12345678 and password1 are refused; a password containing 123456 for the phone +213555123456 is refused.
  • The Account row holds a scrypt hash; no log line contains the submitted password or code.
  • Two auth.otp.send calls 30 seconds apart: the second returns 429 OTP_RESEND_TOO_SOON with retryAfterSeconds near 30; a sixth code within an hour returns 429 RATE_LIMITED.
  • A wrong code returns 422 OTP_INVALID with attemptsRemaining: 4; after the fifth wrong code the right code is refused.
  • A code verified after 5 minutes returns 422 OTP_EXPIRED; a PASSWORD_RESET token sent to auth.signup.reseller returns 422 VERIFICATION_TOKEN_INVALID with PURPOSE_MISMATCH.
  • After auth.password.change, the other sessions return 401; the current one keeps working.

Data — Account, Verification, Session, NotificationDelivery, AuditLog; throttling counters in Redis.

Interfaces

AUTH-08 — Account statuses gate what users can do

ReleasePrioritySourceAppsOwner tasks
V1MUSTcahier §6, §10API · BO · RA · RW · DASRV-05 · MOB-06

Statement — The system shall give every user one account status and enforce, on every API call, what that status allows.

Rules

  1. Statuses (AccountStatus): PENDING_VALIDATION (resellers only), ACTIVE, SUSPENDED, DISABLED. Guests have no account and follow AUTH-10.

  2. What each status allows (reseller columns apply to reseller users; staff and delegates are ACTIVE, SUSPENDED or DISABLED):

    StatusSign-inCatalogPricesCartCheckout, assisted-order confirmation, payment declaration, return requestProfile, documents, tickets, notifications
    PENDING_VALIDATIONYesYesOf the declared type if catalog.pricesVisibleBeforeValidation, else hidden as for a guestYesNo — 403 ACCOUNT_PENDING_VALIDATIONYes
    ACTIVEYesYesOf the customer typeYesYesYes
    SUSPENDEDNoNoNoNoNoNo
    DISABLEDNoNoNoNoNoNo

    Every call from a SUSPENDED or DISABLED user answers 403 ACCOUNT_SUSPENDED (details.status tells which).

  3. The account status (access) is distinct from the commercial status of the store (CRM-04): a store commercially SUSPENDED keeps an ACTIVE account — the owner signs in, reads orders and invoices and pays — but checkout, assisted orders and return requests answer 403 RESELLER_SUSPENDED.

  4. Transitions (Lifecycles › AccountStatus): PENDING_VALIDATION → ACTIVE at approval (CRM-09); PENDING_VALIDATION → DISABLED at a rejection with disableAccount (duplicate, fraud); ACTIVE ↔ SUSPENDED; ACTIVE or SUSPENDED → DISABLED; DISABLED → ACTIVE (reactivation). A reason is required except for approval. Staff and delegates change through admin.users.setStatus (users.update; reactivating a disabled account needs users.administer); reseller logins through the same operation from the reseller 360° page by a user holding crm.administer.

  5. Leaving ACTIVE or PENDING_VALIDATION revokes every session and closes live streams; the user is told by SMS (resellers, delegates) or email (staff); AuditLog STATUS_CHANGE records the actor and the reason.

  6. History is never deleted (SEC-06): a disabled user's orders, visits and audit entries stay attributed to them; they disappear from assignment pickers.

  7. A disabled user's phone stays reserved until an administrator holding users.administer releases it (the number moves to the user's history), so a company SIM can be given to a new delegate.

  8. The status is checked right after authentication and before permissions (ACL-05), from a cache of at most 60 seconds invalidated on change.

  9. me.get returns status and, for resellers, reseller.validationStatus and rejectionReason, which drive RA-06 (pending and rejected states).

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.

Acceptance criteria

  • A pending reseller lists products with Demi-gros prices, adds to the cart and uploads an RC document; cart.checkout returns 403 ACCOUNT_PENDING_VALIDATION.
  • With catalog.pricesVisibleBeforeValidation = false, the same reseller gets price: null and priceLocked: true like a guest.
  • When an administrator suspends a reseller login, the reseller's next call returns 403 ACCOUNT_SUSPENDED, an SMS is sent and the activity log records the reason.
  • A commercially suspended store with an active login can open its invoices and gets 403 RESELLER_SUSPENDED at checkout.
  • Disabling delegate Karim keeps his visits attributed to him and removes him from the delegate picker of BO-12.
  • Suspending without a reason returns 422 REASON_REQUIRED.

Data — User (status, reason), Reseller (validationStatus, status), Session, Notification, AuditLog.

Interfaces

AUTH-09 — Device registration and minimum app version

ReleasePrioritySourceAppsOwner tasks
V1MUSTcahier §9, §11RA · DA · APISRV-05 · MOB-05 · MOB-08

Statement — The system shall register each mobile installation for push notifications and refuse calls from app versions below the configured minimum, telling the user how to update.

Rules

  1. After sign-in and at every app start, the app calls me.devices.register with the device id (X-Device-Id, a UUID generated at first launch and kept in secure storage), platform, app, app version, OS version, model, locale and FCM token. The operation upserts on the device id and is idempotent.
  2. A device belongs to one user at a time: registering a device id bound to another user moves it to the caller, so the previous user's notifications stop on that phone.
  3. A refreshed FCM token re-registers the device; tokens reported invalid by FCM are deleted by the notification worker; devices not seen for 90 days are pruned.
  4. The version policy (AppVersionPolicy per app and platform: minimum and latest version, maintenance flag and message) is edited in BO-66 (ADM-07) and read without a session through appConfig.get at the splash screen (RA-01, DA-01) — guests included.
  5. Mobile requests send X-App-Version and X-App-Platform. Below the minimum, the API answers 426 UPGRADE_REQUIRED with details.storeUrl and details.minVersion on every operation except appConfig.get, auth.logout and — for the delegate app, during the 72 hours after the minimum was raised — sync.batch, so field work recorded offline is never stranded by a forced update.
  6. At or above the minimum but below the latest version, the app shows a dismissible « Une mise à jour est disponible », at most once a day per version.
  7. Maintenance on: every operation except appConfig.get answers 503 MAINTENANCE with details.until (never for the back-office); the apps show their maintenance state (RA-36, DA-28).
  8. Versions compare semantically (1.10.0 above 1.9.3); a mobile session without X-App-Version counts as below the minimum.
  9. me.devices.remove and sign-out unregister the device's push token.

Acceptance criteria

  • Registering twice with the same device id and a new FCM token leaves one Device row with the new token.
  • When reseller B signs in on reseller A's former phone, a push for A is not delivered to it.
  • With minimum 1.3.0, a call from 1.2.9 returns 426 UPGRADE_REQUIRED with the store URL; appConfig.get still answers 200.
  • One hour after the minimum was raised, a delegate app in 1.2.9 can still replay its queue through sync.batch; 73 hours later the same call returns 426.
  • With maintenance until 14:00, catalog.products.list returns 503 MAINTENANCE with details.until.

Data — Device, AppVersionPolicy, Session, NotificationDelivery.

Interfaces

AUTH-10 — Guest mode without prices

ReleasePrioritySourceAppsOwner tasks
V1MUSTTeam decision (17 Oct 2026)RA · RW · APISRV-05 · SRV-11 · WEB-01 · WEB-02 · MOB-06 · MRA-01 · MRA-02 · MRA-03

Statement — The system shall let anyone browse the reseller app and web without an account — catalog, product pages, promotions, banners and the WhatsApp support button — while hiding every price and refusing any cart or order action until the visitor signs up or signs in.

Rules

  1. A guest has no session and no token: the apps call the operations marked optional — catalog.* (except favorites), promotions.list, promotions.get, banners.list, support.whatsapp, appConfig.get and reference.* — without Authorization. The welcome screen (RA-02) offers « Se connecter », « Créer un compte » and « Découvrir le catalogue ».
  2. Prices hidden everywhere. For a guest every price field is null and priceLocked: true — unit prices, promotion prices, recommended prices included; the apps show the lock « Créez un compte pour voir les prix » (RA-41). Price filters and price sorts are refused (400 VALIDATION_FAILED, field code SIGN_IN_REQUIRED); the facets carry no price range (CAT-12).
  3. Visibility. A guest sees only products and promotions visible to every reseller (no segment- or reseller-specific visibility rule, CAT-05), and availability only as En stock / En rupture (STK-07).
  4. No commitments. Cart, favorites, orders, finance, tickets, notifications and the live stream need a session (401 UNAUTHENTICATED). In the apps, « Ajouter au panier », the favourite heart and « Commander » open the sign-up prompt of RA-41 instead of calling the API; the prompt keeps the product the guest was looking at and returns to it after sign-up or sign-in.
  5. WhatsApp button. The fixed button works for guests: support.whatsapp resolves the support region from the device's position (nearest commune → wilaya → region) or a wilaya the guest picks, else the default number (SUP-05).
  6. After sign-up. The new reseller sees the prices of the declared customer type right away while catalog.pricesVisibleBeforeValidation is true; ordering still waits for account validation (AUTH-08).
  7. Caching. Guest responses carry no personal data and may be cached publicly for 5 minutes (CDN) — they must never be served to a signed-in caller; priced responses are Cache-Control: private, no-store.
  8. Tracking. Guests are not tracked individually: no invitation step and no analytics event carries personal data; an install from an invitation link still records APP_INSTALLED through the device attribution (IVT-04).
  9. Signing out of the reseller app returns to guest mode; an expired token is dropped silently and the app continues as a guest.

Acceptance criteria

  • Without a token, catalog.products.list returns cards with price: null and priceLocked: true, and RA-12 shows the lock on every card.
  • A guest request with sort=price_asc or priceMin returns 400 VALIDATION_FAILED with SIGN_IN_REQUIRED; catalog.facets for a guest has no priceRange.
  • A product visible only to the segment Grossistes Oran is absent from guest lists and its detail returns 404 NOT_FOUND.
  • cart.get and favorites.list without a token return 401 UNAUTHENTICATED; tapping « Ajouter au panier » as a guest opens the RA-41 prompt and no API call is made.
  • A guest in Oran taps the WhatsApp button and support.whatsapp (with the device position) returns the Ouest region number.
  • After sign-up with Détail, the same product page shows the Détail price without restarting the app.
  • A guest response is never returned to a signed-in reseller from a cache (checked with the reseller's token after a guest request on the same URL).

Data — No account data. Reads Product, ProductVariant, ProductVisibilityRule, Promotion, Banner, SupportRegion, SupportRegionWilaya, AppVersionPolicy.

Interfaces

Notes / open points — Whether a pending account sees its declared type's prices, the Détail price only, or nothing until validation is D-18 (default: declared type).

AUTH-11 — Customer type declared at sign-up, reviewed at validation

ReleasePrioritySourceAppsOwner tasks
V1MUSTTeam decision (17 Oct 2026)RA · RW · BO · APISRV-05 · DSH-03 · WEB-01 · MOB-06

Statement — The system shall record a customer type (SUPER_GROS, DEMI_GROS or DETAIL) for every reseller — declared at sign-up or set by the back-office — have it confirmed or corrected when the account is validated, and allow later changes only by the back-office, with a reason and an audit entry.

Rules

  1. Every reseller who can order has exactly one customerType (Reseller.customerType); it selects which of the three variant prices they pay (PRC-02). Field prospects may have none until validation.
  2. Declaration. At self sign-up the type is required (AUTH-03); RA-05 shows the three types with their descriptions (Super-gros — grossiste, gros volumes; Demi-gros — volumes intermédiaires; Détail — boutique, petites quantités). A delegate may record a type when creating a prospect; the back-office sets it directly when creating a reseller or importing customers.
  3. Review at validation. The validator (CRM-09) sees the declared type and, when relevant, the type recorded by the delegate; approving confirms it or changes it (customerTypeReason required when it differs, 3–500 characters); approving a record without any type requires one (422 CUSTOMER_TYPE_REQUIRED). The approval message tells the reseller their confirmed tier.
  4. Later changes. Only the back-office changes the type (admin.resellers.update, crm.update), always with a reason (422 REASON_REQUIRED otherwise). The reseller can never change it: profile.update refuses the field (422 FIELD_NOT_EDITABLE); a delegate cannot change it after creation.
  5. Effects of a change. The new prices apply to the next read: the server cart is re-priced (lines flagged PRICE_CHANGED), the catalog shows the new prices; orders already submitted keep their snapshot prices. The reseller is notified (account.customer_type_changed, transactional) and the apps refresh Me.
  6. Traceability. Every change — declaration, confirmation, correction — is kept in the type history (from, to, time, actor, reason) shown on the reseller 360° page, and written to AuditLog (UPDATE) and the CRM timeline.
  7. Customer types are reported and filterable in the CRM list (BO-09), dashboards and exports; the declared and confirmed types are both kept so HOCO can measure how often declarations are corrected.

Acceptance criteria

  • Signing up without a type returns 400 VALIDATION_FAILED; with DETAIL the reseller's catalog shows the Détail prices.
  • The validator approves a reseller who declared DETAIL as DEMI_GROS with a reason; the 360° page shows declared DETAIL, confirmed DEMI_GROS, and the approval message names Demi-gros.
  • Approving a field prospect without a type and without customerType returns 422 CUSTOMER_TYPE_REQUIRED.
  • profile.update with customerType returns 422 FIELD_NOT_EDITABLE.
  • Changing an active reseller from DEMI_GROS to SUPER_GROS without a reason returns 422 REASON_REQUIRED; with a reason, the next cart.get shows the new prices with PRICE_CHANGED and an order submitted the day before keeps its prices.
  • The activity log shows each type change with before, after, actor and reason.

Data — Reseller (customerType, declared type, type history), AuditLog, Interaction, Notification.

Interfaces

Notes / open points — The criteria that qualify a shop for each type and who reviews the declaration are D-01 and D-30.

Error codes

CodeHTTPRaised when
INVALID_CREDENTIALS401Unknown identifier or wrong password (indistinguishable)
UNAUTHENTICATED401Missing, expired or revoked session (details.reason)
WRONG_APP403The user type cannot use this app (details.userType)
ACCOUNT_SUSPENDED403Account SUSPENDED or DISABLED (details.status)
ACCOUNT_PENDING_VALIDATION403Pending reseller attempting checkout, assisted-order confirmation, payment declaration or return request
PASSWORD_CHANGE_REQUIRED403Temporary password not changed yet
REAUTH_REQUIRED403Sensitive back-office action without a fresh session
INVITATION_NOT_FOUND · INVITATION_EXPIRED404 · 410Unknown, expired or replaced invitation code at sign-up
PHONE_ALREADY_EXISTS · INVITATION_ALREADY_USED409Sign-up on a phone or an invited record that already has an account
OTP_INVALID · OTP_EXPIRED422Wrong, expired, replaced or used one-time code
VERIFICATION_TOKEN_INVALID422Verification token expired, used, or of another purpose
INVALID_CURRENT_PASSWORD422Wrong current password on a password change
CUSTOMER_TYPE_REQUIRED · FIELD_NOT_EDITABLE · REASON_REQUIRED422Customer type missing at approval, changed by the reseller, or changed without reason
OTP_RESEND_TOO_SOON · RATE_LIMITED · TOO_MANY_ATTEMPTS429Cooldown, hourly caps, attempts exhausted or sign-in lock
UPGRADE_REQUIRED426App version below the minimum
MAINTENANCE · OTP_DELIVERY_FAILED503Maintenance mode; SMS gateway failure
Field codes in 400 VALIDATION_FAILED400INVALID_PHONE, INVALID_IDENTIFIER, COMMUNE_NOT_IN_WILAYA, PASSWORD_TOO_WEAK, PASSWORD_REUSED, TERMS_NOT_ACCEPTED, SIGN_IN_REQUIRED

On this page