Prospects, points of sale, follow-ups & invitations
The delegate's CRM in the field — scoped list and map of points of sale, duplicate check, offline prospect creation, point-of-sale sheet and edits, notes and photos, invitations with activation tracking, and follow-up tasks.
These operations cover the field side of the CRM (cahier §4.2, §6.5, §6.6, §6.8): finding the points of sale (points de vente) the delegate works with, registering new prospects on the spot, keeping their record up to date, inviting them to install the reseller app and following up. The lifecycle rules of a reseller are owned by Lifecycles — Reseller; the back-office counterpart is CRM.
Offline behaviour. The app keeps every point of sale of its scope as a ResellerCard in its local database (kept fresh by sync.delta) and the full sheet of today's stops (from the today snapshot). Creating a prospect, editing a record, adding a note or a photo, sharing an invitation and handling tasks are offline-capable: applied locally, queued with occurredAt and an Idempotency-Key, replayed through sync.batch. Creations carry a client-generated UUIDv7 id, so items queued after a creation (photo, note, unplanned visit) reference it and declare it in dependsOn. Three operations need the network: the duplicate check (check-phone, the app falls back to its local cache), invitation creation (the code is server-generated) and the reads of fresh sheets and invitation steps.
Scope rule
A delegate D sees and acts on a point of sale R only when R is in D's delegate scope, evaluated by the server on every call (ACL-03, cahier §3.1 « Les délégués voient uniquement leur zone, leurs tournées, leurs revendeurs attribués »):
| Clause | Condition | Allowed |
|---|---|---|
| (a) Assigned | R.assignedDelegateId = D — any status, any commune | Read, edit, notes, photos, invitations, tasks, assisted orders, visits |
| (b) Unassigned prospect of my zones | R.assignedDelegateId is null and R.zoneId is one of D's zones (zone derived from the commune, BR-46) and R.status is a qualification status — NEW_PROSPECT, TO_CONTACT, INTERESTED, NOT_INTERESTED, TO_FOLLOW_UP | Same as (a) — visible so that delegates pick prospects up instead of creating duplicates |
| (c) Planned in my tours | R is the point of sale of a visit of a tour of D dated from today − 7 days to today + 30 days (any tour status) — covers replacements during leave | Same as (a) |
This is the decided rule of the permissions matrix. Everything else is out of scope — in particular a record assigned to another delegate, even inside D's zones (unless planned in D's tours), and unassigned records beyond the qualification statuses (the back-office assigns them). A prospect created by D is assigned to D at creation (BR-47), so it falls under (a); a reassignment cancels the store's future PLANNED visits in D's tours (BR-48). Out-of-scope records answer 404 NOT_FOUND (existence is not leaked); a record that was in D's scope (D created it or was assigned to it before) answers 403 OUT_OF_SCOPE, so an offline action replayed after a reassignment is explained in the sync centre. The duplicate check is the only place where D learns that a phone number exists outside the scope, without any detail.
| Method | Path | Operation | Access | Apps | Release |
|---|---|---|---|---|---|
| GET | /delegate/resellers | delegate.resellers.listPoints of sale and prospects in my scope: search, status filter, distance sort | delegate | DA | V1 |
| GET | /delegate/resellers/check-phone | delegate.resellers.checkPhoneDuplicate check before creating a prospect | delegate | DA | V1 |
| POST | /delegate/resellers | delegate.resellers.createCreate a prospect/reseller in the field (client-generated id; offline-safe) | delegateIdempotency-Key | DA | V1 |
| GET | /delegate/resellers/{resellerId} | delegate.resellers.getPoint-of-sale sheet: details, status, recent visits, orders summary, open tasks, photos | delegate | DA | V1 |
| PATCH | /delegate/resellers/{resellerId} | delegate.resellers.updateUpdate a prospect/reseller in my scope (status changes per allowed transitions) | delegateIdempotency-Key | DA | V1 |
| POST | /delegate/resellers/{resellerId}/notes | delegate.resellers.addNoteAdd a note or call log to the interaction timeline | delegateIdempotency-Key | DA | V1 |
| POST | /delegate/resellers/{resellerId}/photos | delegate.resellers.addPhotoAttach a façade/interior photo | delegateIdempotency-Key | DA | V1 |
| POST | /delegate/resellers/{resellerId}/invitations | delegate.invitations.createCreate (or return the active) invitation: code, link, QR payload, share texts | delegateIdempotency-Key | DA | V1 |
| GET | /delegate/invitations/{invitationId} | delegate.invitations.getInvitation with its activation steps | delegate | DA | V1 |
| POST | /delegate/invitations/{invitationId}/shared | delegate.invitations.markSharedRecord that the invitation was shared and through which channel | delegateIdempotency-Key | DA | V1 |
| GET | /delegate/tasks | delegate.tasks.listMy follow-ups and tasks (due today, overdue, upcoming) | delegate | DA | V1 |
| PATCH | /delegate/tasks/{taskId} | delegate.tasks.updateReschedule or annotate a task | delegateIdempotency-Key | DA | V1 |
| POST | /delegate/tasks/{taskId}/complete | delegate.tasks.completeComplete a task with an outcome | delegateIdempotency-Key | DA | V1 |
Conventions on this page
- Caller: delegate session; mobile headers on every call.
- Mutations require
Idempotency-KeyandoccurredAt(device time; refused with422 OCCURRED_AT_OUT_OF_RANGEwhen more than 5 minutes ahead or older than 72 hours). - Errors common to every operation, not repeated:
400 VALIDATION_FAILED,400 IDEMPOTENCY_KEY_REQUIRED(mutation without the header),401 UNAUTHENTICATED,403 ACCOUNT_SUSPENDED,403 PERMISSION_DENIED,404 NOT_FOUND,409 IDEMPOTENCY_KEY_REUSED,409 IDEMPOTENCY_IN_PROGRESS,422 OCCURRED_AT_OUT_OF_RANGE(mutations),426 UPGRADE_REQUIRED,429 RATE_LIMITED,500 INTERNAL_ERROR,503 MAINTENANCE. - Phones may be sent as typed (
0558 12 34 56,00213…,+213…); they are normalised to E.164 and returned that way.
Shared shapes of this page
type ResellerStatus = "NEW_PROSPECT" | "TO_CONTACT" | "INTERESTED" | "APP_INSTALLED" | "ACCOUNT_CREATED"
| "FIRST_ORDER" | "ACTIVE" | "NOT_INTERESTED" | "TO_FOLLOW_UP" | "SUSPENDED";
type CustomerType = "SUPER_GROS" | "DEMI_GROS" | "DETAIL";
// What a list row, a map pin or a tour stop needs — kept for the whole scope in the local database.
interface ResellerCard extends ResellerSummary {
addressLine: string;
landmark: string | null;
whatsapp: string | null;
businessTypeCode: string | null; // BUSINESS_TYPE item
potential: "LOW" | "MEDIUM" | "HIGH" | null;
customerType: CustomerType | null; // declared or proposed; null when unknown
hasAccount: boolean; // a reseller user exists for this record
assignedToMe: boolean;
facadeThumbnailUrl: string | null; // signed, 5 minutes
lastVisitAt: Iso | null; // last visit DONE, by anyone
}
// The point-of-sale sheet (DA-09).
interface ResellerSheet extends ResellerCard {
email: string | null;
wilayaName: string;
communeId: Uuid;
locationSource: "FIELD_SIGNUP" | "CHECK_IN_VALIDATED" | "BACK_OFFICE" | "IMPORT" | null;
locationCandidate: GeoPoint | null; // proposed by a check-in, awaiting back-office validation
validationStatus: "PENDING" | "APPROVED" | "REJECTED";
customerTypeConfirmed: boolean; // false while only proposed (confirmed at validation, AUTH-11)
source: "DELEGATE_APP" | "BACK_OFFICE" | "SELF_SIGNUP" | "IMPORT";
accountStatus: "PENDING_VALIDATION" | "ACTIVE" | "SUSPENDED" | "DISABLED" | null;
assignedDelegate: UserSummary | null;
createdBy: UserSummary | null;
createdAt: Iso;
zone: { id: Uuid; name: string } | null;
facadePhoto: FileRef | null;
photos: ResellerPhotoView[]; // latest 10
lastVisits: Array<{ visitId: Uuid; number: string; endedAt: Iso; interestLevel: "LOW" | "MEDIUM" | "HIGH" | null; nextAction: string | null; by: UserSummary }>; // last 5
ordersSummary: { count: number; lastOrderAt: Iso | null; totalTtc90d: Money }; // no purchase prices, no credit
openTasks: TaskView[];
recentInteractions: InteractionView[]; // last 10, newest first
invitation: { id: Uuid; displayCode: string; active: boolean; currentStep: string | null } | null;
allowedStatusTransitions: ResellerStatus[]; // what this caller may set now
editableFields: string[]; // fields this caller may PATCH now
scopeClause: "ASSIGNED" | "ZONE_PROSPECT" | "TOUR_STOP";
version: number; // optimistic lock on the editable fields
updatedAt: Iso;
}
interface ResellerPhotoView {
id: Uuid;
fileId: Uuid;
kind: "FACADE" | "INTERIOR" | "SHELF" | "OTHER";
url: string;
thumbnailUrl: string;
takenAt: Iso;
location: GeoPoint | null;
caption: string | null;
isCurrentFacade: boolean;
uploadedBy: UserSummary;
createdAt: Iso;
}
interface InteractionView {
id: Uuid;
type: "CALL" | "VISIT" | "NOTE" | "WHATSAPP" | "SMS" | "EMAIL" | "ORDER" | "PAYMENT" | "CLAIM" | "STATUS_CHANGE";
body: string | null;
occurredAt: Iso;
author: UserSummary | null; // null = system
ref: { kind: "VISIT" | "ORDER" | "TASK" | "TICKET"; id: Uuid; number: string | null } | null;
}
interface TaskView {
id: Uuid;
type: "CALL" | "VISIT" | "WHATSAPP" | "SEND_CATALOG" | "OTHER";
status: "OPEN" | "DONE" | "CANCELLED";
title: string;
dueAt: Iso;
isOverdue: boolean; // derived: OPEN and dueAt in the past
reseller: ResellerSummary;
sourceVisitId: Uuid | null;
note: string | null;
createdBy: UserSummary | null; // null = system (inactivity scan)
createdAt: Iso;
completedAt: Iso | null;
outcome: string | null;
version: number;
}Purchase prices, margins, credit limits and balances are never part of these shapes (ACL-04).
GET /delegate/resellers
delegate.resellers.list · Access session, delegate scope (clauses a–c) · Apps DA · Idempotency-Key — · Offline-capable no — offline, the app runs the same filters on its local cards · Release V1 · Requirements CRM-04, ACL-03
Purpose. Prospects: the list and the map of the points of sale in scope, with search, status filters and distance sort.
Request. Query parameters:
| Name | Type | Rule |
|---|---|---|
q | string, 2–60 chars | Matches store name, owner name, REV- number and phone digits (accent- and case-insensitive) |
status | ResellerStatus, repeatable | Default: all |
assignment | MINE · UNASSIGNED | MINE = clause (a), UNASSIGNED = clause (b); default both, plus (c) |
near | lat,lng | Reference point for distances (device position or map centre) |
radiusKm | number, 0.1–50 | Requires near; default 10 |
sort | distance · name · lastVisit | distance requires near (default when near is set), else name; lastVisit = least recently visited first |
cursor, limit | Default 20, max 100 |
Response 200 OK — cursor list of ResellerListItem.
interface ResellerListItem extends ResellerCard {
distanceMeters: number | null; // straight line from `near`
validationStatus: "PENDING" | "APPROVED" | "REJECTED";
openTasksCount: number;
nextTaskDueAt: Iso | null;
}Example: GET /v1/delegate/resellers?near=35.71143,-0.57262&radiusKm=3&sort=distance
{
"items": [
{
"id": "0192d1aa-3b4c-7d5e-8f60-718293a4b123",
"number": "REV-000123",
"storeName": "Magasin El Wafa",
"ownerName": "Karim Belkacem",
"phone": "+213555123456",
"wilayaCode": "31",
"communeName": "Bir El Djir",
"status": "ACTIVE",
"location": { "lat": 35.7112, "lng": -0.5724 },
"addressLine": "Hai Sabah, cité 300 logements, local n° 4",
"landmark": "En face de la mosquée El Feth",
"whatsapp": "+213555123456",
"businessTypeCode": "PHONE_SHOP",
"potential": "HIGH",
"customerType": "DEMI_GROS",
"hasAccount": true,
"assignedToMe": true,
"facadeThumbnailUrl": "https://files.hoco.dz/store-photos/0192d1aa-b123-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=91aa",
"lastVisitAt": "2026-11-03T08:41:00Z",
"distanceMeters": 32,
"validationStatus": "APPROVED",
"openTasksCount": 1,
"nextTaskDueAt": "2026-11-10T08:00:00Z"
},
{
"id": "0193a31c-8e2f-7a40-b1c2-d3e4f5a6b342",
"number": "REV-000342",
"storeName": "Phone Center Nour",
"ownerName": "Mohamed Benali",
"phone": "+213558123456",
"wilayaCode": "31",
"communeName": "Bir El Djir",
"status": "NEW_PROSPECT",
"location": { "lat": 35.7071, "lng": -0.5802 },
"addressLine": "Hai Sabah, rue des Frères Benaouda",
"landmark": null,
"whatsapp": "+213558123456",
"businessTypeCode": "PHONE_SHOP",
"potential": "MEDIUM",
"customerType": "DETAIL",
"hasAccount": false,
"assignedToMe": true,
"facadeThumbnailUrl": "https://files.hoco.dz/store-photos/0193a31c-b342-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=e5c9",
"lastVisitAt": null,
"distanceMeters": 837,
"validationStatus": "PENDING",
"openTasksCount": 0,
"nextTaskDueAt": null
},
{
"id": "0192d1aa-3b4c-7d5e-8f60-718293a4b287",
"number": "REV-000287",
"storeName": "Electro Plus",
"ownerName": "Sofiane Medjahed",
"phone": "+213661457812",
"wilayaCode": "31",
"communeName": "Bir El Djir",
"status": "INTERESTED",
"location": { "lat": 35.7236, "lng": -0.5513 },
"addressLine": "Cité 200 logements, bloc C",
"landmark": null,
"whatsapp": "+213661457812",
"businessTypeCode": "ELECTRONICS_STORE",
"potential": "MEDIUM",
"customerType": "DETAIL",
"hasAccount": false,
"assignedToMe": true,
"facadeThumbnailUrl": "https://files.hoco.dz/store-photos/0192d1aa-b287-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=41d0",
"lastVisitAt": "2026-10-27T10:15:00Z",
"distanceMeters": 2353,
"validationStatus": "APPROVED",
"openTasksCount": 1,
"nextTaskDueAt": "2026-11-03T08:00:00Z"
}
],
"nextCursor": null
}Errors — 400 VALIDATION_FAILED (sort=distance or radiusKm without near, near malformed, q too short).
Rules & side effects. Read-only. Distances are Haversine; records without coordinates are excluded when near is given and listed last otherwise. The map view clusters pins on the device.
Used by DA-13.
GET /delegate/resellers/check-phone
delegate.resellers.checkPhone · Access session, delegate scope · Apps DA · Idempotency-Key — · Offline-capable no — offline, the app checks its local cards and the server repeats the check at creation (PHONE_ALREADY_EXISTS) · Release V1 · Requirements CRM-03
Purpose. Tell the delegate, while typing the phone of a new store (« Le téléphone doit être unique », cahier §6.5), whether the number is already known — before they fill the whole form.
Request. Query phone (any accepted format). Rate limit: 30 calls per minute per user.
Response 200 OK
interface CheckPhoneResult {
phone: string; // normalised E.164
maskedPhone: string; // "05•• •• •• 56" — shown when the record is out of scope
available: boolean;
existing: ResellerSummary | null; // only when the record is in the caller's scope
assignedToMe: boolean;
inMyScope: boolean;
}{
"phone": "+213558123456",
"maskedPhone": "05•• •• •• 56",
"available": true,
"existing": null,
"assignedToMe": false,
"inMyScope": false
}When the number belongs to a record outside the scope: available: false, existing: null, inMyScope: false — the app says « Ce numéro est déjà enregistré — contactez le back-office » with maskedPhone, and offers no creation (BR-04).
Errors — 400 VALIDATION_FAILED (phone is not an Algerian mobile number — 9 national digits starting with 5, 6 or 7, because the main phone receives OTPs: details.fields[0].code = "INVALID_PHONE").
Rules & side effects. Read-only; the lookup uses the normalised main phone (unique index). WhatsApp numbers are not unique and not checked. Each call is logged for abuse detection (enumeration).
Used by DA-14.
POST /delegate/resellers
delegate.resellers.create · Access session, delegate scope (the commune may be outside the caller's zones; the record is assigned to the caller) · Apps DA · Idempotency-Key required · Offline-capable yes — client-generated id, created locally (the new card is usable at once: unplanned visit, notes, photos), queued and replayed; a duplicate phone found at replay comes back as a CONFLICT · Release V1 · Requirements FLD-11, CRM-02, CRM-03, AUTH-11
Purpose. « Nouveau revendeur »: register a store on the spot (cahier §6.5) with the minimum data of cahier §15.5, its GPS position and a façade photo; the record waits for back-office validation.

Request. Body:
interface CreateProspectRequest {
id: Uuid; // client-generated UUIDv7
storeName: string; // 2–120
ownerName: string; // 2–120
phone: string; // main phone — unique after normalisation
whatsapp?: string | null;
email?: string | null;
wilayaCode: string; // "31"
communeId: Uuid; // must belong to the wilaya
addressLine: string; // 3–200
landmark?: string | null; // ≤ 120
location: GeoPoint | null; // required unless locationUnavailableReason is given
locationUnavailableReason?: "PERMISSION_DENIED" | "NO_FIX" | null;
businessTypeCode: string; // BUSINESS_TYPE item
potential: "LOW" | "MEDIUM" | "HIGH";
customerType?: CustomerType | null; // proposed pricing tier, confirmed at validation
facadePhotoFileId?: Uuid | null; // completed upload, purpose STORE_PHOTO
status: "NEW_PROSPECT" | "TO_CONTACT" | "INTERESTED";
note?: string | null; // ≤ 1000 — first entry of the timeline
visitId?: Uuid | null; // the visit during which the store was registered, if any
occurredAt: Iso;
}{
"id": "0193a31c-8e2f-7a40-b1c2-d3e4f5a6b342",
"storeName": "Phone Center Nour",
"ownerName": "Mohamed Benali",
"phone": "0558 12 34 56",
"whatsapp": "0558 12 34 56",
"email": null,
"wilayaCode": "31",
"communeId": "0192b7c0-31aa-7a00-8b00-000000310004",
"addressLine": "Hai Sabah, rue des Frères Benaouda",
"landmark": null,
"location": { "lat": 35.7071, "lng": -0.5802, "accuracy": 8, "capturedAt": "2026-11-03T09:03:40Z" },
"businessTypeCode": "PHONE_SHOP",
"potential": "MEDIUM",
"customerType": "DETAIL",
"facadePhotoFileId": "0193a31c-6d7e-7f80-9a91-b2c3d4e5f601",
"status": "NEW_PROSPECT",
"note": "Boutique ouverte il y a 3 mois, vend surtout des coques et des câbles sans marque.",
"visitId": null,
"occurredAt": "2026-11-03T09:05:00Z"
}Response 201 Created (200 OK when the same id already exists with the same content) — the ResellerSheet.
{
"id": "0193a31c-8e2f-7a40-b1c2-d3e4f5a6b342",
"number": "REV-000342",
"storeName": "Phone Center Nour",
"ownerName": "Mohamed Benali",
"phone": "+213558123456",
"wilayaCode": "31",
"communeName": "Bir El Djir",
"status": "NEW_PROSPECT",
"location": { "lat": 35.7071, "lng": -0.5802, "accuracy": 8, "capturedAt": "2026-11-03T09:03:40Z" },
"addressLine": "Hai Sabah, rue des Frères Benaouda",
"landmark": null,
"whatsapp": "+213558123456",
"businessTypeCode": "PHONE_SHOP",
"potential": "MEDIUM",
"customerType": "DETAIL",
"hasAccount": false,
"assignedToMe": true,
"facadeThumbnailUrl": "https://files.hoco.dz/store-photos/0193a31c-b342-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=e5c9",
"lastVisitAt": null,
"email": null,
"wilayaName": "Oran",
"communeId": "0192b7c0-31aa-7a00-8b00-000000310004",
"locationSource": "FIELD_SIGNUP",
"locationCandidate": null,
"validationStatus": "PENDING",
"customerTypeConfirmed": false,
"source": "DELEGATE_APP",
"accountStatus": null,
"assignedDelegate": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
"createdBy": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
"createdAt": "2026-11-03T09:05:00Z",
"zone": { "id": "0192e8c4-1f2a-7d3b-8c4d-2e1f0a9b8c01", "name": "Oran" },
"facadePhoto": { "id": "0193a31c-6d7e-7f80-9a91-b2c3d4e5f601", "url": "https://files.hoco.dz/store-photos/0193a31c-b342.webp?X-Amz-Expires=300&X-Amz-Signature=72fa", "thumbnailUrl": "https://files.hoco.dz/store-photos/0193a31c-b342-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=e5c9", "mimeType": "image/webp", "size": 191230, "purpose": "STORE_PHOTO" },
"photos": [
{ "id": "0193a31c-6d7e-7f80-9a91-b2c3d4e5f6a1", "fileId": "0193a31c-6d7e-7f80-9a91-b2c3d4e5f601", "kind": "FACADE", "url": "https://files.hoco.dz/store-photos/0193a31c-b342.webp?X-Amz-Expires=300&X-Amz-Signature=72fa", "thumbnailUrl": "https://files.hoco.dz/store-photos/0193a31c-b342-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=e5c9", "takenAt": "2026-11-03T09:03:12Z", "location": { "lat": 35.7071, "lng": -0.5802, "accuracy": 8 }, "caption": null, "isCurrentFacade": true, "uploadedBy": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" }, "createdAt": "2026-11-03T09:05:01Z" }
],
"lastVisits": [],
"ordersSummary": { "count": 0, "lastOrderAt": null, "totalTtc90d": 0 },
"openTasks": [],
"recentInteractions": [
{ "id": "0193a31c-8e30-7a41-b1c2-d3e4f5a6b3a1", "type": "NOTE", "body": "Boutique ouverte il y a 3 mois, vend surtout des coques et des câbles sans marque.", "occurredAt": "2026-11-03T09:05:00Z", "author": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" }, "ref": null }
],
"invitation": null,
"allowedStatusTransitions": ["TO_CONTACT", "INTERESTED", "NOT_INTERESTED", "TO_FOLLOW_UP"],
"editableFields": ["storeName", "ownerName", "phone", "whatsapp", "email", "wilayaCode", "communeId", "addressLine", "landmark", "location", "businessTypeCode", "potential", "customerType", "status"],
"scopeClause": "ASSIGNED",
"version": 1,
"updatedAt": "2026-11-03T09:05:01Z"
}Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Required field missing or too long; commune not in the wilaya (COMMUNE_NOT_IN_WILAYA); unknown business type; file not a STORE_PHOTO of the caller |
| 409 | PHONE_ALREADY_EXISTS | The normalised main phone exists (BR-04). details: { phone, maskedPhone, existing: ResellerSummary | null, assignedToMe: boolean } — existing is filled only when that record is in the caller's scope |
| 409 | CLIENT_ID_CONFLICT | id already used by another record |
| 422 | LOCATION_REQUIRED | location is null and no locationUnavailableReason given |
| 422 | GPS_ACCURACY_TOO_LOW | location.accuracy above field.gpsMaxAccuracyMeters (details: { accuracy, maxAccuracyMeters }) — the store's position is the reference for future check-ins |
| 422 | FILE_NOT_UPLOADED | facadePhotoFileId upload not completed |
{
"error": {
"code": "PHONE_ALREADY_EXISTS",
"message": "Ce numéro est déjà enregistré : Electro Plus (REV-000287).",
"details": {
"phone": "+213661457812",
"maskedPhone": "06•• •• •• 12",
"existing": {
"id": "0192d1aa-3b4c-7d5e-8f60-718293a4b287",
"number": "REV-000287",
"storeName": "Electro Plus",
"ownerName": "Sofiane Medjahed",
"phone": "+213661457812",
"wilayaCode": "31",
"communeName": "Bir El Djir",
"status": "INTERESTED"
},
"assignedToMe": true
},
"requestId": "req_01JBZ4N6P3"
}
}Rules & side effects
- Server-set fields:
number(REV-sequence),source = DELEGATE_APP,createdByDelegateId= caller,assignedDelegateId= caller (BR-47),zone= the caller's zone covering the commune (or the zone of the commune when it lies outside the caller's zones — the record is then flagged « hors zone » for the validator),validationStatus = PENDING,locationSource = FIELD_SIGNUP(lifecycles). - Customer type (AUTH-11):
customerTypeis only a proposal (customerTypeConfirmed = false); the validator confirms or changes it inadmin.resellers.validate. Until then, assisted-order prices use the proposed type (orDETAILwhen none) with a warning. - Façade photo: the form requires it (cahier §6.5, FLD-11), but the API accepts a record without
facadePhotoFileIdso that a slow upload never blocks the prospect; the photo then follows throughdelegate.resellers.addPhoto(kind = FACADE,dependsOnthe creation), and the validation queue shows « photo de façade manquante » until it arrives. - Without a position (
locationUnavailableReason), the record is created withlocation = nulland flagged for the validator; the first check-in proposes a position candidate (check-in algorithm). notebecomes anInteractionNOTE;visitIdlinks the creation to the visit for reports.- Outbox
reseller.created→ SSEaccount.pendingto validators (BO-13); counts toward theSIGNUPSobjective and the home « Nouveaux prospects ». The record appears in the CRM with its source, delegate and position (cahier §6.5 acceptance criterion).
Used by DA-14.
GET /delegate/resellers/{resellerId}
delegate.resellers.get · Access session, delegate scope (clauses a–c) · Apps DA · Idempotency-Key — · Offline-capable no — today's stops are in the snapshot; other sheets show the cached card offline · Release V1 · Requirements CRM-01, CRM-07
Purpose. « Fiche point de vente »: contact actions (call, WhatsApp, itinerary), status, recent visits, orders summary, open tasks, photos, timeline and invitation state.
Request. Path resellerId.
Response 200 OK — ResellerSheet.
{
"id": "0192d1aa-3b4c-7d5e-8f60-718293a4b287",
"number": "REV-000287",
"storeName": "Electro Plus",
"ownerName": "Sofiane Medjahed",
"phone": "+213661457812",
"wilayaCode": "31",
"communeName": "Bir El Djir",
"status": "INTERESTED",
"location": { "lat": 35.7236, "lng": -0.5513 },
"addressLine": "Cité 200 logements, bloc C",
"landmark": null,
"whatsapp": "+213661457812",
"businessTypeCode": "ELECTRONICS_STORE",
"potential": "MEDIUM",
"customerType": "DETAIL",
"hasAccount": false,
"assignedToMe": true,
"facadeThumbnailUrl": "https://files.hoco.dz/store-photos/0192d1aa-b287-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=41d0",
"lastVisitAt": "2026-11-03T09:58:00Z",
"email": "electroplus.bej@gmail.com",
"wilayaName": "Oran",
"communeId": "0192b7c0-31aa-7a00-8b00-000000310004",
"locationSource": "FIELD_SIGNUP",
"locationCandidate": null,
"validationStatus": "APPROVED",
"customerTypeConfirmed": true,
"source": "DELEGATE_APP",
"accountStatus": null,
"assignedDelegate": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
"createdBy": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
"createdAt": "2026-10-27T10:02:00Z",
"zone": { "id": "0192e8c4-1f2a-7d3b-8c4d-2e1f0a9b8c01", "name": "Oran" },
"facadePhoto": { "id": "0192d1b0-7a1b-7c2d-8e3f-4a5b6c7d8e02", "url": "https://files.hoco.dz/store-photos/0192d1aa-b287.webp?X-Amz-Expires=300&X-Amz-Signature=5d21", "thumbnailUrl": "https://files.hoco.dz/store-photos/0192d1aa-b287-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=41d0", "mimeType": "image/webp", "size": 201554, "purpose": "STORE_PHOTO" },
"photos": [
{ "id": "0192d1b0-7a1b-7c2d-8e3f-4a5b6c7d8ea2", "fileId": "0192d1b0-7a1b-7c2d-8e3f-4a5b6c7d8e02", "kind": "FACADE", "url": "https://files.hoco.dz/store-photos/0192d1aa-b287.webp?X-Amz-Expires=300&X-Amz-Signature=5d21", "thumbnailUrl": "https://files.hoco.dz/store-photos/0192d1aa-b287-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=41d0", "takenAt": "2026-10-27T10:00:30Z", "location": { "lat": 35.7235, "lng": -0.5512, "accuracy": 7 }, "caption": null, "isCurrentFacade": true, "uploadedBy": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" }, "createdAt": "2026-10-27T10:02:05Z" }
],
"lastVisits": [
{ "visitId": "0193a2f0-5b6c-7e8d-9f01-23456789ab12", "number": "VIS-2026-001232", "endedAt": "2026-11-03T09:58:00Z", "interestLevel": "HIGH", "nextAction": "WHATSAPP_MESSAGE", "by": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" } },
{ "visitId": "0193897b-2c3d-7e4f-9a50-6b7c8d9e0f31", "number": "VIS-2026-001108", "endedAt": "2026-10-27T10:15:00Z", "interestLevel": "MEDIUM", "nextAction": "NEW_VISIT", "by": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" } }
],
"ordersSummary": { "count": 0, "lastOrderAt": null, "totalTtc90d": 0 },
"openTasks": [
{ "id": "0193a330-1a2b-7c3d-8e4f-5a6b7c8d9e51", "type": "WHATSAPP", "status": "OPEN", "title": "Message WhatsApp — Electro Plus", "dueAt": "2026-11-04T09:00:00Z", "isOverdue": false, "reseller": { "id": "0192d1aa-3b4c-7d5e-8f60-718293a4b287", "number": "REV-000287", "storeName": "Electro Plus", "ownerName": "Sofiane Medjahed", "phone": "+213661457812", "wilayaCode": "31", "communeName": "Bir El Djir", "status": "INTERESTED" }, "sourceVisitId": "0193a2f0-5b6c-7e8d-9f01-23456789ab12", "note": "Vérifier l'installation de l'application et la création du compte.", "createdBy": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" }, "createdAt": "2026-11-03T09:52:10Z", "completedAt": null, "outcome": null, "version": 1 }
],
"recentInteractions": [
{ "id": "0193a333-0b1c-7d2e-8f30-4a5b6c7d8e61", "type": "VISIT", "body": "Visite de prospection — intérêt élevé. Présentation des écouteurs M1 Pro et du pack chargeur + câble.", "occurredAt": "2026-11-03T09:58:00Z", "author": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" }, "ref": { "kind": "VISIT", "id": "0193a2f0-5b6c-7e8d-9f01-23456789ab12", "number": "VIS-2026-001232" } },
{ "id": "0193a333-0b1c-7d2e-8f30-4a5b6c7d8e60", "type": "STATUS_CHANGE", "body": "Intéressé (rapport de visite)", "occurredAt": "2026-10-27T10:15:00Z", "author": null, "ref": { "kind": "VISIT", "id": "0193897b-2c3d-7e4f-9a50-6b7c8d9e0f31", "number": "VIS-2026-001108" } }
],
"invitation": { "id": "0193a325-5f60-7a71-8b82-9c0d1e2f3a71", "displayCode": "K7MQ-4R9T", "active": true, "currentStep": "SENT" },
"allowedStatusTransitions": ["TO_CONTACT", "NOT_INTERESTED", "TO_FOLLOW_UP"],
"editableFields": ["storeName", "ownerName", "phone", "whatsapp", "email", "wilayaCode", "communeId", "addressLine", "landmark", "businessTypeCode", "potential", "status"],
"scopeClause": "ASSIGNED",
"version": 4,
"updatedAt": "2026-11-03T09:58:01Z"
}Field notes. editableFields and allowedStatusTransitions are computed for the caller (here location is not editable because the record is validated, and customerType because it was confirmed). Signed URLs last 5 minutes; the app refreshes the sheet rather than the URL. The « Commandes » block shows ordersSummary and links to delegate.orders.list filtered on the reseller.
Errors — 404 NOT_FOUND (never in scope) · 403 OUT_OF_SCOPE (was in scope, reassigned since — details.assignedTo is not revealed).
Rules & side effects. Read-only.
Used by DA-09.
PATCH /delegate/resellers/{resellerId}
delegate.resellers.update · Access session, delegate scope (clauses a–c) · Apps DA · Idempotency-Key required · Offline-capable yes — queued with version (optimistic lock) and occurredAt; a concurrent back-office edit comes back as a CONFLICT · Release V1 · Requirements CRM-01, CRM-04
Purpose. « Modifier la fiche »: correct the store's data and move its qualification status.
Request. Body — omitted fields are unchanged, null clears an optional field:
interface UpdateResellerRequest {
version: number; // ResellerSheet.version the edit is based on
storeName?: string;
ownerName?: string;
phone?: string; // only while the record has no account
whatsapp?: string | null;
email?: string | null;
wilayaCode?: string; // with communeId
communeId?: Uuid;
addressLine?: string;
landmark?: string | null;
location?: GeoPoint; // see rule 3
businessTypeCode?: string;
potential?: "LOW" | "MEDIUM" | "HIGH";
customerType?: CustomerType; // proposal, only while validationStatus = PENDING
status?: "TO_CONTACT" | "INTERESTED" | "NOT_INTERESTED" | "TO_FOLLOW_UP";
statusReason?: string | null; // 3–300, required for NOT_INTERESTED
followUp?: { type: "CALL" | "VISIT" | "WHATSAPP" | "SEND_CATALOG" | "OTHER"; dueAt: Iso; note?: string | null } | null; // required for TO_FOLLOW_UP
occurredAt: Iso;
}{
"version": 4,
"landmark": "Au-dessus de la pharmacie Ennour",
"potential": "HIGH",
"status": "TO_FOLLOW_UP",
"followUp": { "type": "CALL", "dueAt": "2026-11-06T09:00:00Z", "note": "Rappeler après l'installation de l'application." },
"occurredAt": "2026-11-03T15:20:00Z"
}Response 200 OK — the updated ResellerSheet (same shape as delegate.resellers.get; version incremented to 5, status TO_FOLLOW_UP, the new task in openTasks).
Errors
| Status | Code | When |
|---|---|---|
| 403 | OUT_OF_SCOPE | The record left the caller's scope after the edit was made offline (reassigned) |
| 409 | VERSION_CONFLICT | version is not the current one — details: { currentVersion, current: ResellerSheet } |
| 409 | PHONE_ALREADY_EXISTS | New phone already used (details as in creation) |
| 409 | RESELLER_TRANSITION_NOT_ALLOWED | Target status not allowed from the current one (details: { from, to, allowed, reason } — reason = "AUTOMATIC_STATUS" when the record reached a funnel status meanwhile, e.g. the prospect installed the app) |
| 403 | FIELD_NOT_EDITABLE | details.fields[] with { field, reason }: phone (HAS_ACCOUNT), customerType (CONFIRMED_AT_VALIDATION), location (VALIDATED_LOCATION) |
| 422 | REASON_REQUIRED | NOT_INTERESTED without statusReason, or TO_FOLLOW_UP without followUp |
| 422 | GPS_ACCURACY_TOO_LOW | New location less precise than field.gpsMaxAccuracyMeters |
Rules & side effects
- Edits apply only to the fields listed in
editableFields;versionincrements on any change of an editable field (status automation and counters do not increment it, so they never cause a false conflict). - Status (lifecycles — manual transitions): only between qualification statuses (
NEW_PROSPECT,TO_CONTACT,INTERESTED,NOT_INTERESTED,TO_FOLLOW_UP→TO_CONTACT,INTERESTED,NOT_INTERESTED,TO_FOLLOW_UP) and only when the current status is one of them; funnel statuses andSUSPENDEDare never set by a delegate.TO_FOLLOW_UPcreates theCrmTaskfromfollowUp;NOT_INTERESTEDcancels the record's open tasks. Each change writesResellerStatusChange(triggerMANUAL, appDELEGATE_APP), anInteractionSTATUS_CHANGE, and outboxreseller.status_changed. - Location (anti-fraud — the store's position is the reference of every check-in): a delegate may set it only while it is
null, or while the record is stillPENDINGvalidation and was created by them. Otherwise403 FIELD_NOT_EDITABLE; a moved store is reported with a note, and the next check-in proposes a candidate the back-office validates. - Phone: changeable only while the record has no account (the owner changes it later through the reseller app's OTP flow); uniqueness re-checked.
- Every change is audited (
AuditLog, before/after); outboxreseller.updatedrefreshes the back-office 360° page (BO-11) and reaches other devices throughsync.delta.
POST /delegate/resellers/{resellerId}/notes
delegate.resellers.addNote · Access session, delegate scope (clauses a–c) · Apps DA · Idempotency-Key required · Offline-capable yes — client-generated id, queued · Release V1 · Requirements CRM-07
Purpose. Add a note or log a call, a WhatsApp exchange or an SMS to the store's timeline (cahier §4.2 « Historique des appels, visites, notes… »).
Request. Body:
interface AddNoteRequest {
id: Uuid; // client-generated UUIDv7
type: "NOTE" | "CALL" | "WHATSAPP" | "SMS";
body: string; // 1–2000
durationSeconds?: number | null; // CALL only
occurredAt: Iso;
}{
"id": "0193a3a0-2b3c-7d4e-8f50-6a7b8c9d0e71",
"type": "CALL",
"body": "Appel au gérant : il a installé l'application, création du compte prévue ce soir.",
"durationSeconds": 142,
"occurredAt": "2026-11-03T15:31:00Z"
}Response 201 Created (200 OK on the same id) — InteractionView.
{
"id": "0193a3a0-2b3c-7d4e-8f50-6a7b8c9d0e71",
"type": "CALL",
"body": "Appel au gérant : il a installé l'application, création du compte prévue ce soir.",
"occurredAt": "2026-11-03T15:31:00Z",
"author": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
"ref": null
}Errors — 403 OUT_OF_SCOPE (reassigned since) · 409 CLIENT_ID_CONFLICT.
Rules & side effects. Creates an Interaction (immutable — corrections are new notes); outbox interaction.created (timeline of BO-11). Notes never change the status.
Used by DA-09, DA-27 (saving the content of a conflicted visit as a note).
POST /delegate/resellers/{resellerId}/photos
delegate.resellers.addPhoto · Access session, delegate scope (clauses a–c) · Apps DA · Idempotency-Key required · Offline-capable yes — upload queue first (files.createUpload → PUT → files.completeUpload), then this call with the known fileId, dependsOn the creation when the store was created offline · Release V1 · Requirements CRM-10
Purpose. Attach a façade, interior or shelf photo to the store (cahier §15.5 « Photos »).
Request. Body:
interface AddResellerPhotoRequest {
id: Uuid; // client-generated UUIDv7
fileId: Uuid; // completed upload, purpose STORE_PHOTO, by the caller
kind: "FACADE" | "INTERIOR" | "SHELF" | "OTHER";
takenAt: Iso;
location: GeoPoint | null;
caption?: string | null; // ≤ 140
occurredAt: Iso;
}{
"id": "0193a3a4-7c8d-7e9f-8a01-2b3c4d5e6f81",
"fileId": "0193a3a4-1d2e-7f30-9a41-5b6c7d8e9f02",
"kind": "INTERIOR",
"takenAt": "2026-11-03T09:40:12Z",
"location": { "lat": 35.72362, "lng": -0.55128, "accuracy": 6, "capturedAt": "2026-11-03T09:40:12Z" },
"caption": "Vitrine accessoires — aucune marque HOCO",
"occurredAt": "2026-11-03T09:40:20Z"
}Response 201 Created — ResellerPhotoView.
{
"id": "0193a3a4-7c8d-7e9f-8a01-2b3c4d5e6f81",
"fileId": "0193a3a4-1d2e-7f30-9a41-5b6c7d8e9f02",
"kind": "INTERIOR",
"url": "https://files.hoco.dz/store-photos/0193a3a4-1d2e.webp?X-Amz-Expires=300&X-Amz-Signature=b0c3",
"thumbnailUrl": "https://files.hoco.dz/store-photos/0193a3a4-1d2e-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=19ed",
"takenAt": "2026-11-03T09:40:12Z",
"location": { "lat": 35.72362, "lng": -0.55128, "accuracy": 6, "capturedAt": "2026-11-03T09:40:12Z" },
"caption": "Vitrine accessoires — aucune marque HOCO",
"isCurrentFacade": false,
"uploadedBy": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
"createdAt": "2026-11-03T09:40:22Z"
}Errors — 400 VALIDATION_FAILED (wrong file purpose or owner) · 403 OUT_OF_SCOPE · 409 CLIENT_ID_CONFLICT · 422 FILE_NOT_UPLOADED · 422 PHOTO_LIMIT_REACHED (50 photos per store; details.max).
Rules & side effects. Stored as ResellerPhoto (private, signed reads — cahier §10 « Protection des documents »). A FACADE photo becomes the current façade (isCurrentFacade) and the previous one stays in the history. Outbox reseller.photo_added.
POST /delegate/resellers/{resellerId}/invitations
delegate.invitations.create · Access session, delegate scope (clauses a–c) · Apps DA · Idempotency-Key required · Offline-capable no — the code and the signed link are generated by the server; offline, the screen shows « Connexion requise pour générer l'invitation » (a valid invitation already cached is shown offline) · Release V1 · Requirements IVT-01, IVT-02
Purpose. « Inviter un revendeur »: create the store's invitation — or return the active one — with its code, link, QR payload and ready-to-send texts (cahier §6.6).

Request. Body { "occurredAt": Iso }.
{ "occurredAt": "2026-11-03T09:44:00Z" }Response 201 Created (new invitation) or 200 OK (the active one returned) — InvitationView.
interface InvitationView {
id: Uuid;
code: string; // 8 chars: A–Z without I, L, O, plus 0–9 (BR-03)
displayCode: string; // "K7MQ-4R9T"
url: string; // https://app.hoco.dz/i/<code>
qrPayload: string; // = url — the app renders the QR (qr_flutter)
shareTexts: { whatsapp: string; sms: string }; // French, contain the link and the code
reseller: ResellerSummary;
delegate: UserSummary;
active: boolean;
createdAt: Iso;
expiresAt: Iso;
steps: Array<{ step: "SENT" | "APP_INSTALLED" | "ACCOUNT_CREATED" | "PHONE_VERIFIED" | "CATALOG_VIEWED" | "FIRST_ORDER"; at: Iso | null; inferred: boolean }>;
currentStep: string | null; // last step reached
shares: Array<{ channel: "QR" | "WHATSAPP" | "SMS" | "LINK"; at: Iso }>;
}{
"id": "0193a325-5f60-7a71-8b82-9c0d1e2f3a71",
"code": "K7MQ4R9T",
"displayCode": "K7MQ-4R9T",
"url": "https://app.hoco.dz/i/K7MQ4R9T",
"qrPayload": "https://app.hoco.dz/i/K7MQ4R9T",
"shareTexts": {
"whatsapp": "Bonjour Sofiane, c'est Yacine de HOCO. Installez l'application revendeur HOCO pour voir vos prix et commander HOCO, ACEFAST et BOROFONE en direct : https://app.hoco.dz/i/K7MQ4R9T — Votre code de parrainage : K7MQ-4R9T.",
"sms": "HOCO : installez l'appli revendeur et commandez en direct : https://app.hoco.dz/i/K7MQ4R9T Code : K7MQ-4R9T"
},
"reseller": {
"id": "0192d1aa-3b4c-7d5e-8f60-718293a4b287",
"number": "REV-000287",
"storeName": "Electro Plus",
"ownerName": "Sofiane Medjahed",
"phone": "+213661457812",
"wilayaCode": "31",
"communeName": "Bir El Djir",
"status": "INTERESTED"
},
"delegate": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
"active": true,
"createdAt": "2026-11-03T09:44:01Z",
"expiresAt": "2026-12-03T09:44:01Z",
"steps": [
{ "step": "SENT", "at": null, "inferred": false },
{ "step": "APP_INSTALLED", "at": null, "inferred": false },
{ "step": "ACCOUNT_CREATED", "at": null, "inferred": false },
{ "step": "PHONE_VERIFIED", "at": null, "inferred": false },
{ "step": "CATALOG_VIEWED", "at": null, "inferred": false },
{ "step": "FIRST_ORDER", "at": null, "inferred": false }
],
"currentStep": null,
"shares": []
}Field notes. The reference screen shows a 12-character code (« AB12-CD34-EF56 ») and a app.delegate.com link; the API uses the platform convention — 8 characters displayed XXXX-XXXX, link https://app.hoco.dz/i/<code> served by invitations.landing. The « Copier le lien » and « Copier » buttons copy url and displayCode. The SMS text is GSM-7 only (fits one SMS).
Errors
| Status | Code | When |
|---|---|---|
| 403 | OUT_OF_SCOPE | Record reassigned since |
| 403 | RESELLER_SUSPENDED | ResellerStatus SUSPENDED |
| 409 | RESELLER_HAS_ACCOUNT | The store already has a reseller account (details.accountCreatedAt) — nothing to invite |
Rules & side effects
- At most one active invitation per store (lifecycles): the active one (not expired, not revoked) is returned with
200; when it belongs to a previous delegate of the store, it is revoked and a new one is created for the caller, so the sign-up is attributed to the delegate who works the store now. - Code (BR-03): 8 random characters from
A–ZwithoutI,L,O, plus0–9, unique; typed input is normalised (Oread as0,I/Las1); the link carries no personal data. Validityinvitations.validityDays(default 30). - Creation does not set
SENT: the step is recorded when the delegate shares (markShared). Outboxinvitation.created.
Used by DA-16.
GET /delegate/invitations/{invitationId}
delegate.invitations.get · Access session, delegate scope (invitations of the caller, or of a store in scope) · Apps DA · Idempotency-Key — · Offline-capable no — the last state is cached · Release V1 · Requirements IVT-03
Purpose. « Suivi d'installation »: the activation steps, updated automatically (cahier §6.6 « Les étapes doivent être mises à jour automatiquement »).
Request. Path invitationId.
Response 200 OK — InvitationView. Example in the afternoon, after the owner installed the app from the WhatsApp link:
{
"id": "0193a325-5f60-7a71-8b82-9c0d1e2f3a71",
"code": "K7MQ4R9T",
"displayCode": "K7MQ-4R9T",
"url": "https://app.hoco.dz/i/K7MQ4R9T",
"qrPayload": "https://app.hoco.dz/i/K7MQ4R9T",
"shareTexts": {
"whatsapp": "Bonjour Sofiane, c'est Yacine de HOCO. Installez l'application revendeur HOCO pour voir vos prix et commander HOCO, ACEFAST et BOROFONE en direct : https://app.hoco.dz/i/K7MQ4R9T — Votre code de parrainage : K7MQ-4R9T.",
"sms": "HOCO : installez l'appli revendeur et commandez en direct : https://app.hoco.dz/i/K7MQ4R9T Code : K7MQ-4R9T"
},
"reseller": {
"id": "0192d1aa-3b4c-7d5e-8f60-718293a4b287",
"number": "REV-000287",
"storeName": "Electro Plus",
"ownerName": "Sofiane Medjahed",
"phone": "+213661457812",
"wilayaCode": "31",
"communeName": "Bir El Djir",
"status": "APP_INSTALLED"
},
"delegate": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
"active": true,
"createdAt": "2026-11-03T09:44:01Z",
"expiresAt": "2026-12-03T09:44:01Z",
"steps": [
{ "step": "SENT", "at": "2026-11-03T09:46:00Z", "inferred": false },
{ "step": "APP_INSTALLED", "at": "2026-11-03T14:10:23Z", "inferred": false },
{ "step": "ACCOUNT_CREATED", "at": null, "inferred": false },
{ "step": "PHONE_VERIFIED", "at": null, "inferred": false },
{ "step": "CATALOG_VIEWED", "at": null, "inferred": false },
{ "step": "FIRST_ORDER", "at": null, "inferred": false }
],
"currentStep": "APP_INSTALLED",
"shares": [
{ "channel": "QR", "at": "2026-11-03T09:45:10Z" },
{ "channel": "WHATSAPP", "at": "2026-11-03T09:46:00Z" }
]
}Field notes. Steps are listed in funnel order; ACCOUNT_CREATED and PHONE_VERIFIED are recorded together (an account cannot exist without the OTP). A later step back-fills earlier ones with inferred = true (a store that installed from the Play Store and typed the code at sign-up gets APP_INSTALLED inferred). The reference's « Complété / En attente » labels map to at set / null.
Errors — 404 NOT_FOUND.
Rules & side effects. Read-only. Each step reached creates the invitation.step_completed notification for the delegate (in-app for every step, push for ACCOUNT_CREATED and FIRST_ORDER — notifications); while the app is open the matching notification.created arrives on the live stream (events.stream) and the screen refetches without polling.
Used by DA-16.
POST /delegate/invitations/{invitationId}/shared
delegate.invitations.markShared · Access session, delegate scope (caller's invitation) · Apps DA · Idempotency-Key required · Offline-capable yes — a share made offline (QR shown, SMS sent) is queued with occurredAt · Release V1 · Requirements IVT-02
Purpose. Record that the invitation was shared and through which channel — the app calls it when the QR is shown full screen, when the WhatsApp or SMS intent returns, and when the link or the code is copied.
Request. Body { "channel": "QR" | "WHATSAPP" | "SMS" | "LINK", "occurredAt": Iso }.
{ "channel": "WHATSAPP", "occurredAt": "2026-11-03T09:46:00Z" }Response 200 OK — InvitationView with the share appended and SENT set at the first share (see the example of delegate.invitations.get).
Errors — 404 NOT_FOUND · 409 INVITATION_INACTIVE (expired or revoked — create a new one; details.expiredAt).
Rules & side effects. Appends an InvitationEvent share entry; the first share sets the step SENT (at = its occurredAt); repeated shares only add entries. Outbox invitation.shared (funnel report admin.invitations.list). Delivery of WhatsApp and SMS messages happens on the delegate's phone (share intents); the platform does not send them in V1.
Used by DA-16.
GET /delegate/tasks
delegate.tasks.list · Access session — tasks assigned to the caller · Apps DA · Idempotency-Key — · Offline-capable no — open tasks are kept locally through sync.delta · Release V1 · Requirements CRM-08
Purpose. « Relances »: the caller's follow-ups — due today, overdue, upcoming (cahier §6.8 « Planifier une prochaine action et une date de relance »).
Request. Query parameters: bucket (TODAY · OVERDUE · UPCOMING; default all open), status (OPEN · DONE · CANCELLED, repeatable; default OPEN), resellerId, type (repeatable TaskType), cursor, limit. Buckets use Africa/Algiers days: TODAY = dueAt within today, OVERDUE = OPEN and dueAt before now, UPCOMING = after today. Sorted by dueAt ascending.
Response 200 OK — cursor list of TaskView plus counters for the tabs.
{
"items": [
{
"id": "0193a0c1-8e9f-7a10-8b21-3c4d5e6f7a91",
"type": "CALL",
"status": "OPEN",
"title": "Relance inactivité — Librairie Moderne",
"dueAt": "2026-11-03T15:00:00Z",
"isOverdue": false,
"reseller": {
"id": "0192d1aa-3b4c-7d5e-8f60-718293a4b198",
"number": "REV-000198",
"storeName": "Librairie Moderne",
"ownerName": "Rachid Ouali",
"phone": "+213662781903",
"wilayaCode": "31",
"communeName": "Oran",
"status": "TO_FOLLOW_UP"
},
"sourceVisitId": null,
"note": "Aucune commande validée depuis 60 jours.",
"createdBy": null,
"createdAt": "2026-11-03T01:00:12Z",
"completedAt": null,
"outcome": null,
"version": 1
},
{
"id": "0193a31b-77aa-7b10-8c21-3d4e5f6a7b81",
"type": "CALL",
"status": "OPEN",
"title": "Relance téléphonique — Magasin El Wafa",
"dueAt": "2026-11-10T08:00:00Z",
"isOverdue": false,
"reseller": {
"id": "0192d1aa-3b4c-7d5e-8f60-718293a4b123",
"number": "REV-000123",
"storeName": "Magasin El Wafa",
"ownerName": "Karim Belkacem",
"phone": "+213555123456",
"wilayaCode": "31",
"communeName": "Bir El Djir",
"status": "ACTIVE"
},
"sourceVisitId": "0193a2f0-5b6c-7e8d-9f01-23456789ab11",
"note": "Intéressé par les écouteurs true wireless. Souhaite recevoir le catalogue HOCO.",
"createdBy": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
"createdAt": "2026-11-03T08:35:02Z",
"completedAt": null,
"outcome": null,
"version": 1
}
],
"nextCursor": null,
"counts": { "today": 1, "overdue": 0, "upcoming": 1 }
}Errors — 400 VALIDATION_FAILED (unknown bucket or type).
Rules & side effects. Read-only. Tasks come from visit reports, the back-office (admin.tasks.create) and the inactivity scan; a task stays visible to its assignee even if the store is reassigned (the store details then show only the summary).
Used by DA-19, DA-05 (followUps counters), DA-09.
PATCH /delegate/tasks/{taskId}
delegate.tasks.update · Access session — assignee only · Apps DA · Idempotency-Key required · Offline-capable yes — queued with version and occurredAt · Release V1 · Requirements CRM-08
Purpose. Reschedule a follow-up or annotate it.
Request. Body { "version": number, "dueAt"?: Iso, "note"?: string | null, "occurredAt": Iso } — dueAt from now to 90 days ahead; note ≤ 500.
{
"version": 1,
"dueAt": "2026-11-09T08:00:00Z",
"note": "Le gérant sera à Alger jusqu'au 8/11.",
"occurredAt": "2026-11-03T16:02:00Z"
}Response 200 OK — TaskView (dueAt and note updated, version 2).
{
"id": "0193a31b-77aa-7b10-8c21-3d4e5f6a7b81",
"type": "CALL",
"status": "OPEN",
"title": "Relance téléphonique — Magasin El Wafa",
"dueAt": "2026-11-09T08:00:00Z",
"isOverdue": false,
"reseller": {
"id": "0192d1aa-3b4c-7d5e-8f60-718293a4b123",
"number": "REV-000123",
"storeName": "Magasin El Wafa",
"ownerName": "Karim Belkacem",
"phone": "+213555123456",
"wilayaCode": "31",
"communeName": "Bir El Djir",
"status": "ACTIVE"
},
"sourceVisitId": "0193a2f0-5b6c-7e8d-9f01-23456789ab11",
"note": "Le gérant sera à Alger jusqu'au 8/11.",
"createdBy": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
"createdAt": "2026-11-03T08:35:02Z",
"completedAt": null,
"outcome": null,
"version": 2
}Errors — 404 NOT_FOUND (not the caller's task) · 409 TASK_NOT_OPEN (task DONE or CANCELLED; details.status) · 409 VERSION_CONFLICT (changed by the back-office; details.current: TaskView).
Rules & side effects. Rescheduling or annotating never changes the status (lifecycles); the due-date reminder moves with dueAt; audited; outbox task.updated.
Used by DA-19.
POST /delegate/tasks/{taskId}/complete
delegate.tasks.complete · Access session — assignee only · Apps DA · Idempotency-Key required · Offline-capable yes — queued with occurredAt · Release V1 · Requirements CRM-08
Purpose. Close a follow-up with its outcome and, optionally, plan the next one in the same gesture.
Request. Body:
interface CompleteTaskRequest {
outcome: string; // 3–500, required
nextTask?: { type: "CALL" | "VISIT" | "WHATSAPP" | "SEND_CATALOG" | "OTHER"; dueAt: Iso; note?: string | null } | null;
occurredAt: Iso;
}{
"outcome": "Commande prévue jeudi ; souhaite une visite pour voir les nouveautés.",
"nextTask": { "type": "VISIT", "dueAt": "2026-11-05T08:00:00Z", "note": "Présenter les nouveautés ACEFAST." },
"occurredAt": "2026-11-03T14:20:00Z"
}Response 200 OK
{
"task": {
"id": "0193a0c1-8e9f-7a10-8b21-3c4d5e6f7a91",
"type": "CALL",
"status": "DONE",
"title": "Relance inactivité — Librairie Moderne",
"dueAt": "2026-11-03T15:00:00Z",
"isOverdue": false,
"reseller": {
"id": "0192d1aa-3b4c-7d5e-8f60-718293a4b198",
"number": "REV-000198",
"storeName": "Librairie Moderne",
"ownerName": "Rachid Ouali",
"phone": "+213662781903",
"wilayaCode": "31",
"communeName": "Oran",
"status": "TO_FOLLOW_UP"
},
"sourceVisitId": null,
"note": "Aucune commande validée depuis 60 jours.",
"createdBy": null,
"createdAt": "2026-11-03T01:00:12Z",
"completedAt": "2026-11-03T14:20:00Z",
"outcome": "Commande prévue jeudi ; souhaite une visite pour voir les nouveautés.",
"version": 2
},
"nextTask": {
"id": "0193a3c8-4d5e-7f60-8a71-9b0c1d2e3f01",
"type": "VISIT",
"status": "OPEN",
"title": "Nouvelle visite — Librairie Moderne",
"dueAt": "2026-11-05T08:00:00Z",
"isOverdue": false,
"reseller": {
"id": "0192d1aa-3b4c-7d5e-8f60-718293a4b198",
"number": "REV-000198",
"storeName": "Librairie Moderne",
"ownerName": "Rachid Ouali",
"phone": "+213662781903",
"wilayaCode": "31",
"communeName": "Oran",
"status": "TO_FOLLOW_UP"
},
"sourceVisitId": null,
"note": "Présenter les nouveautés ACEFAST.",
"createdBy": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
"createdAt": "2026-11-03T14:20:01Z",
"completedAt": null,
"outcome": null,
"version": 1
}
}Errors — 404 NOT_FOUND · 409 TASK_NOT_OPEN · 422 REASON_REQUIRED (outcome missing or shorter than 3 characters; details.field = "outcome").
Rules & side effects
OPEN→DONE;completedAt=occurredAt; anInteractionis written on the timeline:CALL→CALL,WHATSAPP→WHATSAPP,SEND_CATALOGandOTHER→NOTE,VISIT→ none (the visit itself is the interaction).- A
VISITtask of a store is also completed automatically when a visit to that store ends (outcome « Visite réalisée VIS-… »). nextTaskcreates a newOPENtask for the caller; aVISITtask becomes a stop proposal in the planner (BO-21) — here Librairie Moderne appears in the 5 November tour.- Outbox
task.completed(andtask.created); the inactivity statusTO_FOLLOW_UPis left to the order rules (it returns toACTIVEwhen an order is validated).
Used by DA-19.
Tours & visits
Every operation of a delegate's day in the field: the offline snapshot of today's tour, start/suspend/resume, reordering, unplanned visits, GPS check-in, checklist, note, photos, structured report, end, not-done and tour closing.
Assisted orders
The delegate's first-order assistance: a draft priced with the visited reseller's customer-type prices, sent for the reseller's (or the administration's) confirmation, then confirmed by the back-office like every order — plus the read-only orders of the delegate's resellers.