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).
| ID | Requirement | Release | Priority | Source | Apps |
|---|---|---|---|---|---|
AUTH-01 | Back-office staff sign-in (email or phone + password) | V1 | MUST | §3, §10 | BO |
AUTH-02 | Delegate sign-in with phone + password; suspended accounts blocked with an explicit message | V1 | MUST | §6 | DA |
AUTH-03 | Reseller self sign-up: phone verified by OTP, store details, declared customer type (super-gros, demi-gros, détail), password, optional invitation code | V1 | MUST | §5, §6.6, §12 | RA · RW |
AUTH-04 | Reseller sign-in (phone + password) and password reset by OTP | V1 | MUST | §5, §11 | RA · RW |
AUTH-05 | Account claim: a prospect created in the field is activated by the reseller without creating a duplicate | V1 | MUST | §4.2, §6.5, §6.6 | RA · RW · API |
AUTH-06 | Sessions: expiry, multi-device, listing and revocation, sign-out | V1 | MUST | §10 | BO · RA · RW · DA |
AUTH-07 | Password policy, hashing, OTP expiry/resend cooldown and attempt limiting | V1 | MUST | §10 | API |
AUTH-08 | Account statuses (pending validation, active, suspended, disabled) gate what each user can do | V1 | MUST | §6, §10 | API · BO · RA · RW · DA |
AUTH-09 | Device registration for push and minimum-app-version enforcement | V1 | MUST | §9, §11 | RA · DA · API |
AUTH-10 | Guest mode: the reseller app and web can be browsed without an account; prices stay hidden until the visitor signs up and declares a customer type | V1 | MUST | Team decision (17 Oct 2026) | RA · RW · API |
AUTH-11 | Customer type declared at sign-up, reviewed at account validation; any later change is made by the back-office and audited | V1 | MUST | Team decision (17 Oct 2026) | RA · RW · BO · API |
Settings used in this area
| Setting or constant | Default | Meaning |
|---|---|---|
catalog.pricesVisibleBeforeValidation (setting) | true | A 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 three | Customer 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 attempts | Code length, validity and wrong attempts allowed per code. |
| OTP sending (constant) | 1 per 60 s · 5 per hour per phone · 20 per hour per IP | Resend cooldown and hourly caps. |
| Verification token (constant) | 10 min, single use | Proof 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 failures | Brute-force protection. |
| Session lifetime (constant) | 30 days reseller and delegate · 7 days back-office, sliding | Expiry without activity. |
| Fresh-session window (constant) | 15 min | Back-office actions marked sensitive need a sign-in this recent (D-33). |
Requirements
AUTH-01 — Back-office staff sign-in
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
- One identifier field accepts an email (trimmed, lower-cased, looked up among
STAFFusers only) or a phone in any common format, normalised to E.164 (CRM-03); the value is an email when it contains@. - The request declares the app (
app: "DASHBOARD"). The password is checked first: an unknown identifier and a wrong password both answer401 INVALID_CREDENTIALSwith 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. - Only after a correct password: a user type other than
STAFF→403 WRONG_APP;SUSPENDEDorDISABLED→403 ACCOUNT_SUSPENDEDwithdetails.status;ACTIVE→ session. - Throttling and lockout follow AUTH-07 R7.
- Accounts created by an administrator (ADM-01) carry a temporary password and
mustChangePassword = true: until the password is changed, every operation exceptauth.password.change,auth.logout,auth.session,me.getandappConfig.getanswers403 PASSWORD_CHANGE_REQUIRED. Staff passwords are at least 10 characters. - The session is the httpOnly cookie
hoco.session_tokenon.hoco.dz(Secure,SameSite=Lax), valid 7 days and sliding (AUTH-06); cookie-authenticated mutations are accepted only from the allowed origins. - 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_REQUIREDand the dashboard asks for the password again (a newauth.loginreplaces the session). - Every success writes
AuditLogLOGIN(appDASHBOARD, IP, user agent) and updateslastLoginAt; every failure writesLOGIN_FAILEDwith 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 notificationsecurity.new_deviceby email. - 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 withNADIA@hoco.dzand the right password, then the cookie is set and the response carries her roles and permissions inuser. - Given her phone
+213555123456, signing in with0555 12 34 56also succeeds. - A wrong password and an unknown email return identical
401 INVALID_CREDENTIALSbodies. - A delegate's correct phone and password with
app: "DASHBOARD"return403 WRONG_APPand noSet-Cookie. - A suspended staff user gets
403 ACCOUNT_SUSPENDEDwith the right password and401 INVALID_CREDENTIALSwith a wrong one. - After 5 consecutive failures from one IP, a 6th attempt within 15 minutes returns
429 TOO_MANY_ATTEMPTSwithRetry-After, even with the right password; the same account from another IP is not locked. - With a temporary password,
admin.orders.listreturns403 PASSWORD_CHANGE_REQUIREDuntilauth.password.changesucceeds. - 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
- Screens: BO-01 Sign in
- API:
auth.loginPOST /auth/login
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
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
- 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).
- Identifier: the phone, normalised to E.164;
app: "DELEGATE". Password first, then403 WRONG_APPfor another user type, then the status (AUTH-01 R2–R3). ACTIVE→ bearer token (30 days, sliding) stored in secure storage.SUSPENDED→403 ACCOUNT_SUSPENDEDand DA-02 shows « Votre compte est suspendu. Contactez votre responsable. » with the support contacts ofappConfig.get;DISABLED→ the same code withdetails.status = "DISABLED"and « Ce compte est désactivé. ». Nothing is stored in either case.- A delegate suspended while signed in loses every session at once; their next call — including
sync.batch— answers403 ACCOUNT_SUSPENDED, so the app shows the suspension message rather than "session expired". - A new sign-in on the same device (
X-Device-Id) and app replaces the previous session of that device. - 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.
- 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). - Throttling and audit as AUTH-01 R4 and R8 (app
DELEGATE_APP).
Acceptance criteria
- Given active delegate Karim (
+213661234567), signing in with0661 23 45 67returns a token anduser.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_APPwithdetails.userType = "RESELLER". - Given Karim signed in, when an administrator suspends him, his next call returns
403 ACCOUNT_SUSPENDEDand 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
- Screens: DA-02 Sign in
- API:
auth.loginPOST /auth/login
AUTH-03 — Reseller self sign-up with OTP and customer type
| Release | Priority | Source | Apps | Owner tasks |
|---|---|---|---|---|
| V1 | MUST | cahier §5, §6.6, §12 | RA · RW | SRV-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
- Steps: phone and optional invitation code (RA-03) →
auth.otp.sendpurposeSIGNUP→ code (RA-04) andauth.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). - Phones must be Algerian mobiles: after normalisation
+213followed by 9 digits starting with 5, 6 or 7 (field codeINVALID_PHONE). auth.otp.sendforSIGNUPrefuses a phone that already has a user account with409 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).- 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.
- Required at sign-up: owner name (2–80), store name (2–120), business type (
BUSINESS_TYPElist), wilaya and commune (the commune must belong to the wilaya — GEO-01), address line (5–200), customer type (one ofsignup.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 codeTERMS_NOT_ACCEPTED). Optional: landmark, GPS position (with permission), WhatsApp (« Même numéro » switch), email, invitation code. - Invitation code: 8 characters, case-insensitive, with or without the dash (
ab12-cd34=AB12CD34), pre-filled from the linkhttps://app.hoco.dz/i/AB12CD34or the install referrer (IVT-04). An unknown code answers404 INVITATION_NOT_FOUNDand the app offers « Continuer sans code »; an expired or replaced one410 INVITATION_EXPIRED. - 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(sourceSELF_SIGNUP, numberREV-…, statusACCOUNT_CREATED, validationPENDING) with its store address as default delivery address. TheUser(typeRESELLER, roleRESELLER, phone verified, statusPENDING_VALIDATION) is created in the same transaction. - Assignment of a new record: to the attributed invitation's delegate; without invitation it stays unassigned and the validator assigns it (CRM-05).
- 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.pricesVisibleBeforeValidationis true) and on the pending state of RA-06 for ordering. - Side effects: invitation steps
ACCOUNT_CREATEDandPHONE_VERIFIEDon the attributed invitation and theSIGNUPSobjective credit to its delegate;account.pending_validationto users holdingcrm.validate(in-app and back-office live event) and to the assigned delegate (push);AuditLogCREATE; CRM timelineSTATUS_CHANGE. At most 10 sign-ups per hour per IP (429 RATE_LIMITED). - A retry after a lost response fails with
422 VERIFICATION_TOKEN_INVALID(USED) or409 PHONE_ALREADY_EXISTS; the app then triesauth.loginwith 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"returns201with a session; the account isPENDING_VALIDATION, the resellerACCOUNT_CREATED, sourceSELF_SIGNUP,customerTypeDEMI_GROS, and it appears in BO-13 within 2 seconds. - Right after sign-up,
catalog.products.listreturns the Demi-gros prices andcart.checkoutreturns403 ACCOUNT_PENDING_VALIDATION. auth.otp.sendwith the phone of an existing account returns409 PHONE_ALREADY_EXISTSwithcanSignIn: true.- Omitting
customerTypereturns400 VALIDATION_FAILED; a commune of another wilaya returns field codeCOMMUNE_NOT_IN_WILAYA; terms not accepted returnTERMS_NOT_ACCEPTED. - An expired or reused verification token returns
422 VERIFICATION_TOKEN_INVALID. - With code
ab12-cd34from 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 returns404 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.sendPOST /auth/otp/send·auth.otp.verifyPOST /auth/otp/verify·auth.signup.resellerPOST /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
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
- Sign-in uses the phone in any common format and the password with
app: "RESELLER"; the server records the client asRESELLER_APP(mobile headers present) orRESELLER_WEB. Password first, then403 WRONG_APP, then the status (AUTH-01 R2–R3). Throttling and audit as AUTH-01. ACTIVEandPENDING_VALIDATIONsign in (pending users get the access of AUTH-08);SUSPENDEDandDISABLEDget403 ACCOUNT_SUSPENDEDwith the support contacts.- 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). - Reset (RA-08, RW-01):
auth.otp.sendpurposePASSWORD_RESETanswers the same202whether 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. auth.otp.verifyreturns a verification token;auth.password.resetwith the token and a new password answers204. The new password follows AUTH-07 and must differ from the current one (field codePASSWORD_REUSED).- 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.
- Signing out of the reseller app returns it to guest browsing (AUTH-10) and keeps the catalog cache.
- 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
202and the SMS gateway log shows no message. - With the correct code, reset returns
204; the old password fails; every previous session returns401 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 codePASSWORD_REUSED. - A pending reseller signs in;
cart.checkoutthen returns403 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.loginPOST /auth/login·auth.otp.sendPOST /auth/otp/send·auth.password.resetPOST /auth/password/reset
AUTH-05 — Account claim without duplicate
| Release | Priority | Source | Apps | Owner tasks |
|---|---|---|---|---|
| V1 | MUST | cahier §4.2, §6.5, §6.6 | RA · RW · API | SRV-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
- Eligibility. A claim applies to a
Resellerwithout a linked user, whatever its status or source (aNEW_PROSPECTfrom the field, aNOT_INTERESTEDrecord, anACTIVEcustomer imported at go-live). - 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. - Flow.
auth.otp.send(SIGNUP) →auth.otp.verifyreturnssignup.claimable = trueand 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. - No duplicate. In one transaction the server locks the matching
Resellerrow, checks that no user is linked (otherwise409 PHONE_ALREADY_EXISTS), creates theUserand sets the link. No secondReselleris ever created for the phone; the unique index on the normalised main phone is the final guarantee under concurrency (CRM-03). - 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 toAuditLog(UPDATE, actor = the new user, contextsignup.claim) so the validator sees what changed. - 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.
- 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 recordsACCOUNT_CREATEDandPHONE_VERIFIEDand its delegate gets theSIGNUPScredit. - Lifecycle. The reseller moves to
ACCOUNT_CREATEDunless it is already further (FIRST_ORDER,ACTIVE);SUSPENDEDstaysSUSPENDED(Lifecycles › Reseller). - Account status. A record already
APPROVED(a point of sale validated by the back-office, an imported customer) gets anACTIVEaccount at once and no validation is requested; otherwise the account isPENDING_VALIDATIONand the record enters the validation queue as kindCLAIM(CRM-09). - Notifications. The creating and assigned delegates receive « Le revendeur Magasin El Wafa a activé son compte » (push and in-app); validators receive
account.pending_validationwhen validation is needed.
Acceptance criteria
- Given prospect
REV-000123created by delegate Yacine with phone+213555123456and no user, when the owner signs up with0555 12 34 56, then verification returnsclaimable: truewith 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 statusACCOUNT_CREATED; the response hasclaimed: 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 other409 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
APPROVEDgets anACTIVEaccount; a prospect stillPENDINGappears in BO-13 with kindCLAIM. - An imported
ACTIVEcustomer who claims staysACTIVEand 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.resellerPOST /auth/signup/reseller
AUTH-06 — Sessions, devices and sign-out
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
- 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.
- Transport: a request carrying
X-App-Platformis mobile — the token is returned in the body (hs_…, kept in secure storage, sent asAuthorization: Bearer); otherwise the API sets the httpOnly cookiehoco.session_tokenand never returns the token. Tokens are opaque; the database stores only their hash. - One session per device and app: a new sign-in from the same
X-Device-Idfor 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. - Each session records the client (
ClientApp), device id, platform, app version, device name, user agent, IP,createdAtandlastActiveAt(refreshed at most every 5 minutes). auth.sessions.listreturns the caller's active sessions with acurrentflag;auth.sessions.revokerevokes one of them — an unknown, already revoked or foreign session id answers404 NOT_FOUND; revoking the current one is a sign-out. Resellers see them under Paramètres (RA-35), staff under Mon profil (BO-68).auth.logoutrevokes the current session, detaches the device bound to it and deletes its push token, and closes itsevents.streamconnection; it answers204even for an expired token so clients can always clear their state.- Forced revocation: password change (other sessions), password reset, suspension and disabling (all sessions). Administrators holding
users.administerlist and revoke any user's sessions withadmin.users.sessions; each revocation writesAuditLogLOGOUTwith the administrator as actor. - An expired or revoked token answers
401 UNAUTHENTICATEDwithdetails.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, withcurrenton the caller's. - When phone A revokes phone B's session, B's next request returns
401 UNAUTHENTICATEDwithreason: "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.logouton 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
401and the activity log showsLOGOUTby that administrator.
Data — Session, Device, User, AuditLog.
Interfaces
- Screens: RA-35 Settings · BO-68 My profile & sessions
- API:
auth.logoutPOST /auth/logout·auth.sessionGET /auth/session·auth.sessions.listGET /auth/sessions·auth.sessions.revokeDELETE /auth/sessions/{sessionId}·admin.users.sessionsGET · DELETE /admin/users/{userId}/sessions[/{sessionId}]
AUTH-07 — Password policy, hashing, OTP and attempt limits
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
- 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_FAILEDwith field codePASSWORD_TOO_WEAKanddetails.minLength; the same password as the current one returnsPASSWORD_REUSED. - 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. - One-time codes: 6 digits from a cryptographic random source, stored hashed in
Verificationwith 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. - Sending limits: 1 code per 60 seconds per destination and purpose (
429 OTP_RESEND_TOO_SOONwithdetails.retryAfterSeconds), 5 per hour per phone and 20 per hour per IP (429 RATE_LIMITED). A gateway failure answers503 OTP_DELIVERY_FAILEDand is not counted. RA-04 shows the cooldown (« Renvoyer le code dans 0:42 »). - A wrong code returns
422 OTP_INVALIDwithdetails.attemptsRemaining; the fifth wrong attempt destroys the code (429 TOO_MANY_ATTEMPTS); an expired, replaced or used code returns422 OTP_EXPIRED. - 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_INVALIDwithdetails.reason:EXPIRED,USED,PURPOSE_MISMATCH). - 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. - 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
NotificationDeliveryfor cost follow-up (INT-07). auth.password.changerequires the current password (422 INVALID_CURRENT_PASSWORD, blocked 15 minutes after 5 wrong attempts) and revokes the user's other sessions.- 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
azerty12is accepted for a reseller and refused for staff (PASSWORD_TOO_WEAK,minLength: 10);12345678andpassword1are refused; a password containing123456for the phone+213555123456is refused.- The
Accountrow holds a scrypt hash; no log line contains the submitted password or code. - Two
auth.otp.sendcalls 30 seconds apart: the second returns429 OTP_RESEND_TOO_SOONwithretryAfterSecondsnear 30; a sixth code within an hour returns429 RATE_LIMITED. - A wrong code returns
422 OTP_INVALIDwithattemptsRemaining: 4; after the fifth wrong code the right code is refused. - A code verified after 5 minutes returns
422 OTP_EXPIRED; aPASSWORD_RESETtoken sent toauth.signup.resellerreturns422 VERIFICATION_TOKEN_INVALIDwithPURPOSE_MISMATCH. - After
auth.password.change, the other sessions return401; the current one keeps working.
Data — Account, Verification, Session, NotificationDelivery, AuditLog; throttling counters in Redis.
Interfaces
- Screens: RA-04 OTP verification · RA-08 Forgot password · DA-03 Forgot password · BO-02 Forgot / reset password
- API:
auth.otp.sendPOST /auth/otp/send·auth.otp.verifyPOST /auth/otp/verify·auth.password.resetPOST /auth/password/reset·auth.password.changePOST /auth/password/change
AUTH-08 — Account statuses gate what users can do
Statement — The system shall give every user one account status and enforce, on every API call, what that status allows.
Rules
-
Statuses (
AccountStatus):PENDING_VALIDATION(resellers only),ACTIVE,SUSPENDED,DISABLED. Guests have no account and follow AUTH-10. -
What each status allows (reseller columns apply to reseller users; staff and delegates are
ACTIVE,SUSPENDEDorDISABLED):Status Sign-in Catalog Prices Cart Checkout, assisted-order confirmation, payment declaration, return request Profile, documents, tickets, notifications PENDING_VALIDATIONYes Yes Of the declared type if catalog.pricesVisibleBeforeValidation, else hidden as for a guestYes No — 403 ACCOUNT_PENDING_VALIDATIONYes ACTIVEYes Yes Of the customer type Yes Yes Yes SUSPENDEDNo No No No No No DISABLEDNo No No No No No Every call from a
SUSPENDEDorDISABLEDuser answers403 ACCOUNT_SUSPENDED(details.statustells which). -
The account status (access) is distinct from the commercial status of the store (CRM-04): a store commercially
SUSPENDEDkeeps anACTIVEaccount — the owner signs in, reads orders and invoices and pays — but checkout, assisted orders and return requests answer403 RESELLER_SUSPENDED. -
Transitions (Lifecycles › AccountStatus):
PENDING_VALIDATION → ACTIVEat approval (CRM-09);PENDING_VALIDATION → DISABLEDat a rejection withdisableAccount(duplicate, fraud);ACTIVE ↔ SUSPENDED;ACTIVEorSUSPENDED → DISABLED;DISABLED → ACTIVE(reactivation). A reason is required except for approval. Staff and delegates change throughadmin.users.setStatus(users.update; reactivating a disabled account needsusers.administer); reseller logins through the same operation from the reseller 360° page by a user holdingcrm.administer. -
Leaving
ACTIVEorPENDING_VALIDATIONrevokes every session and closes live streams; the user is told by SMS (resellers, delegates) or email (staff);AuditLogSTATUS_CHANGErecords the actor and the reason. -
History is never deleted (SEC-06): a disabled user's orders, visits and audit entries stay attributed to them; they disappear from assignment pickers.
-
A disabled user's phone stays reserved until an administrator holding
users.administerreleases it (the number moves to the user's history), so a company SIM can be given to a new delegate. -
The status is checked right after authentication and before permissions (ACL-05), from a cache of at most 60 seconds invalidated on change.
-
me.getreturnsstatusand, for resellers,reseller.validationStatusandrejectionReason, which drive RA-06 (pending and rejected states).
| Value | UI label (fr) | Meaning |
|---|---|---|
PENDING_VALIDATION | En attente de validation | Reseller signed up; can browse the catalog (if allowed by settings) but cannot order until validated. |
ACTIVE | Actif | Full access for the user's role. |
SUSPENDED | Suspendu | Temporarily blocked; sign-in refused with an explicit message; sessions revoked. |
DISABLED | Désactivé | Permanently deactivated; history kept, sign-in impossible. |
Acceptance criteria
- A pending reseller lists products with Demi-gros prices, adds to the cart and uploads an RC document;
cart.checkoutreturns403 ACCOUNT_PENDING_VALIDATION. - With
catalog.pricesVisibleBeforeValidation = false, the same reseller getsprice: nullandpriceLocked: truelike 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_SUSPENDEDat 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
- Screens: RA-06 Account pending validation / rejected · RA-07 Sign in · DA-02 Sign in
- API:
auth.loginPOST /auth/login·me.getGET /me
AUTH-09 — Device registration and minimum app version
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
- After sign-in and at every app start, the app calls
me.devices.registerwith 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. - 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.
- 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.
- The version policy (
AppVersionPolicyper app and platform: minimum and latest version, maintenance flag and message) is edited in BO-66 (ADM-07) and read without a session throughappConfig.getat the splash screen (RA-01, DA-01) — guests included. - Mobile requests send
X-App-VersionandX-App-Platform. Below the minimum, the API answers426 UPGRADE_REQUIREDwithdetails.storeUrlanddetails.minVersionon every operation exceptappConfig.get,auth.logoutand — 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. - 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.
- Maintenance on: every operation except
appConfig.getanswers503 MAINTENANCEwithdetails.until(never for the back-office); the apps show their maintenance state (RA-36, DA-28). - Versions compare semantically (
1.10.0above1.9.3); a mobile session withoutX-App-Versioncounts as below the minimum. me.devices.removeand sign-out unregister the device's push token.
Acceptance criteria
- Registering twice with the same device id and a new FCM token leaves one
Devicerow 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 from1.2.9returns426 UPGRADE_REQUIREDwith the store URL;appConfig.getstill answers200. - One hour after the minimum was raised, a delegate app in
1.2.9can still replay its queue throughsync.batch; 73 hours later the same call returns426. - With maintenance until 14:00,
catalog.products.listreturns503 MAINTENANCEwithdetails.until.
Data — Device, AppVersionPolicy, Session, NotificationDelivery.
Interfaces
- Screens: RA-01 Splash & version check · DA-01 Splash & version check
- API:
me.devices.registerPOST /me/devices·me.devices.removeDELETE /me/devices/{deviceId}
AUTH-10 — Guest mode without prices
| Release | Priority | Source | Apps | Owner tasks |
|---|---|---|---|---|
| V1 | MUST | Team decision (17 Oct 2026) | RA · RW · API | SRV-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
- 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.getandreference.*— withoutAuthorization. The welcome screen (RA-02) offers « Se connecter », « Créer un compte » and « Découvrir le catalogue ». - Prices hidden everywhere. For a guest every price field is
nullandpriceLocked: 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 codeSIGN_IN_REQUIRED); the facets carry no price range (CAT-12). - 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).
- 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. - WhatsApp button. The fixed button works for guests:
support.whatsappresolves the support region from the device's position (nearest commune → wilaya → region) or a wilaya the guest picks, else the default number (SUP-05). - After sign-up. The new reseller sees the prices of the declared customer type right away while
catalog.pricesVisibleBeforeValidationis true; ordering still waits for account validation (AUTH-08). - 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. - 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_INSTALLEDthrough the device attribution (IVT-04). - 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.listreturns cards withprice: nullandpriceLocked: true, and RA-12 shows the lock on every card. - A guest request with
sort=price_ascorpriceMinreturns400 VALIDATION_FAILEDwithSIGN_IN_REQUIRED;catalog.facetsfor a guest has nopriceRange. - A product visible only to the segment Grossistes Oran is absent from guest lists and its detail returns
404 NOT_FOUND. cart.getandfavorites.listwithout a token return401 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
- Screens: RA-02 Welcome (sign in, sign up, browse as guest) · RA-09 Home · RA-12 Catalog · RA-14 Product detail · RA-41 Guest mode — price lock and sign-up prompts · RW-01 Sign in, sign-up (with customer type), OTP, password reset — and guest browsing
- API: no dedicated operation
- Operations used by guests (access
optional):catalog.home,catalog.products.list,catalog.facets,catalog.products.get,promotions.list,promotions.get,banners.list,support.whatsapp,appConfig.get.
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
| Release | Priority | Source | Apps | Owner tasks |
|---|---|---|---|---|
| V1 | MUST | Team decision (17 Oct 2026) | RA · RW · BO · API | SRV-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
- 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. - 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.
- 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 (
customerTypeReasonrequired 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. - Later changes. Only the back-office changes the type (
admin.resellers.update,crm.update), always with a reason (422 REASON_REQUIREDotherwise). The reseller can never change it:profile.updaterefuses the field (422 FIELD_NOT_EDITABLE); a delegate cannot change it after creation. - 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 refreshMe. - 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. - 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; withDETAILthe reseller's catalog shows the Détail prices. - The validator approves a reseller who declared
DETAILasDEMI_GROSwith a reason; the 360° page shows declaredDETAIL, confirmedDEMI_GROS, and the approval message names Demi-gros. - Approving a field prospect without a type and without
customerTypereturns422 CUSTOMER_TYPE_REQUIRED. profile.updatewithcustomerTypereturns422 FIELD_NOT_EDITABLE.- Changing an active reseller from
DEMI_GROStoSUPER_GROSwithout a reason returns422 REASON_REQUIRED; with a reason, the nextcart.getshows the new prices withPRICE_CHANGEDand 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
- Screens: RA-05 Sign-up — store details, customer type & password · BO-12 Reseller create / edit · BO-13 Account validation queue · RW-01 Sign in, sign-up (with customer type), OTP, password reset — and guest browsing
- API:
auth.signup.resellerPOST /auth/signup/reseller·admin.resellers.updatePATCH /admin/resellers/{resellerId}·admin.resellers.validatePOST /admin/resellers/{resellerId}/validation
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
| Code | HTTP | Raised when |
|---|---|---|
INVALID_CREDENTIALS | 401 | Unknown identifier or wrong password (indistinguishable) |
UNAUTHENTICATED | 401 | Missing, expired or revoked session (details.reason) |
WRONG_APP | 403 | The user type cannot use this app (details.userType) |
ACCOUNT_SUSPENDED | 403 | Account SUSPENDED or DISABLED (details.status) |
ACCOUNT_PENDING_VALIDATION | 403 | Pending reseller attempting checkout, assisted-order confirmation, payment declaration or return request |
PASSWORD_CHANGE_REQUIRED | 403 | Temporary password not changed yet |
REAUTH_REQUIRED | 403 | Sensitive back-office action without a fresh session |
INVITATION_NOT_FOUND · INVITATION_EXPIRED | 404 · 410 | Unknown, expired or replaced invitation code at sign-up |
PHONE_ALREADY_EXISTS · INVITATION_ALREADY_USED | 409 | Sign-up on a phone or an invited record that already has an account |
OTP_INVALID · OTP_EXPIRED | 422 | Wrong, expired, replaced or used one-time code |
VERIFICATION_TOKEN_INVALID | 422 | Verification token expired, used, or of another purpose |
INVALID_CURRENT_PASSWORD | 422 | Wrong current password on a password change |
CUSTOMER_TYPE_REQUIRED · FIELD_NOT_EDITABLE · REASON_REQUIRED | 422 | Customer type missing at approval, changed by the reseller, or changed without reason |
OTP_RESEND_TOO_SOON · RATE_LIMITED · TOO_MANY_ATTEMPTS | 429 | Cooldown, hourly caps, attempts exhausted or sign-in lock |
UPGRADE_REQUIRED | 426 | App version below the minimum |
MAINTENANCE · OTP_DELIVERY_FAILED | 503 | Maintenance mode; SMS gateway failure |
Field codes in 400 VALIDATION_FAILED | 400 | INVALID_PHONE, INVALID_IDENTIFIER, COMMUNE_NOT_IN_WILAYA, PASSWORD_TOO_WEAK, PASSWORD_REUSED, TERMS_NOT_ACCEPTED, SIGN_IN_REQUIRED |
Requirements
How HOCO's requirements are written, prioritised and traced, and the 25 requirement areas with their V1/V2 counts.
Roles, permissions & data scope
The eight roles, the module × action permission matrix, delegate and reseller data scopes, least privilege and server-side enforcement on every operation.