HOCO B2B
SRSAPI referenceBack-office endpoints

Field operations

Back-office field operations — delegates, zones, objectives, tour planning and live follow-up, visits and their unlocking, visit types, checklist templates and the invitation funnel.

These operations plan and measure the field activity of cahier §4.5 (« Planifier l'activité terrain et mesurer les résultats commerciaux »): delegate accounts and zones (wilayas, communes, points of sale), objectives, daily / weekly / monthly tours with imposed or recommended order, live follow-up of starts, visits, positions and closings, analysis of visit reports, and the invitation funnel. Screens: delegates (BO-16 to BO-18), zones (BO-19), objectives (BO-20), tour planner, editor and live detail (BO-21 to BO-23), visits (BO-24, BO-25), visit types and checklists (BO-26) and the field dashboard with its live map (BO-06). Tour and visit statuses are defined on Lifecycles › Tour and Visit; the delegate's side is on tours and visits.

Who uses it. The Administrateur commercial (cahier §3: « délégués, tournées ») plans and follows; the Super administrateur too; the Direction reads performance. Delegates never call these operations.

PermissionUnlocks on this page
field.viewDelegates, performance, zones (also with crm.view), objectives, tours, tracks, live map, visits, visit types, checklists, invitation funnel
field.createCreating delegates, objectives and tours
field.updateEditing delegates, objectives and tours; stops; recommended order; cancelling a tour
field.deleteDeleting objectives
field.validateReopening a closed tour; unlocking a finished visit
field.administerZones, visit types, checklist templates

Settings used (settings keys): field.gpsToleranceMeters (150), field.gpsMaxAccuracyMeters (100), field.allowOutOfToleranceCheckIn (false), field.locationIntervalSeconds (300), field.locationDistanceMeters (200), field.locationRetentionDays (90), field.reportRequiredToEndVisit (true), field.closingRequiresSync (true), field.reopenMaxDays (7), field.visitUnlockHours (24), invitations.validityDays (30). GPS rules: BR-50 to BR-55.

Audit. CREATE / UPDATE / DELETE on delegates, zones, objectives, tours, visit types and templates; STATUS_CHANGE on tour cancellation; OVERRIDE on tour reopening, visit unlocking and stops added outside the delegate's scope. Domain history: TourStatusChange, DelegateZone, ResellerAssignment.

Real time. Tours started, suspended, resumed and closed, check-ins, finished and not-done visits and delegate positions reach the back-office through the SSE stream (tour.*, visit.*, delegate.position, permission field.view), so the live map and the tour detail update without polling (cahier §6.4 « La visite apparaît en temps réel dans le back-office »).

Conventions: lists return Page<T> = { items, total, page, pageSize } (responses); GeoPoint, ResellerSummary, UserSummary, FileRef are shared schemas; distances in metres (haversine, BR-50); dates YYYY-MM-DD and times of day HH:mm in Africa/Algiers; editable records carry version; a missing reason answers 422 REASON_REQUIRED. 401, 429 and 500 apply everywhere and are not repeated.

MethodPathOperationAccessAppsRelease
GET · POST · PATCH/admin/delegates[/{delegateId}]admin.delegates.crud
Delegates (account, code, zones, status)
field.*BOV1
GET/admin/delegates/{delegateId}/performanceadmin.delegates.performance
Delegate KPIs over a period
field.viewBOV1
GET · POST · PATCH · DELETE/admin/zones[/{zoneId}]admin.zones.crud
Zones and their wilayas/communes
field.administerBOV1
GET · POST · PATCH · DELETE/admin/objectives[/{objectiveId}]admin.objectives.crud
Objectives per delegate and period
field.*BOV1
GET/admin/toursadmin.tours.list
Tours by delegate and date range (planner calendar)
field.viewBOV1
POST/admin/toursadmin.tours.create
Plan a tour (or a recurring series) with stops
field.createBOV1
GET/admin/tours/{tourId}admin.tours.get
Tour detail with visits, check-ins, distances, photos, reports and closing summary
field.viewBOV1
PATCH/admin/tours/{tourId}admin.tours.update
Edit date, ordering mode, notes (planned tours only)
field.updateBOV1
PUT/admin/tours/{tourId}/stopsadmin.tours.setStops
Replace the ordered stops (synced to the delegate)
field.updateBOV1
POST/admin/tours/{tourId}/recommend-orderadmin.tours.recommendOrder
Compute a recommended visiting order (nearest neighbour from the start point)
field.updateBOV1
POST/admin/tours/{tourId}/canceladmin.tours.cancel
Cancel a tour with a reason
field.updateBOV1
POST/admin/tours/{tourId}/reopenadmin.tours.reopen
Authorised reopening of a closed tour
field.validateBOV1
GET/admin/tours/{tourId}/trackadmin.tours.track
Position trail of a tour (retention policy applies)
field.viewBOV1
GET/admin/live/delegatesadmin.live.delegates
Delegates currently on tour with last known position and progress
field.viewBOV1
GET/admin/visitsadmin.visits.list
Visits with status, delegate, reseller, outcome filters
field.viewBOV1
GET/admin/visits/{visitId}admin.visits.get
Visit with check-in distance, checklist, photos and report
field.viewBOV1
POST/admin/visits/{visitId}/unlockadmin.visits.unlock
Allow correction of a finished visit (logged)
field.validateBOV1
GET · POST · PATCH/admin/visit-types[/{visitTypeId}]admin.visitTypes.crud
Visit types with photo/report requirements and checklist template
field.administerBOV1
GET · POST · PATCH/admin/checklist-templates[/{templateId}]admin.checklists.crud
Checklist templates
field.administerBOV1
GET/admin/invitationsadmin.invitations.list
Invitation funnel by delegate (sent → installed → account → verified → catalog → first order)
field.viewBOV1

Schemas

Delegate

A delegate is a User of type DELEGATE with role DELEGATE, a DelegateProfile and its DelegateZone rows; delegateId is the user id.

interface Delegate {
  id: Uuid;                          // user id
  code: string;                      // unique, free format (example "D014") — Assumption
  firstName: string;
  lastName: string;
  displayName: string;
  phone: string;                     // login, E.164
  email?: string;
  avatarUrl?: string;
  accountStatus: AccountStatus;
  zones: { id: Uuid; name: string; isPrimary: boolean }[];   // isPrimary: the delegate is the zone's primary delegate (BR-47)
  wilayaCodes: string[];             // derived from the zones
  homeBase?: GeoPoint;               // default start point of recommended orders
  manager?: UserSummary;
  counts: { assignedResellers: number; prospects: number; openTasks: number };
  today?: { tourId: Uuid; number: string; status: TourStatus; visitsDone: number; visitsPlanned: number };
  lastSeenAt?: Iso;                  // last authenticated call from the delegate app
  appVersion?: string;
  locationConsentAt?: Iso;           // BR-53
  createdAt: Iso;
  version: number;
}

Tour

interface TourListItem {
  id: Uuid;
  number: string;                    // TRN-2026-000045
  date: string;                      // YYYY-MM-DD
  delegate: UserSummary;
  status: TourStatus;
  orderingMode: TourOrderingMode;    // IMPOSED | RECOMMENDED
  seriesId?: Uuid;                   // recurring series
  stops: number;
  visitsDone: number;
  visitsNotDone: number;
  plannedKm?: number;                // straight-line legs of the planned order
  km?: number;                       // travelled (BR-54), when started
  startedAt?: Iso;
  completedAt?: Iso;
  late: boolean;                     // derived: TO_START after its date, or IN_PROGRESS / SUSPENDED after its date
  version: number;
}

interface TourStop {                 // one stop = one Visit
  visitId: Uuid;
  visitNumber: string;               // VIS-2026-001234
  sequence: number;                  // 1-based planned order
  reseller: ResellerSummary;
  plannedAt?: string;                // HH:mm local time slot start
  priority: VisitPriority;           // NORMAL | HIGH
  visitType: { id: Uuid; name: string };
  status: VisitStatus;
  late: boolean;                     // derived: PLANNED and its time slot has passed
  checkIn?: { at: Iso; position: GeoPoint; distanceMeters: number | null; withinTolerance: boolean; justification?: string };
  startedAt?: Iso;
  endedAt?: Iso;
  durationMinutes?: number;
  notDone?: { reasonCode: string; comment?: string; at: Iso };
  photosCount: number;
  hasReport: boolean;
  orderId?: Uuid;                    // assisted order prepared during the visit
  addedBy: "PLAN" | "DELEGATE";      // DELEGATE = unplanned visit added on the spot
}

interface TourDetail extends TourListItem {
  notes?: string;
  stopsDetail: TourStop[];
  startPosition?: GeoPoint;
  startPositionMissing?: boolean;    // started without location permission (BR-53)
  history: (StatusChange & { position?: GeoPoint })[];   // TourStatusChange
  closing?: {                        // final report (cahier §6.9)
    comment: string;
    visitsPlanned: number; visitsDone: number; visitsNotDone: number;
    signups: number; appInstalls: number; firstOrders: number; orders: number; revenueHt: Money;
    km: number; conversionRateBps: number;
    objectives: { metric: ObjectiveMetric; target: number; achieved: number }[];
    version: number;                 // a reopened tour keeps previous final reports as versions
  };
  livePosition?: { position: GeoPoint; at: Iso; stale: boolean };
  createdBy: UserSummary;
  createdAt: Iso;
}

Delegates

GET /admin/delegates

admin.delegates.crud (list) · Permission field.view · Apps BO · Release V1 · Requirements FLD-01

Purpose. Lists delegates with their zones, today's tour and counters.

Request

QueryTypeValues / defaultNotes
statusAccountStatus[]repeatable; default ACTIVE, SUSPENDED
zoneIdUuid
wilayaCodestring
onTourTodaybooleanTour of today IN_PROGRESS or SUSPENDED
qstringmin 2 charsName, code, phone
sort / order / page / pageSizelastName (default), code, lastSeenAt · asc · 1 · 25 (max 200)

Response 200 OK

{
  "items": [
    {
      "id": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12", "code": "D014", "firstName": "Yacine", "lastName": "Merabet", "displayName": "Yacine Merabet",
      "phone": "+213770112233", "accountStatus": "ACTIVE",
      "zones": [{ "id": "01928f20-2a3b-7c4d-8e5f-6a7b8c9d0e1f", "name": "Oran Est", "isPrimary": true }],
      "wilayaCodes": ["31"],
      "homeBase": { "lat": 35.6971, "lng": -0.6308 },
      "counts": { "assignedResellers": 64, "prospects": 21, "openTasks": 7 },
      "today": { "tourId": "01930a10-3c4d-7e5f-8a6b-7c8d9e0f1a2b", "number": "TRN-2026-000045", "status": "IN_PROGRESS", "visitsDone": 4, "visitsPlanned": 9 },
      "lastSeenAt": "2026-11-04T10:12:40Z", "appVersion": "1.0.3", "locationConsentAt": "2026-10-28T07:02:00Z",
      "createdAt": "2026-10-26T09:00:00Z", "version": 3
    }
  ],
  "total": 11,
  "page": 1,
  "pageSize": 25
}

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks field.view

Rules & side effects

  1. Read-only.

Used by BO-16 · BO-21 (delegate filter) · BO-04 (filter)

GET /admin/delegates/{delegateId}

admin.delegates.crud (get) · Permission field.view · Apps BO · Release V1 · Requirements FLD-01

Purpose. One delegate (header of BO-17; performance comes from admin.delegates.performance).

Response 200 OK — a Delegate.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks field.view
404NOT_FOUNDUnknown delegate

Rules & side effects

  1. Read-only.

Used by BO-17 · BO-18

POST /admin/delegates

admin.delegates.crud (create) · Permission field.create · Apps BO · Release V1 · Requirements FLD-01

Purpose. Creates a delegate: the login account, the delegate profile and the zones it covers (cahier §4.5 « Création des comptes délégués et affectation des zones »).

Request

interface CreateDelegateBody {
  firstName: string;
  lastName: string;
  phone: string;                     // login; unique among users
  email?: string;
  code: string;                      // unique
  zoneIds: Uuid[];                   // ≥ 1
  primaryZoneIds?: Uuid[];           // zones where this delegate becomes the primary delegate
  homeBase?: GeoPoint;
  managerId?: Uuid;
  sendInvitation: "SMS" | "EMAIL";   // temporary password, change forced at first sign-in
}
{
  "firstName": "Yacine", "lastName": "Merabet", "phone": "0770 11 22 33", "code": "D014",
  "zoneIds": ["01928f20-2a3b-7c4d-8e5f-6a7b8c9d0e1f"], "primaryZoneIds": ["01928f20-2a3b-7c4d-8e5f-6a7b8c9d0e1f"],
  "homeBase": { "lat": 35.6971, "lng": -0.6308 }, "sendInvitation": "SMS"
}

Response 201 Created — the Delegate (accountStatus ACTIVE, phone +213770112233, version 1).

Errors

StatusCodeWhen
400VALIDATION_FAILEDInvalid phone, no zone, primaryZoneIds not a subset of zoneIds
403PERMISSION_DENIEDCaller lacks field.create
404NOT_FOUNDUnknown zone or manager
409PHONE_ALREADY_EXISTS · EMAIL_ALREADY_EXISTSLogin already used
409DUPLICATE_VALUEcode already used

Rules & side effects

  1. Creates User (type DELEGATE, role DELEGATE, mustChangePassword), DelegateProfile and DelegateZone rows in one transaction; a primary-zone flag moves from the zone's previous primary delegate.
  2. Audit CREATE (passwords are never logged).
  3. Notifications: user.invited → the new delegate (SMS with the temporary password and the store link).

Used by BO-18

PATCH /admin/delegates/{delegateId}

admin.delegates.crud (update) · Permission field.update · Apps BO · Release V1 · Requirements FLD-01

Purpose. Edits a delegate's profile and zones, or suspends, reactivates or disables the account.

Request

interface UpdateDelegateBody {
  firstName?: string; lastName?: string; email?: string | null; code?: string;
  zoneIds?: Uuid[];                  // replaces the zones
  primaryZoneIds?: Uuid[];
  homeBase?: GeoPoint | null;
  managerId?: Uuid | null;
  status?: "ACTIVE" | "SUSPENDED" | "DISABLED";   // same rules as admin.users.setStatus
  statusReason?: string;             // required with status
  version: number;
}
{ "zoneIds": ["01928f20-2a3b-7c4d-8e5f-6a7b8c9d0e1f", "01928f21-3b4c-7d5e-9f6a-7b8c9d0e1f2a"], "version": 3 }

Response 200 OK — the updated Delegate.

Errors

StatusCodeWhen
400VALIDATION_FAILEDEmpty zoneIds, status without statusReason
403PERMISSION_DENIEDCaller lacks field.update
404NOT_FOUNDUnknown delegate, zone or manager
409DUPLICATE_VALUEcode taken
409DELEGATE_HAS_OPEN_TOURSDISABLED while the delegate has a tour PLANNED, TO_START, IN_PROGRESS or SUSPENDED — details: { tourIds }; reassign or cancel them first
409VERSION_CONFLICTStale version

Rules & side effects

  1. Zone changes change the delegate's scope at the next sync.delta: resellers of removed zones that are not assigned to the delegate disappear from the app; assigned resellers stay (assignment is explicit, BR-45).
  2. SUSPENDED revokes sessions and moves an IN_PROGRESS tour to SUSPENDED (reason « Compte suspendu ») — AccountStatus.
  3. Audit UPDATE (zones before/after) or STATUS_CHANGE.
  4. Notifications: account.suspended → the delegate on suspension (SMS + in-app).

Used by BO-18 · BO-17

GET /admin/delegates/{delegateId}/performance

admin.delegates.performance · Permission field.view · Apps BO · Release V1 · Requirements FLD-02 · ANA-03

Purpose. A delegate's KPIs over a period, with objectives achievement and a time series — the same definitions as the dashboard (card catalogue) and the delegate's own delegate.performance.

Request

QueryTypeValues / defaultNotes
from / tostring (date)default: current month≤ 366 days
granularitystringDAY | WEEK (default) | MONTHSeries buckets

Response 200 OK

{
  "delegate": { "id": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12", "displayName": "Yacine Merabet" },
  "period": { "from": "2026-11-01", "to": "2026-11-30" },
  "tours": { "planned": 19, "completed": 3, "cancelled": 0 },
  "visits": { "planned": 41, "done": 35, "notDone": 6, "rateBps": 8537, "avgDurationMinutes": 27, "outOfTolerance": 1 },
  "prospects": { "created": 9, "appInstalls": 6, "accountsCreated": 5, "firstOrders": 3, "conversionRateBps": 3333 },
  "orders": { "count": 12, "assisted": 4, "revenueHt": 23480000 },
  "km": 412.6,
  "objectives": [
    { "id": "01930000-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "metric": "VISITS", "period": "MONTH", "target": 180, "achieved": 35, "rateBps": 1944 },
    { "id": "01930000-2b3c-7d4e-9f5a-6b7c8d9e0f1a", "metric": "FIRST_ORDERS", "period": "MONTH", "target": 10, "achieved": 3, "rateBps": 3000 },
    { "id": "01930000-3c4d-7e5f-8a6b-7c8d9e0f1a2b", "metric": "REVENUE", "period": "MONTH", "target": 150000000, "achieved": 23480000, "rateBps": 1565 }
  ],
  "series": [
    { "x": "2026-11-01", "visitsDone": 35, "signups": 5, "firstOrders": 3, "revenueHt": 23480000, "km": 412.6 }
  ],
  "generatedAt": "2026-11-04T10:30:00Z"
}

Field notes. Definitions: visits = visits of the delegate's tours dated in the period (planned excludes CANCELLED); prospects.* = cohort of points of sale created by the delegate in the period and the funnel steps they reached by now; conversionRateBps = firstOrders ÷ created; orders and revenueHt = orders attributed to the delegate at submission (BR-49) validated in the period (HT net of discounts); km per BR-54.

Errors

StatusCodeWhen
400VALIDATION_FAILEDBad dates or granularity
403PERMISSION_DENIEDCaller lacks field.view
404NOT_FOUNDUnknown delegate
422REPORT_RANGE_TOO_LARGEPeriod over 366 days

Rules & side effects

  1. Read-only; served from the analytics aggregates refreshed on field and order events.

Used by BO-17 · BO-06

Zones

A zone groups whole wilayas and/or communes (Zone, ZoneArea); a commune belongs to at most one zone, an explicit commune beats its wilaya (BR-46). Zones are assigned to delegates; each zone may have a primary delegate who receives unassigned sign-ups (BR-47).

interface Zone {
  id: Uuid;
  name: string;                      // unique
  color?: string;                    // #RRGGBB on the map editor
  areas: { wilayaCode: string; wilayaName: string; communeIds: Uuid[] | null }[];   // null = the whole wilaya
  primaryDelegate?: UserSummary;
  delegates: UserSummary[];
  communeCount: number;
  resellerCount: number;
  createdAt: Iso; updatedAt: Iso; version: number;
}

GET /admin/zones

admin.zones.crud (list) · Permission field.view or crm.view · Apps BO · Release V1 · Requirements GEO-02

Purpose. Lists zones (map editor, delegate form, CRM and dashboard filters).

Request

QueryTypeValues / defaultNotes
wilayaCodestringZones covering the wilaya or one of its communes
delegateIdUuid
qstringmin 2 charsName
sort / order / page / pageSizename (default) · asc · 1 · 50 (max 200)

Response 200 OK

{
  "items": [
    {
      "id": "01928f20-2a3b-7c4d-8e5f-6a7b8c9d0e1f", "name": "Oran Est", "color": "#0EA5E9",
      "areas": [{ "wilayaCode": "31", "wilayaName": "Oran", "communeIds": ["0191c0aa-3101-7a00-8c00-000000003107", "0191c0aa-3101-7a00-8c00-000000003112", "0191c0aa-3101-7a00-8c00-000000003119", "0191c0aa-3101-7a00-8c00-000000003124"] }],
      "primaryDelegate": { "id": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12", "displayName": "Yacine Merabet" },
      "delegates": [{ "id": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12", "displayName": "Yacine Merabet" }],
      "communeCount": 4, "resellerCount": 96,
      "createdAt": "2026-10-26T08:30:00Z", "updatedAt": "2026-10-26T08:30:00Z", "version": 1
    }
  ],
  "total": 6,
  "page": 1,
  "pageSize": 50
}

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller has neither field.view nor crm.view

Rules & side effects

  1. Read-only. Reading is relaxed to field.view or crm.view (the bundle's field.administer applies to writes) — Assumption.

Used by BO-19 · BO-18 · BO-09 (filter)

GET /admin/zones/{zoneId}

admin.zones.crud (get) · Permission field.view or crm.view · Apps BO · Release V1 · Requirements GEO-02

Purpose. One zone with its communes (names) for the map editor.

Response 200 OK — a Zone with areas[].communes: [{ id, name }] in addition to the ids.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller has neither field.view nor crm.view
404NOT_FOUNDUnknown zone

Rules & side effects

  1. Read-only.

Used by BO-19

POST /admin/zones

admin.zones.crud (create) · Permission field.administer · Apps BO · Release V1 · Requirements GEO-02

Purpose. Creates a zone from wilayas and communes.

Request

interface CreateZoneBody {
  name: string;
  color?: string;
  areas: { wilayaCode: string; communeIds?: Uuid[] | null }[];   // ≥ 1; omitted/null communeIds = whole wilaya
  delegateIds?: Uuid[];
  primaryDelegateId?: Uuid;
}
{ "name": "Oran Ouest", "color": "#22C55E", "areas": [{ "wilayaCode": "31", "communeIds": ["0191c0aa-3101-7a00-8c00-000000003101", "0191c0aa-3101-7a00-8c00-000000003115"] }, { "wilayaCode": "46" }] }

Response 201 Created — the Zone.

Errors

StatusCodeWhen
400VALIDATION_FAILEDUnknown wilaya, commune of another wilaya, primary delegate not in delegateIds
403PERMISSION_DENIEDCaller lacks field.administer
409DUPLICATE_VALUEName taken
409ZONE_OVERLAPA commune (explicit, or through a whole wilaya) already belongs to another zone — details: { communes: [{ communeId, zoneId }] }

Rules & side effects

  1. Resellers of the covered communes get this zone as their derived zone (unless overridden); assigned delegates do not change. Audit CREATE.

Used by BO-19

PATCH /admin/zones/{zoneId}

admin.zones.crud (update) · Permission field.administer · Apps BO · Release V1 · Requirements GEO-02

Purpose. Renames a zone, changes its areas, delegates or primary delegate.

Request

interface UpdateZoneBody { name?: string; color?: string | null; areas?: CreateZoneBody["areas"]; delegateIds?: Uuid[]; primaryDelegateId?: Uuid | null; version: number }
{ "primaryDelegateId": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12", "version": 1 }

Response 200 OK — the updated Zone plus impact: { resellersRezoned: number } when areas changed.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks field.administer
404NOT_FOUNDUnknown zone or delegate
409DUPLICATE_VALUE · ZONE_OVERLAP · VERSION_CONFLICTAs for creation · stale version

Rules & side effects

  1. Area changes re-derive the zone of the resellers concerned (overrides kept) and change delegates' scopes at the next sync. Audit UPDATE.

Used by BO-19

DELETE /admin/zones/{zoneId}

admin.zones.crud (delete) · Permission field.administer · Apps BO · Release V1 · Requirements GEO-02

Purpose. Deletes a zone that no delegate covers.

Response 204 No Content

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks field.administer
404NOT_FOUNDUnknown zone
409RESOURCE_IN_USEDelegates still cover it, or resellers have it as an explicit (overridden) zone, or targeting references it — details.references

Rules & side effects

  1. Derived zones of resellers become null. Audit DELETE.

Used by BO-19

Objectives

Objectives per delegate and period (cahier §4.5 « Objectifs de visites, inscriptions, installations et commandes », FLD-02). Periods: DAY (that day), WEEK (Sunday to Saturday), MONTH (calendar month), in Africa/Algiers.

interface Objective {
  id: Uuid;
  delegate: UserSummary;
  metric: ObjectiveMetric;           // VISITS, SIGNUPS, APP_INSTALLS, FIRST_ORDERS, ORDERS, REVENUE
  period: ObjectivePeriod;           // DAY, WEEK, MONTH
  periodStart: string;               // YYYY-MM-DD (a Sunday for WEEK, the 1st for MONTH)
  periodEnd: string;
  target: number;                    // count, or Money (HT) for REVENUE
  achieved: number;                  // computed with the performance definitions
  rateBps: number;                   // achieved ÷ target
  closed: boolean;                   // derived: periodEnd has passed
  createdBy: UserSummary;
  createdAt: Iso; updatedAt: Iso; version: number;
}

GET /admin/objectives

admin.objectives.crud (list) · Permission field.view · Apps BO · Release V1 · Requirements FLD-02

Purpose. Lists objectives with their achievement.

Request

QueryTypeValues / defaultNotes
delegateIdUuid[]repeatable
metricObjectiveMetric[]repeatable
periodObjectivePeriod
activeOnstring (date)default todayObjectives whose period contains the date
sort / order / page / pageSizeperiodStart (default), rateBps · desc · 1 · 50 (max 200)

Response 200 OK

{
  "items": [
    { "id": "01930000-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "delegate": { "id": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12", "displayName": "Yacine Merabet" }, "metric": "VISITS", "period": "MONTH", "periodStart": "2026-11-01", "periodEnd": "2026-11-30", "target": 180, "achieved": 35, "rateBps": 1944, "closed": false, "createdBy": { "id": "01929e55-2c3d-7e4f-9a1b-2c3d4e5f6a7b", "displayName": "Nadia Belkacem" }, "createdAt": "2026-10-29T09:00:00Z", "updatedAt": "2026-10-29T09:00:00Z", "version": 1 }
  ],
  "total": 33,
  "page": 1,
  "pageSize": 50
}

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks field.view

Rules & side effects

  1. Read-only.

Used by BO-20 · BO-17

GET /admin/objectives/{objectiveId}

admin.objectives.crud (get) · Permission field.view · Apps BO · Release V1 · Requirements FLD-02

Purpose. One objective.

Response 200 OK — an Objective.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks field.view
404NOT_FOUNDUnknown objective

Rules & side effects

  1. Read-only.

Used by BO-20

POST /admin/objectives

admin.objectives.crud (create) · Permission field.create · Apps BO · Release V1 · Requirements FLD-02

Purpose. Sets the same objective for one or several delegates.

Request

interface CreateObjectivesBody {
  delegateIds: Uuid[];               // 1–100
  metric: ObjectiveMetric;
  period: ObjectivePeriod;
  periodStart: string;               // aligned on the period; today or later for DAY
  target: number;                    // > 0
}
{ "delegateIds": ["0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12"], "metric": "FIRST_ORDERS", "period": "MONTH", "periodStart": "2026-12-01", "target": 10 }

Response 201 Created — { "items": [Objective…], "created": 1 }

Errors

StatusCodeWhen
400VALIDATION_FAILEDperiodStart not aligned (not a Sunday for WEEK, not the 1st for MONTH), target ≤ 0
403PERMISSION_DENIEDCaller lacks field.create
404NOT_FOUNDUnknown delegate
409DUPLICATE_VALUEAn objective already exists for a delegate × metric × period × start — details: { delegateIds } (nothing is created)
409OBJECTIVE_PERIOD_CLOSEDThe period has already ended

Rules & side effects

  1. All-or-nothing. Audit CREATE. Notifications: objective.assigned → each delegate (in-app; proposed event, not yet in the notifications matrix).

Used by BO-20

PATCH /admin/objectives/{objectiveId}

admin.objectives.crud (update) · Permission field.update · Apps BO · Release V1 · Requirements FLD-02

Purpose. Changes the target of an objective whose period has not ended.

Request

interface UpdateObjectiveBody { target: number; version: number }
{ "target": 12, "version": 1 }

Response 200 OK — the updated Objective (rateBps recomputed).

Errors

StatusCodeWhen
400VALIDATION_FAILEDTarget ≤ 0
403PERMISSION_DENIEDCaller lacks field.update
404NOT_FOUNDUnknown objective
409OBJECTIVE_PERIOD_CLOSED · VERSION_CONFLICTPeriod ended · stale version

Rules & side effects

  1. Audit UPDATE with before/after target.

Used by BO-20

DELETE /admin/objectives/{objectiveId}

admin.objectives.crud (delete) · Permission field.delete · Apps BO · Release V1 · Requirements FLD-02

Purpose. Deletes an objective whose period has not ended.

Response 204 No Content

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks field.delete
404NOT_FOUNDUnknown objective
409OBJECTIVE_PERIOD_CLOSEDPeriod ended (history is kept for reports)

Rules & side effects

  1. Audit DELETE.

Used by BO-20

Tours

One tour = one delegate × one date; weekly or monthly planning creates one tour per day (cahier §4.5 « Planification quotidienne, hebdomadaire ou mensuelle »). Only the assigned delegate can start it (cahier §4.5 « Seul le délégué affecté peut démarrer la tournée »). Every planning change reaches the delegate through sync.delta and a push (cahier §6.2 « Une visite ajoutée ou déplacée doit être synchronisée »).

GET /admin/tours

admin.tours.list · Permission field.view · Apps BO · Release V1 · Requirements FLD-03

Purpose. Tours by delegate and date range — the planner calendar (BO-21) and tour lists.

Request

QueryTypeValues / defaultNotes
from / tostring (date)requiredInclusive; at most 62 days
delegateIdUuid[]repeatable
zoneIdUuidTours of the zone's delegates
statusTourStatus[]repeatable
latebooleanNot started, or not closed after its date
seriesIdUuid
sort / order / page / pageSizedate (default), delegate · asc · 1 · 200 (max 200)The calendar fetches a range in one page

Response 200 OK — Page<TourListItem>

{
  "items": [
    {
      "id": "01930a10-3c4d-7e5f-8a6b-7c8d9e0f1a2b", "number": "TRN-2026-000045", "date": "2026-11-04",
      "delegate": { "id": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12", "displayName": "Yacine Merabet" },
      "status": "IN_PROGRESS", "orderingMode": "RECOMMENDED", "seriesId": "01930a00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
      "stops": 9, "visitsDone": 4, "visitsNotDone": 1, "plannedKm": 38.4, "km": 21.7,
      "startedAt": "2026-11-04T07:58:00Z", "late": false, "version": 6
    },
    {
      "id": "01930a10-4d5e-7f6a-9b7c-8d9e0f1a2b3c", "number": "TRN-2026-000046", "date": "2026-11-05",
      "delegate": { "id": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12", "displayName": "Yacine Merabet" },
      "status": "PLANNED", "orderingMode": "RECOMMENDED", "seriesId": "01930a00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
      "stops": 8, "visitsDone": 0, "visitsNotDone": 0, "plannedKm": 41.2, "late": false, "version": 1
    }
  ],
  "total": 38,
  "page": 1,
  "pageSize": 200
}

Errors

StatusCodeWhen
400VALIDATION_FAILEDMissing range, to before from
403PERMISSION_DENIEDCaller lacks field.view
422REPORT_RANGE_TOO_LARGERange over 62 days — details: { maxDays: 62 }

Rules & side effects

  1. Read-only. Live status changes arrive through tour.* SSE events.

Used by BO-21 · BO-17 · BO-06

POST /admin/tours

admin.tours.create · Permission field.create · Apps BO · Release V1 · Requirements FLD-03

Purpose. Plans a tour for one date, for an explicit list of dates, or as a recurring series, with ordered stops (point of sale, time slot, priority, visit type).

Request

interface CreateTourBody {
  delegateId: Uuid;
  date?: string;                     // exactly one of date / dates / recurrence
  dates?: string[];                  // 1–31 dates
  recurrence?: {
    frequency: "DAILY" | "WEEKLY";
    weekdays?: (0 | 1 | 2 | 3 | 4 | 5 | 6)[];   // WEEKLY; 0 = Sunday
    from: string;                    // first date
    until: string;                   // last date; the series holds at most 31 tours
  };
  orderingMode: "IMPOSED" | "RECOMMENDED";
  notes?: string;                    // shown to the delegate, ≤ 1 000
  stops: { resellerId: Uuid; plannedAt?: string; priority?: "NORMAL" | "HIGH"; visitTypeId: Uuid }[];   // 1–40, in visiting order; plannedAt "HH:mm"
  allowOutOfScope?: boolean;         // accept stops outside the delegate's zones and assignments
  outOfScopeReason?: string;         // required with allowOutOfScope
}
{
  "delegateId": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12",
  "recurrence": { "frequency": "WEEKLY", "weekdays": [0, 2, 4], "from": "2026-11-08", "until": "2026-11-26" },
  "orderingMode": "RECOMMENDED",
  "notes": "Priorité aux points de vente sans commande depuis 30 jours.",
  "stops": [
    { "resellerId": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a", "plannedAt": "09:00", "priority": "HIGH", "visitTypeId": "01928e40-1a2b-7c3d-8e4f-5a6b7c8d9e0f" },
    { "resellerId": "0192f4d2-3e4f-7a5b-9c6d-7e8f9a0b1c2d", "plannedAt": "10:00", "priority": "NORMAL", "visitTypeId": "01928e40-1a2b-7c3d-8e4f-5a6b7c8d9e0f" },
    { "resellerId": "0192f4e3-4f5a-7b6c-8d7e-8f9a0b1c2d3e", "priority": "NORMAL", "visitTypeId": "01928e40-2b3c-7d4e-9f5a-6b7c8d9e0f1a" }
  ]
}

Response 201 Created

{
  "seriesId": "01930b50-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
  "items": [
    { "id": "01930b50-2b3c-7d4e-9f5a-6b7c8d9e0f1a", "number": "TRN-2026-000061", "date": "2026-11-08", "delegate": { "id": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12", "displayName": "Yacine Merabet" }, "status": "PLANNED", "orderingMode": "RECOMMENDED", "seriesId": "01930b50-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "stops": 3, "visitsDone": 0, "visitsNotDone": 0, "plannedKm": 14.8, "late": false, "version": 1 }
  ],
  "created": 8,
  "skippedDates": [{ "date": "2026-11-01", "reason": "PUBLIC_HOLIDAY" }],
  "warnings": [{ "code": "RESELLER_NOT_INTERESTED", "resellerId": "0192f4e3-4f5a-7b6c-8d7e-8f9a0b1c2d3e" }]
}

(items lists every created tour; one shown.)

Errors

StatusCodeWhen
400VALIDATION_FAILEDNot exactly one of date / dates / recurrence, more than 31 tours, a date in the past, duplicate reseller in the stops, bad plannedAt, more than 40 stops
403PERMISSION_DENIEDCaller lacks field.create
404NOT_FOUNDUnknown delegate, reseller or visit type
409TOUR_DATE_TAKENThe delegate already has a tour on one of the dates — details: { dates, existingTourIds } (nothing created)
422DELEGATE_INACTIVEDelegate account not ACTIVE
422STOP_OUT_OF_SCOPEA reseller is outside the delegate's zones and assignments and allowOutOfScope is not true — details: { resellerIds }
422REASON_REQUIREDallowOutOfScope without outOfScopeReason

Rules & side effects

  1. All-or-nothing. Dates falling on Friday or Saturday (the working week is Sunday–Thursday) or on a public holiday are skipped and reported in skippedDates for recurrences; an explicit date / dates on such a day is accepted (weekend work happens).
  2. Each tour gets a TRN- number and one Visit per stop (PLANNED, VIS- number, sequence); a tour dated today is created TO_START, later ones PLANNED. Prospects among the stops move from NEW_PROSPECT to TO_CONTACT.
  3. allowOutOfScope grants the delegate access to those resellers for the tour (see the permissions matrix) and writes an OVERRIDE audit row with the reason.
  4. Audit CREATE per tour; TourStatusChange — → PLANNED / TO_START.
  5. Notifications: tour.assigned → the delegate (push + in-app; one notification for a series). SSE: none.

Used by BO-21 · BO-22

GET /admin/tours/{tourId}

admin.tours.get · Permission field.view · Apps BO · Release V1 · Requirements FLD-13 · FLD-14

Purpose. The tour live detail: stops with check-ins, distances and tolerance, durations, not-done reasons, photos and reports, history, closing summary and the delegate's last position.

Response 200 OK — TourDetail

{
  "id": "01930a10-3c4d-7e5f-8a6b-7c8d9e0f1a2b",
  "number": "TRN-2026-000045",
  "date": "2026-11-04",
  "delegate": { "id": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12", "displayName": "Yacine Merabet" },
  "status": "IN_PROGRESS",
  "orderingMode": "RECOMMENDED",
  "stops": 9, "visitsDone": 4, "visitsNotDone": 1, "plannedKm": 38.4, "km": 21.7,
  "startedAt": "2026-11-04T07:58:00Z",
  "late": false,
  "notes": "Priorité aux points de vente sans commande depuis 30 jours.",
  "startPosition": { "lat": 35.6969, "lng": -0.6331, "accuracy": 9, "capturedAt": "2026-11-04T07:58:00Z" },
  "stopsDetail": [
    {
      "visitId": "01930a10-5e6f-7a8b-9c0d-1e2f3a4b5c6e", "visitNumber": "VIS-2026-001234", "sequence": 1,
      "reseller": { "id": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a", "number": "REV-000123", "storeName": "Phone Store El Bahia", "ownerName": "Karim Benali", "phone": "+213555123456", "wilayaCode": "31", "communeName": "Bir El Djir", "status": "ACTIVE", "location": { "lat": 35.7206, "lng": -0.544 } },
      "plannedAt": "09:00", "priority": "HIGH", "visitType": { "id": "01928e40-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "name": "Visite commerciale" },
      "status": "DONE", "late": false,
      "checkIn": { "at": "2026-11-04T08:41:12Z", "position": { "lat": 35.7209, "lng": -0.5446, "accuracy": 14 }, "distanceMeters": 63, "withinTolerance": true },
      "startedAt": "2026-11-04T08:42:00Z", "endedAt": "2026-11-04T09:14:00Z", "durationMinutes": 32,
      "photosCount": 2, "hasReport": true, "addedBy": "PLAN"
    },
    {
      "visitId": "01930a10-6f7a-7b8c-8d9e-0f1a2b3c4d5f", "visitNumber": "VIS-2026-001236", "sequence": 3,
      "reseller": { "id": "0192f4e3-4f5a-7b6c-8d7e-8f9a0b1c2d3e", "number": "REV-000231", "storeName": "Hassi Phone", "ownerName": "Ali Kerroum", "phone": "+213552334455", "wilayaCode": "31", "communeName": "Hassi Bounif", "status": "ACCOUNT_CREATED" },
      "priority": "NORMAL", "visitType": { "id": "01928e40-2b3c-7d4e-9f5a-6b7c8d9e0f1a", "name": "Prospection" },
      "status": "NOT_DONE", "late": false,
      "notDone": { "reasonCode": "STORE_CLOSED", "comment": "Rideau baissé à 10 h 40.", "at": "2026-11-04T09:40:00Z" },
      "photosCount": 1, "hasReport": false, "addedBy": "PLAN"
    }
  ],
  "history": [
    { "from": "TO_START", "to": "IN_PROGRESS", "at": "2026-11-04T07:58:00Z", "actor": { "id": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12", "displayName": "Yacine Merabet" }, "position": { "lat": 35.6969, "lng": -0.6331, "accuracy": 9 } },
    { "from": "PLANNED", "to": "TO_START", "at": "2026-11-03T23:05:00Z", "actor": null }
  ],
  "livePosition": { "position": { "lat": 35.6923, "lng": -0.5871, "accuracy": 11, "capturedAt": "2026-11-04T10:09:30Z" }, "at": "2026-11-04T10:09:30Z", "stale": false },
  "createdBy": { "id": "01929e55-2c3d-7e4f-9a1b-2c3d4e5f6a7b", "displayName": "Nadia Belkacem" },
  "createdAt": "2026-10-29T14:00:00Z",
  "version": 6
}

Field notes. distanceMeters is null when the point of sale had no coordinates (the check-in position becomes a candidate location, BR-51). livePosition.stale = no ping for more than 2 × field.locationIntervalSeconds. closing appears once the tour is COMPLETED.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks field.view
404NOT_FOUNDUnknown tour

Rules & side effects

  1. Read-only. The detail page subscribes to visit.*, tour.* and delegate.position for this tour.

Used by BO-23 · BO-22

PATCH /admin/tours/{tourId}

admin.tours.update · Permission field.update · Apps BO · Release V1 · Requirements FLD-03

Purpose. Edits the date, the delegate, the ordering mode or the notes of a tour not yet started.

Request

interface UpdateTourBody { date?: string; delegateId?: Uuid; orderingMode?: TourOrderingMode; notes?: string | null; version: number }
{ "date": "2026-11-06", "orderingMode": "IMPOSED", "version": 1 }

Response 200 OK — the TourDetail (status TO_START if the new date is today).

Errors

StatusCodeWhen
400VALIDATION_FAILEDDate in the past
403PERMISSION_DENIEDCaller lacks field.update
404NOT_FOUNDUnknown tour or delegate
409TOUR_NOT_EDITABLEStatus other than PLANNED or TO_START
409TOUR_DATE_TAKENThe (new) delegate already has a tour that day
409VERSION_CONFLICTStale version
422DELEGATE_INACTIVE · STOP_OUT_OF_SCOPENew delegate inactive, or stops outside the new delegate's scope

Rules & side effects

  1. Audit UPDATE. Notifications: tour.updated → the delegate (push + in-app); on a delegate change, tour.updated to the former and tour.assigned to the new delegate.

Used by BO-22 · BO-21 (drag on the calendar)

PUT /admin/tours/{tourId}/stops

admin.tours.setStops · Permission field.update · Apps BO · Release V1 · Requirements FLD-03 · FLD-06

Purpose. Replaces the ordered list of stops; the result is synced to the delegate's app.

Request

interface SetStopsBody {
  stops: { visitId?: Uuid; resellerId: Uuid; plannedAt?: string; priority?: "NORMAL" | "HIGH"; visitTypeId: Uuid }[];   // full ordered list; keep existing stops by visitId
  allowOutOfScope?: boolean;
  outOfScopeReason?: string;
  version: number;
}
{
  "stops": [
    { "visitId": "01930a10-5e6f-7a8b-9c0d-1e2f3a4b5c6e", "resellerId": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a", "plannedAt": "09:00", "priority": "HIGH", "visitTypeId": "01928e40-1a2b-7c3d-8e4f-5a6b7c8d9e0f" },
    { "resellerId": "0192f4f4-5a6b-7c7d-8e8f-9a0b1c2d3e4f", "plannedAt": "11:30", "priority": "NORMAL", "visitTypeId": "01928e40-2b3c-7d4e-9f5a-6b7c8d9e0f1a" }
  ],
  "version": 1
}

Response 200 OK — the TourDetail with the new stopsDetail (new visits created PLANNED with VIS- numbers), plannedKm recomputed and version + 1.

Errors

StatusCodeWhen
400VALIDATION_FAILEDEmpty list, more than 40 stops, duplicate reseller, unknown visitId
403PERMISSION_DENIEDCaller lacks field.update
404NOT_FOUNDUnknown tour, reseller or visit type
409TOUR_STOPS_LOCKEDTour IN_PROGRESS or SUSPENDED and the list does not keep every existing stop unchanged and in order (only appending is allowed once started)
409TOUR_NOT_EDITABLETour COMPLETED or CANCELLED
409VERSION_CONFLICTStale version
422STOP_OUT_OF_SCOPE · REASON_REQUIREDAs for creation

Rules & side effects

  1. PLANNED / TO_START: full replacement — removed stops' visits are deleted (never started), kept stops are re-sequenced, new ones are created. Once started (IN_PROGRESS, SUSPENDED), the existing stops must be sent unchanged and in the same order, and new stops can only be appended at the end. Assumption — to reconcile with rule 1 of Lifecycles › Tour, which also allows removing or moving visits not yet started during the day.
  2. In RECOMMENDED mode the delegate may still reorder the remaining visits in the app; in IMPOSED mode the order sent here is final.
  3. Prospects added move to TO_CONTACT. Audit UPDATE with the before/after stop list (OVERRIDE for out-of-scope additions).
  4. Notifications: tour.updated → the delegate (push + in-app, with a sync data message so the offline snapshot refreshes).

Used by BO-22 · BO-23 (append a stop during the day)

POST /admin/tours/{tourId}/recommend-order

admin.tours.recommendOrder · Permission field.update · Apps BO · Release V1 · Requirements FLD-17

Purpose. Computes a recommended visiting order with the nearest-neighbour heuristic (cahier §4.5 « Ordre manuel ou recommandé des visites »); routing-engine optimisation is V2 (FLD-18).

Request

interface RecommendOrderBody {
  startFrom?: "HOME_BASE" | "CURRENT_POSITION" | "CUSTOM";   // default HOME_BASE (CURRENT_POSITION for a started tour)
  customStart?: GeoPoint;            // with CUSTOM
  respectPriority?: boolean;         // default true: HIGH stops first, then NORMAL
  apply?: boolean;                   // default false: only propose
  version?: number;                  // required with apply
}
{ "startFrom": "HOME_BASE", "respectPriority": true }

Response 200 OK

{
  "start": { "kind": "HOME_BASE", "position": { "lat": 35.6971, "lng": -0.6308 } },
  "proposed": [
    { "visitId": "01930a10-5e6f-7a8b-9c0d-1e2f3a4b5c6e", "resellerId": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a", "storeName": "Phone Store El Bahia", "priority": "HIGH", "legKm": 8.4 },
    { "visitId": "01930a10-6f7a-7b8c-8d9e-0f1a2b3c4d5f", "resellerId": "0192f4e3-4f5a-7b6c-8d7e-8f9a0b1c2d3e", "storeName": "Hassi Phone", "priority": "NORMAL", "legKm": 6.1 },
    { "visitId": "01930a10-7a8b-7c9d-9e0f-1a2b3c4d5e70", "resellerId": "0192f4d2-3e4f-7a5b-9c6d-7e8f9a0b1c2d", "storeName": "Smart Accessoires Es Senia", "priority": "NORMAL", "legKm": 9.7 }
  ],
  "totalKm": 24.2,
  "currentKm": 31.9,
  "unplaced": [],
  "applied": false
}

Field notes. Distances are straight-line (haversine) kilometres between consecutive points, not road distances; currentKm is the same measure for the current order. unplaced lists stops without coordinates — they are kept at the end in their current order. Visits already started or finished keep their place and are excluded.

Errors

StatusCodeWhen
400VALIDATION_FAILEDCUSTOM without customStart, apply without version
403PERMISSION_DENIEDCaller lacks field.update
404NOT_FOUNDUnknown tour
409TOUR_NOT_EDITABLETour COMPLETED or CANCELLED
409TOUR_STOPS_LOCKEDapply: true on a started tour in IMPOSED mode
422START_POSITION_UNKNOWNNo home base (or no live position for CURRENT_POSITION)

Rules & side effects

  1. Nearest neighbour: from the start, repeatedly visit the closest remaining stop (HIGH stops first when respectPriority); time slots (plannedAt) are not optimised in V1 — a proposal that breaks the slot order is flagged in warnings.
  2. apply: false has no side effect. apply: true saves the order exactly like admin.tours.setStops (audit, tour.updated).

Used by BO-22

POST /admin/tours/{tourId}/cancel

admin.tours.cancel · Permission field.update · Apps BO · Release V1 · Requirements FLD-04

Purpose. Cancels a tour with a reason (planned tour no longer needed; delegate sick or broken down during the day).

Request

interface CancelTourBody { reason: string; version: number }
{ "reason": "Délégué en arrêt maladie — tournée reportée au 12 novembre.", "version": 1 }

Response 200 OK — the TourDetail with status CANCELLED.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks field.update
404NOT_FOUNDUnknown tour
409TOUR_TRANSITION_NOT_ALLOWEDStatus COMPLETED or CANCELLED — details: { from, to, allowed }
409VERSION_CONFLICTStale version
422REASON_REQUIREDEmpty reason

Rules & side effects

  1. Allowed from PLANNED, TO_START, IN_PROGRESS, SUSPENDED (tour transitions). Visits not started → CANCELLED; finished visits are kept; location sampling stops.
  2. TourStatusChange with the reason; audit STATUS_CHANGE.
  3. Notifications: tour.updated → the delegate (push + in-app). SSE: tour.cancelled.

Used by BO-21 · BO-23

POST /admin/tours/{tourId}/reopen

admin.tours.reopen · Permission field.validate · Apps BO · Release V1 · Requirements FLD-12

Purpose. Authorised reopening of a closed tour (cahier §6.9 « La clôture est définitive sauf réouverture autorisée »), so the delegate can complete missing data and close again.

Request

interface ReopenTourBody { reason: string; version: number }
{ "reason": "Rapport de visite manquant chez Hassi Phone — le délégué doit le compléter.", "version": 9 }

Response 200 OK — the TourDetail with status IN_PROGRESS, the previous closing kept as version 1.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks field.validate
404NOT_FOUNDUnknown tour
409TOUR_TRANSITION_NOT_ALLOWEDStatus not COMPLETED
409TOUR_REOPEN_EXPIREDMore than field.reopenMaxDays (7) days after the tour date
409VERSION_CONFLICTStale version
422REASON_REQUIREDEmpty reason

Rules & side effects

  1. COMPLETED → IN_PROGRESS; finished visits stay locked (unlock them one by one with admin.visits.unlock). Location sampling does not restart for a past date.
  2. TourStatusChange with the reason; audit STATUS_CHANGE plus OVERRIDE (exception to a definitive closing).
  3. Notifications: tour.reopened → the delegate (push + in-app, with a sync data message). SSE: tour.reopened.

Used by BO-23

GET /admin/tours/{tourId}/track

admin.tours.track · Permission field.view · Apps BO · Release V1 · Requirements FLD-13 · FLD-15

Purpose. The position trail of a tour for the map, within the retention period.

Request

QueryTypeValues / defaultNotes
sincestring (timestamp)Incremental fetch for a live tour
simplifyMetersnumber0 (raw) – 200; default 10Douglas–Peucker tolerance

Response 200 OK

{
  "tourId": "01930a10-3c4d-7e5f-8a6b-7c8d9e0f1a2b",
  "status": "IN_PROGRESS",
  "purged": false,
  "retentionUntil": "2027-02-02",
  "points": [
    { "lat": 35.6969, "lng": -0.6331, "accuracy": 9, "capturedAt": "2026-11-04T07:58:00Z" },
    { "lat": 35.7012, "lng": -0.6122, "accuracy": 12, "capturedAt": "2026-11-04T08:03:00Z" },
    { "lat": 35.7209, "lng": -0.5446, "accuracy": 14, "capturedAt": "2026-11-04T08:41:12Z" }
  ],
  "events": [
    { "kind": "TOUR_STARTED", "at": "2026-11-04T07:58:00Z", "position": { "lat": 35.6969, "lng": -0.6331 } },
    { "kind": "CHECK_IN", "at": "2026-11-04T08:41:12Z", "visitId": "01930a10-5e6f-7a8b-9c0d-1e2f3a4b5c6e", "position": { "lat": 35.7209, "lng": -0.5446 } }
  ],
  "km": 21.7,
  "pointsCount": 42
}

Field notes. Points exist only while the tour was IN_PROGRESS (BR-52). After field.locationRetentionDays (90) the points are purged: purged: true, points: [], while km and check-in positions (business data) remain.

Errors

StatusCodeWhen
400VALIDATION_FAILEDsimplifyMeters out of range
403PERMISSION_DENIEDCaller lacks field.view
404NOT_FOUNDUnknown tour

Rules & side effects

  1. Read-only. Access to position trails is personal data of the delegate (cahier §10): reads are rate-limited (60 / min) and not cached by the client beyond the page.

Used by BO-23

GET /admin/live/delegates

admin.live.delegates · Permission field.view · Apps BO · Release V1 · Requirements FLD-13

Purpose. Delegates on tour now, with last known position and progress — the initial state of the live map; delegate.position events keep it current.

Request

QueryTypeValues / defaultNotes
zoneId / wilayaCodeUuid / string
includeIdlebooleanfalseAlso delegates with a tour TO_START or SUSPENDED

Response 200 OK

{
  "items": [
    {
      "delegate": { "id": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12", "displayName": "Yacine Merabet" },
      "tour": { "id": "01930a10-3c4d-7e5f-8a6b-7c8d9e0f1a2b", "number": "TRN-2026-000045", "status": "IN_PROGRESS", "startedAt": "2026-11-04T07:58:00Z" },
      "lastPosition": { "lat": 35.6923, "lng": -0.5871, "accuracy": 11, "capturedAt": "2026-11-04T10:09:30Z" },
      "stale": false,
      "progress": { "planned": 9, "done": 4, "notDone": 1, "remaining": 4, "late": 1 },
      "currentVisit": null,
      "nextStop": { "visitId": "01930a10-8b9c-7d0e-9f1a-2b3c4d5e6f71", "storeName": "Galaxy Phone", "plannedAt": "11:30", "distanceMeters": 2310 },
      "km": 21.7
    }
  ],
  "generatedAt": "2026-11-04T10:10:00Z"
}

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks field.view

Rules & side effects

  1. Read-only; not paginated (a few dozen delegates). stale = no position for more than 2 × field.locationIntervalSeconds. Suspended tours have no fresh position by design (sampling stops).

Used by BO-06 · BO-23

Visits

GET /admin/visits

admin.visits.list · Permission field.view · Apps BO · Release V1 · Requirements FLD-08 · FLD-09

Purpose. Visits with status, delegate, reseller and outcome filters — the visit reports analysis of cahier §4.5.

Request

QueryTypeValues / defaultNotes
statusVisitStatus[]repeatable
delegateIdUuid[]repeatable
resellerId / tourIdUuidReseller 360° tab, tour detail
visitTypeIdUuid[]repeatable
from / tostring (date)Tour date
interestLevelInterestLevel[]repeatableFrom the report
nextActionNextAction[]repeatableFrom the report
notDoneReasonCodestring[]VISIT_NOT_DONE_REASON codes
outOfTolerance / hasReport / hasOrderboolean
qstringmin 2 charsVisit number, store name
sort / order / page / pageSizecheckedInAt (default), date, durationMinutes · desc · 1 · 25 (max 200)

Response 200 OK

{
  "items": [
    {
      "id": "01930a10-5e6f-7a8b-9c0d-1e2f3a4b5c6e", "number": "VIS-2026-001234", "date": "2026-11-04", "status": "DONE",
      "tour": { "id": "01930a10-3c4d-7e5f-8a6b-7c8d9e0f1a2b", "number": "TRN-2026-000045" },
      "delegate": { "id": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12", "displayName": "Yacine Merabet" },
      "reseller": { "id": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a", "number": "REV-000123", "storeName": "Phone Store El Bahia", "ownerName": "Karim Benali", "phone": "+213555123456", "wilayaCode": "31", "communeName": "Bir El Djir", "status": "ACTIVE" },
      "visitType": { "id": "01928e40-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "name": "Visite commerciale" },
      "checkedInAt": "2026-11-04T08:41:12Z", "distanceMeters": 63, "outOfTolerance": false, "durationMinutes": 32,
      "interestLevel": "HIGH", "nextAction": "PHONE_FOLLOW_UP", "photosCount": 2, "hasReport": true, "orderId": null, "locked": true
    }
  ],
  "total": 214,
  "page": 1,
  "pageSize": 25
}

Errors

StatusCodeWhen
400VALIDATION_FAILEDUnknown enum value
403PERMISSION_DENIEDCaller lacks field.view

Rules & side effects

  1. Read-only; new check-ins and ends arrive through visit.* SSE events.

Used by BO-24 · BO-11

GET /admin/visits/{visitId}

admin.visits.get · Permission field.view · Apps BO · Release V1 · Requirements FLD-08 · FLD-10

Purpose. A visit with check-in position and distance, timings, checklist, note, photos (with capture position) and the structured report.

Response 200 OK

{
  "id": "01930a10-5e6f-7a8b-9c0d-1e2f3a4b5c6e", "number": "VIS-2026-001234", "status": "DONE", "date": "2026-11-04",
  "tour": { "id": "01930a10-3c4d-7e5f-8a6b-7c8d9e0f1a2b", "number": "TRN-2026-000045" },
  "delegate": { "id": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12", "displayName": "Yacine Merabet" },
  "reseller": { "id": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a", "number": "REV-000123", "storeName": "Phone Store El Bahia", "ownerName": "Karim Benali", "phone": "+213555123456", "wilayaCode": "31", "communeName": "Bir El Djir", "status": "ACTIVE", "location": { "lat": 35.7206, "lng": -0.544 } },
  "visitType": { "id": "01928e40-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "name": "Visite commerciale", "minPhotos": 1, "reportRequired": true },
  "checkIn": { "at": "2026-11-04T08:41:12Z", "occurredAt": "2026-11-04T08:41:12Z", "receivedAt": "2026-11-04T08:41:15Z", "position": { "lat": 35.7209, "lng": -0.5446, "accuracy": 14 }, "distanceMeters": 63, "withinTolerance": true },
  "startedAt": "2026-11-04T08:42:00Z", "endedAt": "2026-11-04T09:14:00Z", "durationMinutes": 32,
  "checklist": { "templateId": "01928e50-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "templateVersion": 2, "items": [
    { "id": "01928e50-2b3c-7d4e-9f5a-6b7c8d9e0f1a", "label": "Présentoir HOCO en place", "type": "YES_NO", "value": true },
    { "id": "01928e50-3c4d-7e5f-8a6b-7c8d9e0f1a2b", "label": "Nombre de références ACEFAST en rayon", "type": "NUMBER", "value": 6 }
  ] },
  "note": "Demande la nouvelle gamme GaN.",
  "photos": [{ "id": "01930a11-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "file": { "id": "01930a11-2b3c-7d4e-9f5a-6b7c8d9e0f1a", "url": "https://files.hoco.dz/s/vis1234-1.webp?sig=…", "thumbnailUrl": "https://files.hoco.dz/s/vis1234-1-thumb.webp?sig=…", "mimeType": "image/webp", "size": 201334, "purpose": "VISIT_PHOTO" }, "capturedAt": "2026-11-04T08:50:00Z", "position": { "lat": 35.7208, "lng": -0.5445, "accuracy": 10 } }],
  "report": {
    "personMet": "Karim Benali", "personRoleCode": "OWNER", "interestLevel": "HIGH",
    "productsPresented": [{ "productId": "0192b215-6f70-7a81-9b2c-3d4e5f6a7b80", "name": "Chargeur GaN 50 W 2 ports" }],
    "promotionsPresented": [{ "promotionId": "01930050-2b3c-7d4e-9f5a-6b7c8d9e0f1a", "name": "ACEFAST −10 % novembre" }],
    "issueTags": ["PRICE_COMPETITION"], "needs": "Veut des câbles Lightning 2 m.", "nextAction": "PHONE_FOLLOW_UP", "followUpDate": "2026-11-05",
    "comment": "Commande prévue après le réassort.", "taskId": "01930b11-2c3d-7e4f-9a5b-6c7d8e9f0a1b"
  },
  "locked": true, "unlockedUntil": null, "history": []
}

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks field.view
404NOT_FOUNDUnknown visit

Rules & side effects

  1. Read-only; photo URLs signed 5 minutes.

Used by BO-25 · BO-23

POST /admin/visits/{visitId}/unlock

admin.visits.unlock · Permission field.validate · Apps BO · Release V1 · Requirements FLD-08

Purpose. Allows the delegate to correct a finished, locked visit (cahier §6.4 « Une visite terminée ne peut être modifiée sans permission »).

Request

interface UnlockVisitBody { reason: string; hours?: number }   // hours default field.visitUnlockHours (24), 1–168
{ "reason": "Photo du linéaire manquante — à ajouter." }

Response 200 OK — { "id": "01930a10-5e6f-7a8b-9c0d-1e2f3a4b5c6e", "status": "DONE", "locked": false, "unlockedUntil": "2026-11-05T10:20:00Z", "unlockedBy": { "id": "01929e55-2c3d-7e4f-9a1b-2c3d4e5f6a7b", "displayName": "Nadia Belkacem" } }

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks field.validate
404NOT_FOUNDUnknown visit
409VISIT_NOT_LOCKEDVisit not DONE, or already unlocked
422REASON_REQUIREDEmpty reason

Rules & side effects

  1. The delegate may edit checklist, note, photos and report until unlockedUntil; the visit locks again at the next delegate.visits.end or when the window closes.
  2. Audit OVERRIDE with the reason.
  3. Notifications: visit.unlocked → the delegate (push, sync data message; proposed event, not yet in the notifications matrix).

Used by BO-25

Visit types and checklist templates

interface VisitType {
  id: Uuid; code: string; name: string;
  minPhotos: number;                 // photos required to end the visit
  reportRequired: boolean;           // default field.reportRequiredToEndVisit
  reportFields: ("personMet" | "interestLevel" | "productsPresented" | "promotionsPresented" | "issueTags" | "needs" | "nextAction" | "comment" | "attachments")[];
  checklistTemplateId?: Uuid;
  defaultDurationMinutes?: number;
  active: boolean; sortOrder: number; version: number;
}

interface ChecklistTemplate {
  id: Uuid; name: string;
  items: { id: Uuid; label: string; type: "CHECK" | "YES_NO" | "NUMBER" | "TEXT" | "CHOICE"; options?: string[]; required: boolean; sortOrder: number }[];
  active: boolean;
  version: number;                   // templates are versioned; a visit keeps the version it was done with
  usedBy: number;                    // visit types using it
}

GET /admin/visit-types

admin.visitTypes.crud (list) · Permission field.view · Apps BO · Release V1 · Requirements FLD-16

Purpose. Lists visit types (also used by the tour editor).

Request — active (boolean), page / pageSize.

Response 200 OK — Page<VisitType>, e.g. { "items": [{ "id": "01928e40-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "code": "COMMERCIAL", "name": "Visite commerciale", "minPhotos": 1, "reportRequired": true, "reportFields": ["personMet", "interestLevel", "productsPresented", "promotionsPresented", "nextAction", "comment"], "checklistTemplateId": "01928e50-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "defaultDurationMinutes": 30, "active": true, "sortOrder": 1, "version": 2 }], "total": 4, "page": 1, "pageSize": 25 }

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks field.view

Rules & side effects

  1. Read-only (reading relaxed to field.view — Assumption).

Used by BO-26 · BO-22

GET /admin/visit-types/{visitTypeId}

admin.visitTypes.crud (get) · Permission field.view · Apps BO · Release V1 · Requirements FLD-16

Purpose. One visit type. Response 200 OK — a VisitType.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown visit type

Rules & side effects

  1. Read-only.

Used by BO-26

POST /admin/visit-types

admin.visitTypes.crud (create) · Permission field.administer · Apps BO · Release V1 · Requirements FLD-16

Purpose. Creates a visit type with its photo, report and checklist requirements (cahier §6.4 « Les photos peuvent être obligatoires selon le type de visite », §6.8).

Request

{ "code": "MERCHANDISING", "name": "Merchandising", "minPhotos": 2, "reportRequired": false, "reportFields": ["comment"], "checklistTemplateId": "01928e50-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "defaultDurationMinutes": 20 }

Response 201 Created — the VisitType.

Errors

StatusCodeWhen
400VALIDATION_FAILEDminPhotos over 10, unknown report field
403PERMISSION_DENIEDCaller lacks field.administer
409DUPLICATE_VALUECode taken

Rules & side effects

  1. Audit CREATE; the delegate app receives it with sync.delta.

Used by BO-26

PATCH /admin/visit-types/{visitTypeId}

admin.visitTypes.crud (update) · Permission field.administer · Apps BO · Release V1 · Requirements FLD-16

Purpose. Edits or deactivates a visit type (never deleted — visits reference it).

Request — partial VisitType fields plus version, e.g. { "minPhotos": 1, "version": 2 }.

Response 200 OK — the updated VisitType.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks field.administer
404NOT_FOUNDUnknown visit type
409VERSION_CONFLICTStale version

Rules & side effects

  1. Requirements apply to visits not yet started; audit UPDATE.

Used by BO-26

GET /admin/checklist-templates

admin.checklists.crud (list) · Permission field.view · Apps BO · Release V1 · Requirements FLD-16

Purpose. Lists checklist templates. Request — active, page / pageSize. Response 200 OK — Page<ChecklistTemplate>.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks field.view

Rules & side effects

  1. Read-only.

Used by BO-26

GET /admin/checklist-templates/{templateId}

admin.checklists.crud (get) · Permission field.view · Apps BO · Release V1 · Requirements FLD-16

Purpose. One template (?version= for a past version). Response 200 OK — a ChecklistTemplate.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown template or version

Rules & side effects

  1. Read-only.

Used by BO-26 · BO-25

POST /admin/checklist-templates

admin.checklists.crud (create) · Permission field.administer · Apps BO · Release V1 · Requirements FLD-16

Purpose. Creates a checklist template.

Request

{ "name": "Contrôle linéaire", "items": [
  { "label": "Présentoir HOCO en place", "type": "YES_NO", "required": true, "sortOrder": 1 },
  { "label": "Nombre de références ACEFAST en rayon", "type": "NUMBER", "required": false, "sortOrder": 2 },
  { "label": "État du présentoir", "type": "CHOICE", "options": ["Bon", "Abîmé", "Absent"], "required": true, "sortOrder": 3 }
] }

Response 201 Created — the ChecklistTemplate (version 1).

Errors

StatusCodeWhen
400VALIDATION_FAILEDNo item, more than 30 items, CHOICE without options
403PERMISSION_DENIEDCaller lacks field.administer

Rules & side effects

  1. Audit CREATE.

Used by BO-26

PATCH /admin/checklist-templates/{templateId}

admin.checklists.crud (update) · Permission field.administer · Apps BO · Release V1 · Requirements FLD-16

Purpose. Edits a template — a change of items creates a new version.

Request — { name?, items?, active?, version }.

Response 200 OK — the ChecklistTemplate with version + 1 when items changed.

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks field.administer
404NOT_FOUNDUnknown template
409VERSION_CONFLICTStale version

Rules & side effects

  1. Visits in progress keep their version. Audit UPDATE.

Used by BO-26

Invitations

GET /admin/invitations

admin.invitations.list · Permission field.view · Apps BO · Release V1 · Requirements IVT-03 · ANA-04

Purpose. The invitation funnel by delegate (sent → installed → account → verified → catalog → first order, cahier §6.6) and the invitations behind it.

Request

QueryTypeValues / defaultNotes
from / tostring (date)default current monthOn sentAt
delegateIdUuid[]repeatable
zoneIdUuid
reachedStep / stuckAtInvitationStepHighest step reached / exactly that step
channelShareChannel[]repeatable
page / pageSizenumber1 / 25 (max 200)

Response 200 OK

{
  "funnel": { "SENT": 42, "APP_INSTALLED": 23, "ACCOUNT_CREATED": 19, "PHONE_VERIFIED": 19, "CATALOG_VIEWED": 15, "FIRST_ORDER": 7 },
  "byDelegate": [
    { "delegate": { "id": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12", "displayName": "Yacine Merabet" }, "steps": { "SENT": 9, "APP_INSTALLED": 6, "ACCOUNT_CREATED": 5, "PHONE_VERIFIED": 5, "CATALOG_VIEWED": 4, "FIRST_ORDER": 3 }, "conversionBps": 3333 }
  ],
  "items": [
    { "id": "0192f4e3-9a01-7b02-8c03-0d0e0f101112", "code": "Q4MZ-7KTR", "reseller": { "id": "0192f4e3-4f5a-7b6c-8d7e-8f9a0b1c2d3e", "number": "REV-000231", "storeName": "Hassi Phone", "ownerName": "Ali Kerroum", "phone": "+213552334455", "wilayaCode": "31", "communeName": "Hassi Bounif", "status": "ACCOUNT_CREATED" }, "delegate": { "id": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12", "displayName": "Yacine Merabet" }, "channels": ["WHATSAPP"], "sentAt": "2026-11-02T09:10:00Z", "expiresAt": "2026-12-02T09:10:00Z", "steps": [{ "step": "SENT", "at": "2026-11-02T09:10:00Z", "inferred": false }, { "step": "APP_INSTALLED", "at": "2026-11-02T09:31:00Z", "inferred": false }, { "step": "ACCOUNT_CREATED", "at": "2026-11-02T09:40:00Z", "inferred": false }, { "step": "PHONE_VERIFIED", "at": "2026-11-02T09:40:00Z", "inferred": false }], "highestStep": "PHONE_VERIFIED" }
  ],
  "total": 42,
  "page": 1,
  "pageSize": 25
}

Errors

StatusCodeWhen
400VALIDATION_FAILEDUnknown step or channel
403PERMISSION_DENIEDCaller lacks field.view

Rules & side effects

  1. Read-only; the funnel is monotonic (later steps imply earlier ones, back-filled inferred: true — Lifecycles › Invitation funnel).

Used by BO-17 · BO-59 · BO-06

On this page