HOCO B2B
SRSAPI referenceBack-office endpoints

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.

PermissionUnlocks on this page
promotions.viewPromotions, banners, packs (read), preview
promotions.create · promotions.update · promotions.deleteCreate · edit · delete drafts
promotions.validateSchedule, activate, pause, archive
loyalty.view · loyalty.create · loyalty.update · loyalty.delete · loyalty.administerV2 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.

MethodPathOperationAccessAppsRelease
GET · POST · PATCH · DELETE/admin/promotions[/{promotionId}]admin.promotions.crud
Promotions (type, value, scope, targeting, schedule, cap, stacking)
promotions.*BOV1
POST/admin/promotions/{promotionId}/statusadmin.promotions.setStatus
Schedule, activate, pause, archive
promotions.validateBOV1
POST/admin/promotions/previewadmin.promotions.preview
Simulate a promotion on a sample cart and reseller
promotions.viewBOV1
GET · POST · PATCH · DELETE/admin/banners[/{bannerId}]admin.banners.crud
Banners and daily offers
promotions.*BOV1
PUT/admin/banners/orderadmin.banners.reorder
Reorder the banners of a placement in one call
promotions.updateBOV1
GET · POST · PATCH · DELETE/admin/packs[/{packId}]admin.packs.crud
Packs (V2)
promotions.*BOV2
GET · PUT/admin/loyalty/settingsadmin.loyalty.settings
Earning rules, expiry, level thresholds (V2)
loyalty.administerBOV2
POST/admin/loyalty/accounts/{resellerId}/adjustmentsadmin.loyalty.adjust
Manual points adjustment with reason (V2)
loyalty.updateBOV2
GET · POST · PATCH · DELETE/admin/rewards[/{rewardId}]admin.rewards.crud
Rewards catalogue (V2)
loyalty.*BOV2
GET · POST/admin/redemptions[/{redemptionId}/fulfil]admin.redemptions.list
Redemptions to fulfil (V2)
loyalty.updateBOV2

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

QueryTypeValues / defaultNotes
statusPromotionStatus[]repeatable
typePromotionType[]repeatable
isDailyOfferboolean
activeOnstring (date)Running that day
brandId / categoryId / productIdUuidScope contains it
segmentId / wilayaCodeUuid / stringAudience contains it
qstringmin 2 charsName, label
sort / order / page / pageSizestartsAt (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

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks promotions.view

Rules & side effects

  1. 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

StatusCodeWhen
404NOT_FOUNDUnknown promotion

Rules & side effects

  1. 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

StatusCodeWhen
400VALIDATION_FAILEDValue 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
403PERMISSION_DENIEDCaller lacks promotions.create
404NOT_FOUNDUnknown id in scope or audience

Rules & side effects

  1. A FIXED_PRICE above a customer-type price never increases the price (the lower price wins, BR-65). Audit CREATE.
  2. Contract 1.1.0 — a FIXED_PRICE may carry fixedPrices: { SUPER_GROS, DEMI_GROS, DETAIL } (all three, each > 0): each customer type gets its own promotional price, and value may then be null. Without fixedPrices, the single value applies to every type, as before. The reseller catalog shows each reseller the price of its own type.
  3. SRV-42 — name (internal) and label (shown to resellers) are stored apart; notifyResellers defaults to false. perResellerCap limits 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 answers 409 CART_CHANGED with 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

StatusCodeWhen
400VALIDATION_FAILEDAs for creation
403PERMISSION_DENIEDCaller lacks promotions.update
404NOT_FOUNDUnknown promotion
409PROMOTION_NOT_EDITABLEField 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
409VERSION_CONFLICTStale version

Rules & side effects

  1. 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

StatusCodeWhen
404NOT_FOUNDUnknown promotion
409PROMOTION_NOT_EDITABLENot DRAFT — archive it with setStatus

Rules & side effects

  1. 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

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks promotions.validate
404NOT_FOUNDUnknown promotion
409PROMOTION_TRANSITION_NOT_ALLOWEDMove not in the table (e.g. ENDED → ACTIVE, resume after endsAt)
422VALIDATION_FAILEDScheduling guards fail (value, dates, empty scope)

Rules & side effects

  1. SCHEDULED → ACTIVE at startsAt and ACTIVE/PAUSED → ENDED at endsAt or when the cap is consumed are automatic (job promotions.applySchedule, every minute); the engine checks dates itself at every pricing.
  2. Audit STATUS_CHANGE.
  3. Notifications: at activation with notifyResellers → promotion.started to 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.

Used by BO-49 · BO-50

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

StatusCodeWhen
400VALIDATION_FAILEDNeither or both of promotionId / draft, both resellerId and customerType, empty lines
403PERMISSION_DENIEDCaller lacks promotions.view
404NOT_FOUNDUnknown promotion, reseller or variant
422CUSTOMER_TYPE_REQUIREDThe reseller has no customer type yet

Rules & side effects

  1. 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

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks promotions.view

Rules & side effects

  1. 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

StatusCodeWhen
404NOT_FOUNDUnknown banner

Rules & side effects

  1. 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

StatusCodeWhen
400VALIDATION_FAILEDURL link not https, targetId missing for a typed link, endsAt before startsAt
403PERMISSION_DENIEDCaller lacks promotions.create
404NOT_FOUNDUnknown link target or promotion
422FILE_NOT_READY · FILE_PURPOSE_MISMATCHImage not uploaded or not BANNER_IMAGE

Rules & side effects

  1. A banner linked to a promotion is hidden while the promotion is not ACTIVE (BR-68). Audit CREATE.
  2. SRV-42 — every field is stored: imageMobileFileId (BANNER_IMAGE, checked like the image), the full audience (loyalty levels excepted, V2), countdown (default: the linked promotion is a daily offer), and draft: true (status DRAFT, never served to resellers or guests until a PATCH sets ACTIVE or PAUSED). audience.guests defaults to true only 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

StatusCodeWhen
404NOT_FOUNDUnknown banner
409PROMOTION_TRANSITION_NOT_ALLOWED · VERSION_CONFLICTStatus move not allowed · stale version

Rules & side effects

  1. 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

StatusCodeWhen
404NOT_FOUNDUnknown banner

Rules & side effects

  1. 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

StatusCodeWhen
400VALIDATION_FAILEDids misses a banner of the placement, names another one, or repeats one (field ids)
403PERMISSION_DENIEDCaller lacks promotions.update

Rules & side effects

  1. sortOrder becomes 10, 20, 30… in the given order, in one transaction; each moved banner gets an audit UPDATE (sortOrder from → to) and a new version.

Used by BO-51

Packs (V2)

V2

Contract reserved; not built in V1 (cahier §14.2 « packs rentables », PRM-06).

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

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks promotions.view

Rules & side effects

  1. 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

StatusCodeWhen
404NOT_FOUNDUnknown pack

Rules & side effects

  1. 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

StatusCodeWhen
400VALIDATION_FAILEDFewer than 2 items, pack price not below the items' total
403PERMISSION_DENIEDCaller lacks promotions.create

Rules & side effects

  1. 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

StatusCodeWhen
409PROMOTION_NOT_EDITABLE · VERSION_CONFLICTLocked field · stale version

Rules & side effects

  1. 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

StatusCodeWhen
409PROMOTION_NOT_EDITABLENot DRAFT

Rules & side effects

  1. Audit DELETE.

Used by BO-52

Loyalty (V2)

V2

Contract reserved; not built in V1 (cahier §4.6, §14.2 « Fidélité avancée »). Rules: BR-40 to BR-44.

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

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks loyalty.view

Rules & side effects

  1. 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

StatusCodeWhen
400VALIDATION_FAILEDThresholds not ascending, ratio ≤ 0
403PERMISSION_DENIEDCaller lacks loyalty.administer
409VERSION_CONFLICTStale version

Rules & side effects

  1. Apply to points earned from now on; levels recomputed nightly. Audit UPDATE per 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

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks loyalty.update
404NOT_FOUNDUnknown reseller
409LOYALTY_DISABLEDProgramme disabled
422LOYALTY_INSUFFICIENT_POINTS · REASON_REQUIREDBalance would become negative · empty reason

Rules & side effects

  1. Appends a LoyaltyTransaction ADJUST; audit UPDATE with the reason.

Used by BO-53 · BO-11

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

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks loyalty.view

Rules & side effects

  1. 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

StatusCodeWhen
404NOT_FOUNDUnknown reward

Rules & side effects

  1. 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

StatusCodeWhen
400VALIDATION_FAILEDvalue missing for VOUCHER / DISCOUNT, cost ≤ 0

Rules & side effects

  1. 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

StatusCodeWhen
409VERSION_CONFLICTStale version

Rules & side effects

  1. 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

StatusCodeWhen
409RESOURCE_IN_USEAlready redeemed — deactivate it

Rules & side effects

  1. 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

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks loyalty.view

Rules & side effects

  1. 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

StatusCodeWhen
404NOT_FOUNDUnknown redemption
409REDEMPTION_NOT_PENDINGNot PENDING

Rules & side effects

  1. Audit STATUS_CHANGE; reseller notified (in-app).

Used by BO-54

On this page