Promotions & loyalty
Back-office promotion operations — promotions with scope, targeting, schedule, caps and stacking, their status and simulation, banners and daily offers, and the V2 packs and loyalty programme.
These operations run the offers of cahier §4.6: daily offers, scheduled promotions and temporary discounts on products, categories or brands, targeting by wilaya, segment, level, brand or volume, configurable stacking, home banners, and — in V2 — profitable packs and the loyalty programme (points, levels Bronze/Silver/Gold/VIP, rewards). Screens: promotions list and editor (BO-49, BO-50), banners (BO-51), V2 packs, loyalty settings and rewards (BO-52 to BO-54). Statuses: Lifecycles › Promotion; pricing rules: BR-64 to BR-69.
Pricing order (V1). The reseller's price is the variant price of their customerType; promotions apply on top (BR-07). promotions.stackingMode (BEST_ONLY default, or STACK_BY_PRIORITY) decides how several eligible promotions combine.
Who uses it. The Administrateur commercial (cahier §3: « promotions »); the Super administrateur.
| Permission | Unlocks on this page |
|---|---|
promotions.view | Promotions, banners, packs (read), preview |
promotions.create · promotions.update · promotions.delete | Create · edit · delete drafts |
promotions.validate | Schedule, activate, pause, archive |
loyalty.view · loyalty.create · loyalty.update · loyalty.delete · loyalty.administer | V2 loyalty: read · rewards · adjustments and fulfilment · delete rewards · programme settings |
Audit. CREATE, UPDATE, DELETE and STATUS_CHANGE rows on Promotion, Banner, Pack, Reward; V2 adjustments as UPDATE on LoyaltyAccount with the reason.
Conventions: lists return Page<T> = { items, total, page, pageSize } (responses); money in centimes, percentages in basis points; timestamps UTC, schedules evaluated in Africa/Algiers; editable records carry version. 401, 429 and 500 apply everywhere and are not repeated.
| Method | Path | Operation | Access | Apps | Release |
|---|---|---|---|---|---|
| GET · POST · PATCH · DELETE | /admin/promotions[/{promotionId}] | admin.promotions.crudPromotions (type, value, scope, targeting, schedule, cap, stacking) | promotions.* | BO | V1 |
| POST | /admin/promotions/{promotionId}/status | admin.promotions.setStatusSchedule, activate, pause, archive | promotions.validate | BO | V1 |
| POST | /admin/promotions/preview | admin.promotions.previewSimulate a promotion on a sample cart and reseller | promotions.view | BO | V1 |
| GET · POST · PATCH · DELETE | /admin/banners[/{bannerId}] | admin.banners.crudBanners and daily offers | promotions.* | BO | V1 |
| PUT | /admin/banners/order | admin.banners.reorderReorder the banners of a placement in one call | promotions.update | BO | V1 |
| GET · POST · PATCH · DELETE | /admin/packs[/{packId}] | admin.packs.crudPacks (V2) | promotions.* | BO | V2 |
| GET · PUT | /admin/loyalty/settings | admin.loyalty.settingsEarning rules, expiry, level thresholds (V2) | loyalty.administer | BO | V2 |
| POST | /admin/loyalty/accounts/{resellerId}/adjustments | admin.loyalty.adjustManual points adjustment with reason (V2) | loyalty.update | BO | V2 |
| GET · POST · PATCH · DELETE | /admin/rewards[/{rewardId}] | admin.rewards.crudRewards catalogue (V2) | loyalty.* | BO | V2 |
| GET · POST | /admin/redemptions[/{redemptionId}/fulfil] | admin.redemptions.listRedemptions to fulfil (V2) | loyalty.update | BO | V2 |
Schemas
Promotion
interface Promotion {
id: Uuid;
name: string; // internal
label: string; // shown to resellers, ≤ 60: "ACEFAST −10 % novembre"
description?: string;
type: PromotionType; // PERCENT_OFF | AMOUNT_OFF | FIXED_PRICE | FREE_DELIVERY
value: number | null; // PERCENT_OFF: bp (1–9000); AMOUNT_OFF: Money HT per unit; FIXED_PRICE: Money HT per unit; FREE_DELIVERY: null
fixedPrices: { SUPER_GROS: Money; DEMI_GROS: Money; DETAIL: Money } | null; // FIXED_PRICE with one unit price HT per customer type (contract 1.1.0); null = `value` for every type
isDailyOffer: boolean; // shown in the "Offre du jour" carousel with a countdown
status: PromotionStatus;
startsAt: Iso; endsAt: Iso;
scope: { kind: "ALL" | "BRANDS" | "CATEGORIES" | "PRODUCTS" | "VARIANTS"; ids: Uuid[] }; // PromotionScope
audience: { // PromotionAudience; empty = every signed-in reseller
wilayaCodes: string[]; segmentIds: Uuid[]; customerTypes: CustomerType[]; resellerIds: Uuid[];
loyaltyLevels: LoyaltyLevel[]; // V2
};
conditions: { minQtyPerLine?: number; minOrderAmountTtc?: Money }; // volume targeting (cahier §4.6 « volume »)
stockCap?: number; // max units sold under the promotion
perResellerCap?: number; // max units per reseller
usedUnits: number;
priority: number; // higher first (STACK_BY_PRIORITY)
stackable: boolean;
notifyResellers: boolean; // promotion.started push at activation
stats: { orders: number; units: number; discountTotalHt: Money };
createdBy: UserSummary; createdAt: Iso; updatedAt: Iso; version: number;
}Banner
interface Banner {
id: Uuid;
title: string; // internal
placement: "HOME_HERO" | "HOME_DAILY_OFFER" | "CATALOG_TOP" | "PROMOTIONS_TOP"; // BannerPlacement
image: FileRef; // BANNER_IMAGE, public, 1200 × 500 recommended
imageMobile?: FileRef;
link: { kind: "NONE" | "PRODUCT" | "CATEGORY" | "BRAND" | "PROMOTION" | "URL"; targetId?: Uuid; url?: string }; // BannerLinkKind
audience: Promotion["audience"] & { guests: boolean }; // guests: shown in guest mode (no prices)
startsAt: Iso; endsAt: Iso;
status: PromotionStatus; // same lifecycle as promotions
countdown: boolean;
sortOrder: number;
promotionId?: Uuid; // follows the promotion's status when linked
createdAt: Iso; updatedAt: Iso; version: number;
}Promotions
GET /admin/promotions
admin.promotions.crud (list) · Permission promotions.view · Apps BO · Release V1 · Requirements PRM-01
Purpose. Lists promotions with status, type, date and target filters.
Request
| Query | Type | Values / default | Notes |
|---|---|---|---|
status | PromotionStatus[] | repeatable | |
type | PromotionType[] | repeatable | |
isDailyOffer | boolean | ||
activeOn | string (date) | Running that day | |
brandId / categoryId / productId | Uuid | Scope contains it | |
segmentId / wilayaCode | Uuid / string | Audience contains it | |
q | string | min 2 chars | Name, label |
sort / order / page / pageSize | startsAt (default), endsAt, priority, name · desc · 1 · 25 (max 200) |
Response 200 OK — Page<Promotion>
{
"items": [
{
"id": "01930050-2b3c-7d4e-9f5a-6b7c8d9e0f1a", "name": "ACEFAST novembre", "label": "ACEFAST −10 % novembre",
"type": "PERCENT_OFF", "value": 1000, "isDailyOffer": false, "status": "ACTIVE",
"startsAt": "2026-10-31T23:00:00Z", "endsAt": "2026-11-30T22:59:59Z",
"scope": { "kind": "BRANDS", "ids": ["01928a01-2b3c-7d4e-9f5a-6b7c8d9e0f12"] },
"audience": { "wilayaCodes": [], "segmentIds": [], "customerTypes": ["DEMI_GROS", "DETAIL"], "resellerIds": [], "loyaltyLevels": [] },
"conditions": { "minQtyPerLine": 5 }, "stockCap": 500, "perResellerCap": 50, "usedUnits": 86,
"priority": 10, "stackable": false, "notifyResellers": true,
"stats": { "orders": 14, "units": 86, "discountTotalHt": 1806000 },
"createdBy": { "id": "01929e55-2c3d-7e4f-9a1b-2c3d4e5f6a7b", "displayName": "Nadia Belkacem" },
"createdAt": "2026-10-28T10:00:00Z", "updatedAt": "2026-10-31T08:00:00Z", "version": 3
}
],
"total": 12, "page": 1, "pageSize": 25
}Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks promotions.view |
Rules & side effects
- Read-only.
Used by BO-49
GET /admin/promotions/{promotionId}
admin.promotions.crud (get) · Permission promotions.view · Apps BO · Release V1 · Requirements PRM-01
Purpose. One promotion with its stats and the names behind scope and audience ids (scopeItems, audienceItems).
Response 200 OK — a Promotion.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown promotion |
Rules & side effects
- Read-only.
Used by BO-50
POST /admin/promotions
admin.promotions.crud (create) · Permission promotions.create · Apps BO · Release V1 · Requirements PRM-01 · PRM-02 · PRM-03
Purpose. Creates a promotion in DRAFT.
Request — the Promotion writable fields (name, label, description, type, value, isDailyOffer, startsAt, endsAt, scope, audience, conditions, stockCap, perResellerCap, priority, stackable, notifyResellers):
{
"name": "Offre du jour câbles", "label": "Câble X14 à 300 DA", "type": "FIXED_PRICE", "value": 30000, "isDailyOffer": true,
"startsAt": "2026-11-05T07:00:00Z", "endsAt": "2026-11-05T22:59:59Z",
"scope": { "kind": "VARIANTS", "ids": ["0192b210-5e6f-7a8b-9c0d-1e2f3a4b5c6d"] },
"audience": { "wilayaCodes": ["31"], "segmentIds": [], "customerTypes": [], "resellerIds": [], "loyaltyLevels": [] },
"conditions": {}, "stockCap": 300, "perResellerCap": 40, "priority": 20, "stackable": false, "notifyResellers": true
}Response 201 Created — the Promotion (status DRAFT, version 1).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Value invalid for the type (PERCENT_OFF 1–90 %, AMOUNT_OFF positive and below the lowest unit price in scope, FIXED_PRICE positive, FREE_DELIVERY without value), fixedPrices on another type than FIXED_PRICE, endsAt not after startsAt, empty scope ids |
| 403 | PERMISSION_DENIED | Caller lacks promotions.create |
| 404 | NOT_FOUND | Unknown id in scope or audience |
Rules & side effects
- A
FIXED_PRICEabove a customer-type price never increases the price (the lower price wins, BR-65). AuditCREATE. - Contract 1.1.0 — a
FIXED_PRICEmay carryfixedPrices: { SUPER_GROS, DEMI_GROS, DETAIL }(all three, each > 0): each customer type gets its own promotional price, andvaluemay then benull. WithoutfixedPrices, the singlevalueapplies to every type, as before. The reseller catalog shows each reseller the price of its own type. - SRV-42 —
name(internal) andlabel(shown to resellers) are stored apart;notifyResellersdefaults tofalse.perResellerCaplimits the units one reseller buys under the promotion over its whole life: the cart prices at most the units the reseller has left (the rest at the type price, like the stock cap — BR-67), and checkout consumes them atomically with the stock cap: a limit spent in between answers409 CART_CHANGEDwith the re-priced cart. Refused and cancelled orders give the units back. The catalog cards do not check the per-reseller limit; the cart does.
Used by BO-50
PATCH /admin/promotions/{promotionId}
admin.promotions.crud (update) · Permission promotions.update · Apps BO · Release V1 · Requirements PRM-01 · PRM-03
Purpose. Edits a promotion — fully while DRAFT, partially once scheduled or running.
Request — any writable field plus version, e.g. { "endsAt": "2026-12-15T22:59:59Z", "stockCap": 800, "version": 3 }.
Response 200 OK — the updated Promotion.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | As for creation |
| 403 | PERMISSION_DENIED | Caller lacks promotions.update |
| 404 | NOT_FOUND | Unknown promotion |
| 409 | PROMOTION_NOT_EDITABLE | Field locked for the status — details: { status, lockedFields }: SCHEDULED must go back to DRAFT; ACTIVE/PAUSED only accept a later endsAt, a higher stockCap or perResellerCap, label and description; ENDED/ARCHIVED nothing |
| 409 | VERSION_CONFLICT | Stale version |
Rules & side effects
- Submitted orders keep the promotion they were priced with; carts are re-priced at their next read. Audit
UPDATE.
Used by BO-50
DELETE /admin/promotions/{promotionId}
admin.promotions.crud (delete) · Permission promotions.delete · Apps BO · Release V1 · Requirements PRM-01
Purpose. Deletes a promotion still in DRAFT (others are archived).
Response 204 No Content
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown promotion |
| 409 | PROMOTION_NOT_EDITABLE | Not DRAFT — archive it with setStatus |
Rules & side effects
- Audit
DELETE.
Used by BO-49
POST /admin/promotions/{promotionId}/status
admin.promotions.setStatus · Permission promotions.validate · Apps BO · Release V1 · Requirements PRM-01
Purpose. Schedules, activates now, unschedules, pauses, resumes or archives a promotion.
Request
interface SetPromotionStatusBody { to: "SCHEDULED" | "ACTIVE" | "DRAFT" | "PAUSED" | "ARCHIVED"; version: number }{ "to": "SCHEDULED", "version": 1 }Response 200 OK — the Promotion with its new status (ACTIVE directly when to: SCHEDULED and startsAt has passed).
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks promotions.validate |
| 404 | NOT_FOUND | Unknown promotion |
| 409 | PROMOTION_TRANSITION_NOT_ALLOWED | Move not in the table (e.g. ENDED → ACTIVE, resume after endsAt) |
| 422 | VALIDATION_FAILED | Scheduling guards fail (value, dates, empty scope) |
Rules & side effects
SCHEDULED→ACTIVEatstartsAtandACTIVE/PAUSED→ENDEDatendsAtor when the cap is consumed are automatic (jobpromotions.applySchedule, every minute); the engine checks dates itself at every pricing.- Audit
STATUS_CHANGE. - Notifications: at activation with
notifyResellers→promotion.startedto the target audience (PROMOTIONAL, opt-outs and quiet hours respected). A daily offer (daily_offer.published) and a flash sale (flash_sale.started) announce themselves whatever the flag.
POST /admin/promotions/preview
admin.promotions.preview · Permission promotions.view · Apps BO · Release V1 · Requirements PRM-04
Purpose. Simulates pricing on a sample cart for a given reseller — with a saved promotion or an unsaved draft — and explains eligibility (cahier §5.5 « Une promotion doit respecter ses dates, son stock et sa cible »).
Request
interface PromotionPreviewBody {
promotionId?: Uuid; // or
draft?: Partial<Promotion>; // the editor's unsaved values
resellerId?: Uuid; // a reseller, or
customerType?: CustomerType; // a customer type outside any reseller; neither = a guest (contract 1.1.0)
lines: { variantId: Uuid; quantity: number }[]; // 1–50
at?: Iso; // default now
}{ "promotionId": "01930050-2b3c-7d4e-9f5a-6b7c8d9e0f1a", "resellerId": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a", "lines": [{ "variantId": "0192b215-7a8b-7c9d-8e0f-1a2b3c4d5e6f", "quantity": 10 }, { "variantId": "0192b210-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "quantity": 20 }] }Response 200 OK
{
"reseller": { "id": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a", "storeName": "Phone Store El Bahia", "customerType": "DEMI_GROS", "wilayaCode": "31" },
"viewer": { "kind": "RESELLER", "customerType": "DEMI_GROS" },
"priceLocked": false,
"eligibility": { "eligible": true, "reasons": [] },
"lines": [
{ "variantId": "0192b215-7a8b-7c9d-8e0f-1a2b3c4d5e6f", "sku": "AF-A29-50W-WH", "quantity": 10, "basePrice": { "ht": 210000, "ttc": 249900, "vatRate": 1900 }, "applied": [{ "promotionId": "01930050-2b3c-7d4e-9f5a-6b7c8d9e0f1a", "label": "ACEFAST −10 % novembre", "amountHt": 210000 }], "lineTotalHt": 1890000 },
{ "variantId": "0192b210-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "sku": "HC-X14-CC-1M-BK", "quantity": 20, "basePrice": { "ht": 35000, "ttc": 41650, "vatRate": 1900 }, "applied": [], "notApplied": [{ "promotionId": "01930050-2b3c-7d4e-9f5a-6b7c8d9e0f1a", "reason": "OUT_OF_SCOPE" }], "lineTotalHt": 700000 }
],
"totals": { "subtotalHt": 2800000, "discountTotal": 210000, "deliveryFee": 0, "vatTotal": 492100, "totalTtc": 3082100, "currency": "DZD" },
"capRemaining": { "stockCap": 414, "perReseller": 50 }
}Field notes. Contract 1.1.0: the preview runs for a reseller (resellerId, reseller present), for a customer type (customerType: its prices, audiences other than customer types are not met) or for a guest (neither: priceLocked: true, amounts at 0, eligibility from dates, audience and scope only). viewer says which. capRemaining.perReseller is what the previewed reseller may still buy under perResellerCap (the limit itself for a customer type or a guest; null without a limit). Ineligibility reasons: NOT_STARTED, ENDED, PAUSED, AUDIENCE_WILAYA, AUDIENCE_SEGMENT, AUDIENCE_CUSTOMER_TYPE, AUDIENCE_RESELLER, OUT_OF_SCOPE, MIN_QTY, MIN_ORDER_AMOUNT, CAP_REACHED, RESELLER_CAP_REACHED, LOST_TO_BETTER_PROMOTION.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Neither or both of promotionId / draft, both resellerId and customerType, empty lines |
| 403 | PERMISSION_DENIED | Caller lacks promotions.view |
| 404 | NOT_FOUND | Unknown promotion, reseller or variant |
| 422 | CUSTOMER_TYPE_REQUIRED | The reseller has no customer type yet |
Rules & side effects
- Uses the cart pricing engine; writes nothing, consumes no cap.
Used by BO-50
Banners
GET /admin/banners
admin.banners.crud (list) · Permission promotions.view · Apps BO · Release V1 · Requirements PRM-05
Purpose. Lists banners and daily-offer slides by placement.
Request — placement[], status[], activeOn, sort (sortOrder default, startsAt), page / pageSize.
Response 200 OK — Page<Banner>
{
"items": [
{
"id": "01930060-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "title": "Hero ACEFAST GaN", "placement": "HOME_HERO",
"image": { "id": "01930060-2b3c-7d4e-9f5a-6b7c8d9e0f1a", "url": "https://cdn.hoco.dz/banners/acefast-gan-1200.webp", "mimeType": "image/webp", "size": 142880, "purpose": "BANNER_IMAGE" },
"link": { "kind": "PROMOTION", "targetId": "01930050-2b3c-7d4e-9f5a-6b7c8d9e0f1a" },
"audience": { "wilayaCodes": [], "segmentIds": [], "customerTypes": [], "resellerIds": [], "loyaltyLevels": [], "guests": true },
"startsAt": "2026-10-31T23:00:00Z", "endsAt": "2026-11-30T22:59:59Z", "status": "ACTIVE", "countdown": false, "sortOrder": 1,
"promotionId": "01930050-2b3c-7d4e-9f5a-6b7c8d9e0f1a", "createdAt": "2026-10-30T10:00:00Z", "updatedAt": "2026-10-30T10:00:00Z", "version": 1
}
],
"total": 5, "page": 1, "pageSize": 25
}Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks promotions.view |
Rules & side effects
- Read-only.
Used by BO-51
GET /admin/banners/{bannerId}
admin.banners.crud (get) · Permission promotions.view · Apps BO · Release V1 · Requirements PRM-05
Purpose. One banner. Response 200 OK — a Banner.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown banner |
Rules & side effects
- Read-only.
Used by BO-51
POST /admin/banners
admin.banners.crud (create) · Permission promotions.create · Apps BO · Release V1 · Requirements PRM-05
Purpose. Creates a banner (image, link target, targeting, schedule, order).
Request
{ "title": "Offre du jour X14", "placement": "HOME_DAILY_OFFER", "imageFileId": "01930061-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "link": { "kind": "PRODUCT", "targetId": "0192b210-4d5e-7f60-8a1b-2c3d4e5f6a70" }, "audience": { "wilayaCodes": ["31"], "guests": false }, "startsAt": "2026-11-05T07:00:00Z", "endsAt": "2026-11-05T22:59:59Z", "countdown": true, "sortOrder": 1, "promotionId": "01930062-3c4d-7e5f-8a6b-7c8d9e0f1a2b" }Response 201 Created — the Banner (status SCHEDULED when startsAt is in the future, ACTIVE otherwise — banners need no separate activation; DRAFT with "draft": true).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | URL link not https, targetId missing for a typed link, endsAt before startsAt |
| 403 | PERMISSION_DENIED | Caller lacks promotions.create |
| 404 | NOT_FOUND | Unknown link target or promotion |
| 422 | FILE_NOT_READY · FILE_PURPOSE_MISMATCH | Image not uploaded or not BANNER_IMAGE |
Rules & side effects
- A banner linked to a promotion is hidden while the promotion is not
ACTIVE(BR-68). AuditCREATE. - SRV-42 — every field is stored:
imageMobileFileId(BANNER_IMAGE, checked like the image), the fullaudience(loyalty levels excepted, V2),countdown(default: the linked promotion is a daily offer), anddraft: true(statusDRAFT, never served to resellers or guests until aPATCHsetsACTIVEorPAUSED).audience.guestsdefaults totrueonly for an untargeted banner.
Used by BO-51
PATCH /admin/banners/{bannerId}
admin.banners.crud (update) · Permission promotions.update · Apps BO · Release V1 · Requirements PRM-05
Purpose. Edits, reorders, pauses or archives a banner (status: PAUSED | ACTIVE | ARCHIVED).
Request — partial fields plus version, e.g. { "sortOrder": 2, "version": 1 }.
Response 200 OK — the updated Banner.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown banner |
| 409 | PROMOTION_TRANSITION_NOT_ALLOWED · VERSION_CONFLICT | Status move not allowed · stale version |
Rules & side effects
- Audit
UPDATE.
Used by BO-51
DELETE /admin/banners/{bannerId}
admin.banners.crud (delete) · Permission promotions.delete · Apps BO · Release V1 · Requirements PRM-05
Purpose. Deletes a banner. Response 204 No Content.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown banner |
Rules & side effects
- The image file stays 30 days then is purged. Audit
DELETE.
Used by BO-51
PUT /admin/banners/order
admin.banners.reorder · Permission promotions.update · Apps BO · Release V1 · Requirements PRM-05 · Added in contract 1.1.0 (SRV-41)
Purpose. Saves the order of a placement's banners in one call (BO-51 drag and drop, or the up / down arrows), instead of one sortOrder edit per banner.
Request
{ "placement": "HOME_CAROUSEL", "ids": ["01930e10-…-c", "01930e10-…-a", "01930e10-…-b"] }ids lists every non-archived banner of the placement (paused, scheduled and ended included), the first shown first.
Response 200 OK
{ "placement": "HOME_CAROUSEL", "items": [ { "id": "01930e10-…-c", "sortOrder": 10, "version": 1793518200000 }, { "id": "01930e10-…-a", "sortOrder": 20, "version": 1793518200000 }, { "id": "01930e10-…-b", "sortOrder": 30, "version": 1793518200000 } ] }Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | ids misses a banner of the placement, names another one, or repeats one (field ids) |
| 403 | PERMISSION_DENIED | Caller lacks promotions.update |
Rules & side effects
sortOrderbecomes 10, 20, 30… in the given order, in one transaction; each moved banner gets an auditUPDATE(sortOrderfrom → to) and a newversion.
Used by BO-51
Packs (V2)
V2
Pack (V2): { id, name, description, image: FileRef, items: [{ variantId, quantity }], price: Price (pack price per customer type: { superGros, demiGros, detail } HT), referenceTotalHt, savingBps, status: PromotionStatus, startsAt, endsAt, audience, stockCap, version }; added to the cart as a unit (cart.packs.add).
GET /admin/packs
admin.packs.crud (list) · Permission promotions.view · Apps BO · Release V2 · Requirements PRM-06
Purpose. Lists packs. Request — status[], q, page / pageSize. Response 200 OK — Page<Pack>.
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks promotions.view |
Rules & side effects
- Read-only.
Used by BO-52
GET /admin/packs/{packId}
admin.packs.crud (get) · Permission promotions.view · Apps BO · Release V2 · Requirements PRM-06
Purpose. One pack. Response 200 OK — a Pack.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown pack |
Rules & side effects
- Read-only.
Used by BO-52
POST /admin/packs
admin.packs.crud (create) · Permission promotions.create · Apps BO · Release V2 · Requirements PRM-06
Purpose. Creates a pack. Request — e.g. { "name": "Pack voiture", "items": [{ "variantId": "0192b21a-9c0d-7e1f-a2b3-4c5d6e7f8a9b", "quantity": 10 }, { "variantId": "0192b210-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "quantity": 20 }], "price": { "superGros": 1050000, "demiGros": 1150000, "detail": 1290000 }, "startsAt": "2027-01-01T00:00:00Z", "endsAt": "2027-01-31T22:59:59Z" }. Response 201 Created — the Pack (DRAFT).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Fewer than 2 items, pack price not below the items' total |
| 403 | PERMISSION_DENIED | Caller lacks promotions.create |
Rules & side effects
- Audit
CREATE.
Used by BO-52
PATCH /admin/packs/{packId}
admin.packs.crud (update) · Permission promotions.update · Apps BO · Release V2 · Requirements PRM-06
Purpose. Edits a pack (same locking rules as promotions). Response 200 OK — the Pack.
Errors
| Status | Code | When |
|---|---|---|
| 409 | PROMOTION_NOT_EDITABLE · VERSION_CONFLICT | Locked field · stale version |
Rules & side effects
- Audit
UPDATE.
Used by BO-52
DELETE /admin/packs/{packId}
admin.packs.crud (delete) · Permission promotions.delete · Apps BO · Release V2 · Requirements PRM-06
Purpose. Deletes a draft pack. Response 204 No Content.
Errors
| Status | Code | When |
|---|---|---|
| 409 | PROMOTION_NOT_EDITABLE | Not DRAFT |
Rules & side effects
- Audit
DELETE.
Used by BO-52
Loyalty (V2)
GET /admin/loyalty/settings
admin.loyalty.settings (get) · Permission loyalty.view · Apps BO · Release V2 · Requirements LOY-01 · LOY-03
Purpose. Earning rules, expiry and level thresholds.
Response 200 OK
{ "enabled": false, "earnOn": "DELIVERED", "pointsPerAmount": { "amount": 10000, "points": 1 }, "excludedCategoryIds": [], "pointsValidityMonths": 12, "levelPeriodMonths": 12, "levels": [{ "level": "BRONZE", "minPoints": 0 }, { "level": "SILVER", "minPoints": 2000 }, { "level": "GOLD", "minPoints": 6000 }, { "level": "VIP", "minPoints": 15000 }], "bonusRules": [{ "kind": "FIRST_ORDER", "points": 200 }], "version": 1 }Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks loyalty.view |
Rules & side effects
- Read-only; values mirror the
loyalty.*settings of the catalogue.
Used by BO-53
PUT /admin/loyalty/settings
admin.loyalty.settings (update) · Permission loyalty.administer · Apps BO · Release V2 · Requirements LOY-01 · LOY-03
Purpose. Replaces the programme settings (same shape as the read, with version and a reason).
Response 200 OK — the stored settings.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Thresholds not ascending, ratio ≤ 0 |
| 403 | PERMISSION_DENIED | Caller lacks loyalty.administer |
| 409 | VERSION_CONFLICT | Stale version |
Rules & side effects
- Apply to points earned from now on; levels recomputed nightly. Audit
UPDATEper changed key.
Used by BO-53
POST /admin/loyalty/accounts/{resellerId}/adjustments
admin.loyalty.adjust · Permission loyalty.update · Apps BO · Release V2 · Requirements LOY-02
Purpose. Manual points adjustment with a reason (cahier §4.6 « ajustements »).
Request — { "points": -150, "reason": "Points d'une commande retournée non annulés automatiquement." } (points ≠ 0).
Response 201 Created — { "transaction": { "id": "…", "type": "ADJUST", "points": -150, "reason": "…", "at": "2027-02-03T09:00:00Z" }, "balance": 2310 }
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks loyalty.update |
| 404 | NOT_FOUND | Unknown reseller |
| 409 | LOYALTY_DISABLED | Programme disabled |
| 422 | LOYALTY_INSUFFICIENT_POINTS · REASON_REQUIRED | Balance would become negative · empty reason |
Rules & side effects
- Appends a
LoyaltyTransactionADJUST; auditUPDATEwith the reason.
GET /admin/rewards
admin.rewards.crud (list) · Permission loyalty.view · Apps BO · Release V2 · Requirements LOY-04
Purpose. The rewards catalogue: Reward { id, name, type: RewardType, pointsCost, value?: Money, stock?, minLevel?: LoyaltyLevel, validityDays, active, image?, version }. Response 200 OK — Page<Reward>.
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks loyalty.view |
Rules & side effects
- Read-only.
Used by BO-54
GET /admin/rewards/{rewardId}
admin.rewards.crud (get) · Permission loyalty.view · Apps BO · Release V2 · Requirements LOY-04
Purpose. One reward. Response 200 OK — a Reward.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown reward |
Rules & side effects
- Read-only.
Used by BO-54
POST /admin/rewards
admin.rewards.crud (create) · Permission loyalty.create · Apps BO · Release V2 · Requirements LOY-04
Purpose. Adds a reward. Request — e.g. { "name": "Bon d'achat 2 000 DA", "type": "VOUCHER", "pointsCost": 2000, "value": 200000, "validityDays": 60, "stock": 100 }. Response 201 Created — the Reward.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | value missing for VOUCHER / DISCOUNT, cost ≤ 0 |
Rules & side effects
- Audit
CREATE.
Used by BO-54
PATCH /admin/rewards/{rewardId}
admin.rewards.crud (update) · Permission loyalty.update · Apps BO · Release V2 · Requirements LOY-04
Purpose. Edits or deactivates a reward. Response 200 OK — the Reward.
Errors
| Status | Code | When |
|---|---|---|
| 409 | VERSION_CONFLICT | Stale version |
Rules & side effects
- Unavailable rewards cannot be redeemed (cahier §5.5). Audit
UPDATE.
Used by BO-54
DELETE /admin/rewards/{rewardId}
admin.rewards.crud (delete) · Permission loyalty.delete · Apps BO · Release V2 · Requirements LOY-04
Purpose. Deletes a never-redeemed reward. Response 204 No Content.
Errors
| Status | Code | When |
|---|---|---|
| 409 | RESOURCE_IN_USE | Already redeemed — deactivate it |
Rules & side effects
- Audit
DELETE.
Used by BO-54
GET /admin/redemptions
admin.redemptions.list (list) · Permission loyalty.view · Apps BO · Release V2 · Requirements LOY-04
Purpose. Redemptions to fulfil (RedemptionStatus PENDING, FULFILLED, CANCELLED). Request — status[], rewardType[], resellerId, page / pageSize. Response 200 OK — Page<{ id, reseller, reward, points, status, requestedAt, fulfilledAt?, voucherCode? }>.
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks loyalty.view |
Rules & side effects
- Read-only (reading relaxed from the bundle's
loyalty.update— Assumption).
Used by BO-54
POST /admin/redemptions/{redemptionId}/fulfil
admin.redemptions.list (fulfil) · Permission loyalty.update · Apps BO · Release V2 · Requirements LOY-04
Purpose. Marks a redemption fulfilled (gift handed over, early access granted). Request — { "note": "Remis par le délégué le 3 février." }. Response 200 OK — the redemption with status FULFILLED.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown redemption |
| 409 | REDEMPTION_NOT_PENDING | Not PENDING |
Rules & side effects
- Audit
STATUS_CHANGE; reseller notified (in-app).
Used by BO-54
Finance
Back-office finance operations — invoices and their cancellation, payments recorded or declared and their verification, allocations, credit accounts with exposure and limit history, and the V2 overdue and accounting exports.
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.