Catalog & pricing
Back-office catalog operations — brands, category tree, filterable attributes, products and variants with their three customer-type prices, visibility, price history, product import and export.
These operations administer the B2B catalog of cahier §4.3 and §15.4: brands HOCO, ACEFAST and BOROFONE, the category tree (chargers, cables, earphones, power banks, car holders, audio, watches, accessories — with sub-categories), products and their variants, images, visibility per segment or reseller, prices and their history, and Excel/CSV import and export. Screens: products list and editor (BO-27, BO-28), brands (BO-29), categories (BO-30), the price screen (BO-31), the import wizard (BO-32) and product attributes (BO-71).
Pricing model (decision of 17 Oct 2026). Every variant has three selling prices, one per customer type — priceSuperGros, priceDemiGros, priceDetail (centimes, HT) — plus its vatRate, a recommendedPrice (prix conseillé, TTC) and the restricted purchasePrice (HT). A reseller pays the price of their customerType; promotions apply on top (PRC-01, PRC-02). Guests see no price. Segment price lists and quantity tiers are not part of V1: the admin.priceLists.* operations are reserved for the V2 per-reseller agreements (PRC-03) and admin.variants.tiers is dropped — both are documented at the end of this page as such and are not mounted by the V1 server.
Filterable attributes (CAT-12, CAT-13). Each category defines its attributes (power in W, connector, cable length, colour, battery capacity in mAh, Bluetooth version…) with admin.attributes.crud; every variant of a product in that category carries values for them, which feed the reseller filters and counts of catalog.facets.
Who uses it. The Responsable stock maintains products, categories, variants and attributes (cahier §3); the Administrateur commercial manages prices and visibility; the Super administrateur imports the initial catalog. Purchase prices and margins are visible only to holders of pricing.view (cahier §4.7 « Les droits d'accès aux données financières sont strictement limités », ACL-04).
| Permission | Unlocks on this page |
|---|---|
catalog.view | Brands, categories, attributes, products and variants (read) |
catalog.create · catalog.update · catalog.delete | Create · edit (incl. status, visibility, variants, images) · delete or archive |
catalog.administer | Product import; restoring an archived product |
catalog.export | Catalog export |
pricing.view | purchasePrice, margins and the price history |
pricing.update | Writing any price field (priceSuperGros, priceDemiGros, priceDetail, recommendedPrice, purchasePrice, vatRate) |
Settings used (settings keys): catalog.newProductDays (30 — « Nouveauté » badge), pricing.defaultVatRate (1900), stock.lowThresholdDefault.
Audit. CREATE, UPDATE, DELETE and STATUS_CHANGE rows on Brand, Category, AttributeDefinition, Product, ProductVariant, ProductVisibilityRule; IMPORT and EXPORT for the jobs. Every price change also writes a PriceChange row (history, AUD-02).
Conventions: lists return Page<T> = { items, total, page, pageSize } (responses); FileRef, Price, UserSummary are shared schemas; money in centimes, VAT in basis points; images are uploaded with files.createUpload (purpose PRODUCT_IMAGE or BRAND_LOGO, public CDN) and referenced by fileId; editable records carry version (409 VERSION_CONFLICT). 401, 429 and 500 apply everywhere and are not repeated.
| Method | Path | Operation | Access | Apps | Release |
|---|---|---|---|---|---|
| GET · POST · PATCH · DELETE | /admin/brands[/{brandId}] | admin.brands.crudBrands CRUD (delete = archive when referenced) | catalog.* | BO | V1 |
| GET · POST · PATCH · DELETE | /admin/categories[/{categoryId}] | admin.categories.crudCategories CRUD | catalog.* | BO | V1 |
| PUT | /admin/categories/order | admin.categories.reorderReorder / re-parent categories | catalog.update | BO | V1 |
| GET | /admin/products | admin.products.listProducts with status, brand, category and stock filters | catalog.view | BO | V1 |
| POST | /admin/products | admin.products.createCreate a product (draft) with its default variant | catalog.create | BO | V1 |
| GET | /admin/products/{productId} | admin.products.getFull product for the editor (incl. purchase price if permitted) | catalog.view | BO | V1 |
| PATCH | /admin/products/{productId} | admin.products.updateEdit product data, specs, images order | catalog.update | BO | V1 |
| POST | /admin/products/{productId}/status | admin.products.setStatusPublish, hide, archive | catalog.update | BO | V1 |
| PUT | /admin/products/{productId}/visibility | admin.products.visibilityVisibility rules per segment or reseller | catalog.update | BO | V1 |
| POST · PATCH · DELETE | /admin/products/{productId}/variants[/{variantId}] | admin.variants.crudVariants CRUD (SKU, barcode, attributes, prices, limits) | catalog.update | BO | V1 |
| PUT | /admin/variants/{variantId}/quantity-tiers | admin.variants.tiersQuantity discount tiers — dropped from V1 (three customer-type prices instead); kept as a V2 option | pricing.update | BO | V2 |
| GET | /admin/variants/{variantId}/price-history | admin.variants.priceHistoryPrice change history | pricing.view | BO | V1 |
| GET | /admin/variants/prices | admin.variants.pricesPrice grid: one row per variant with its prices (BO-31) | catalog.view | BO | V1 |
| PUT | /admin/variants/prices | admin.variants.bulkPricesSave many variants' prices in one call (PriceChange rows, per-variant failures) | pricing.update | BO | V1 |
| GET · POST · PATCH · DELETE | /admin/attribute-definitions[/{attributeId}] | admin.attributes.crudFilterable attribute definitions per category (key, label, type, unit, allowed values, filter widget) | catalog.* | BO | V1 |
| GET · POST · PATCH · DELETE | /admin/price-lists[/{priceListId}] | admin.priceLists.crudPer-reseller price agreements (V2) | pricing.* | BO | V2 |
| PUT | /admin/price-lists/{priceListId}/items | admin.priceLists.itemsBulk upsert price list items (V2) | pricing.update | BO | V2 |
| POST | /admin/imports/products | admin.products.importImport products from an uploaded Excel/CSV (dry-run then apply) | catalog.administer | BO | V1 |
| POST | /admin/products/export | admin.products.exportExport the catalog (async) | catalog.export | BO | V1 |
Schemas
Brand
interface Brand {
id: Uuid;
name: string; // unique: "HOCO", "ACEFAST", "BOROFONE"
slug: string; // "hoco"
logo?: FileRef; // BRAND_LOGO, public
description?: string;
status: "ACTIVE" | "ARCHIVED";
sortOrder: number;
productCount: number; // non-archived products
createdAt: Iso; updatedAt: Iso; version: number;
}Category
interface Category {
id: Uuid;
name: string; // unique among siblings
slug: string;
parentId: Uuid | null; // null = top-level category; set = sub-category (max depth 2)
path: { id: Uuid; name: string }[]; // from the root, this category included
icon?: FileRef;
sortOrder: number;
status: "ACTIVE" | "ARCHIVED";
productCount: number; // including sub-categories
attributeIds: Uuid[]; // attribute definitions that apply here (own + inherited from the parent)
children?: Category[]; // tree format only
createdAt: Iso; updatedAt: Iso; version: number;
}AttributeDefinition
interface AttributeDefinition {
id: Uuid;
key: string; // snake_case, unique, immutable: "power_w", "connector"
label: { fr: string; ar?: string; en?: string };
type: "NUMBER" | "ENUM" | "BOOLEAN" | "TEXT"; // AttributeType
unit?: string; // NUMBER: "W", "mAh", "m"
decimals?: number; // NUMBER: 0–2
values?: { code: string; label: { fr: string; ar?: string; en?: string }; sortOrder: number; active: boolean }[]; // ENUM
multiValue: boolean; // ENUM: a variant may carry several values (e.g. connectors of a 3-in-1 cable)
categoryIds: Uuid[]; // applies to these categories and their sub-categories
filterable: boolean; // shown as a filter in the reseller apps
filterWidget: "CHECKBOX" | "RANGE" | "TOGGLE" | "NONE"; // FilterWidget: ENUM → CHECKBOX, NUMBER → RANGE or CHECKBOX, BOOLEAN → TOGGLE
requiredToPublish: boolean; // every active variant needs a value before the product can be published
minValue?: number; // NUMBER: lowest accepted value (contract 1.1.0)
maxValue?: number; // NUMBER: highest accepted value (contract 1.1.0)
showOnProductSheet: boolean; // listed in the product sheet's specifications (contract 1.1.0); filters and variant choice keep it either way
sortOrder: number;
status: "ACTIVE" | "ARCHIVED";
variantCount: number; // variants carrying a value
createdAt: Iso; updatedAt: Iso; version: number;
}AdminVariant
interface AdminVariant {
id: Uuid;
sku: string; // unique, immutable once ordered: "HC-X14-CC-1M-BK"
barcode?: string; // EAN-13 / UPC, unique
name: string; // "Noir · 1 m"
attributes: Record<string, string | number | boolean | string[]>; // VariantAttributeValue by attribute key
priceSuperGros: Money; // HT — price of SUPER_GROS resellers
priceDemiGros: Money; // HT — DEMI_GROS
priceDetail: Money; // HT — DETAIL
pricesTtc: { superGros: Money; demiGros: Money; detail: Money }; // computed with vatRate
vatRate: BasisPoints;
recommendedPrice?: Money; // prix conseillé public, TTC
purchasePrice?: Money; // HT — only with pricing.view
marginsBps?: { superGros: number; demiGros: number; detail: number }; // (price − purchase) ÷ price — only with pricing.view
minQty: number; // ≥ 1
qtyStep: number; // ≥ 1; quantities must be minQty + k × qtyStep
maxQtyPerOrder?: number;
weightGrams?: number;
status: "ACTIVE" | "HIDDEN" | "ARCHIVED"; // ProductStatus subset
isDefault: boolean;
stock: { physical: number; reserved: number; available: number; alertLevel: "OK" | "LOW" | "OUT" }; // all warehouses
availability: "IN_STOCK" | "OUT_OF_STOCK"; // what resellers see
createdAt: Iso; updatedAt: Iso; version: number;
}TTC = round(HT × (10 000 + vatRate) ÷ 10 000) to the centime: 35 000 HT at 19 % → 41 650 TTC.
AdminProduct
interface AdminProduct {
id: Uuid;
reference: string; // unique per brand: "HC-X14"
name: string;
slug: string;
brand: { id: Uuid; name: string };
category: { id: Uuid; name: string; parentId: Uuid | null; path: { id: Uuid; name: string }[] };
description: string; // plain text with line breaks, ≤ 5 000
specs: { label: string; value: string }[]; // free technical characteristics (cahier §15.4)
warranty?: { months: number; text?: string };
status: ProductStatus; // DRAFT, ACTIVE, HIDDEN, OUT_OF_STOCK, ARCHIVED
publishedAt?: Iso;
isNew: boolean; // publishedAt within catalog.newProductDays
images: { id: Uuid; file: FileRef; sortOrder: number; alt?: string; variantId?: Uuid }[];
variants: AdminVariant[];
defaultVariantId: Uuid;
visibility: { mode: "EVERYONE" | "INCLUDE_ONLY" | "EXCLUDE"; segments: { id: Uuid; name: string }[]; resellers: ResellerSummary[] };
completeness: { publishable: boolean; missing: string[] }; // e.g. ["images", "variants[0].attributes.power_w"]
activePromotions: { id: Uuid; name: string; endsAt: Iso }[];
createdAt: Iso; updatedAt: Iso; version: number;
}Brands
GET /admin/brands
admin.brands.crud (list) · Permission catalog.view · Apps BO · Release V1 · Requirements CAT-01
Purpose. Lists brands (managed as data, not hard-coded — cahier §4).
Request
| Query | Type | Values / default | Notes |
|---|---|---|---|
status | string | ACTIVE | ARCHIVED; default both | |
q | string | min 2 chars | Name |
sort / order / page / pageSize | sortOrder (default), name, productCount · asc · 1 · 25 (max 200) |
Response 200 OK
{
"items": [
{ "id": "01928a01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "name": "HOCO", "slug": "hoco", "logo": { "id": "01928a02-1a2b-7c3d-8e4f-5a6b7c8d9e02", "url": "https://cdn.hoco.dz/brands/hoco.svg", "mimeType": "image/svg+xml", "size": 4211, "purpose": "BRAND_LOGO" }, "status": "ACTIVE", "sortOrder": 1, "productCount": 412, "createdAt": "2026-10-20T08:00:00Z", "updatedAt": "2026-10-20T08:00:00Z", "version": 1 },
{ "id": "01928a01-2b3c-7d4e-9f5a-6b7c8d9e0f12", "name": "ACEFAST", "slug": "acefast", "status": "ACTIVE", "sortOrder": 2, "productCount": 138, "createdAt": "2026-10-20T08:00:00Z", "updatedAt": "2026-10-20T08:00:00Z", "version": 1 }
],
"total": 3,
"page": 1,
"pageSize": 25
}Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks catalog.view |
Rules & side effects
- Read-only.
Used by BO-29 · BO-27 (filter) · BO-04 (filter)
GET /admin/brands/{brandId}
admin.brands.crud (get) · Permission catalog.view · Apps BO · Release V1 · Requirements CAT-01
Purpose. One brand.
Response 200 OK — a Brand.
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks catalog.view |
| 404 | NOT_FOUND | Unknown brand |
Rules & side effects
- Read-only.
Used by BO-29
POST /admin/brands
admin.brands.crud (create) · Permission catalog.create · Apps BO · Release V1 · Requirements CAT-01
Purpose. Adds a brand.
Request
interface CreateBrandBody { name: string; logoFileId?: Uuid; description?: string; sortOrder?: number }{ "name": "BOROFONE", "logoFileId": "01928a02-3c4d-7e5f-8a6b-7c8d9e0f1a2b", "sortOrder": 3 }Response 201 Created — the Brand (slug borofone, status ACTIVE, productCount 0, version 1).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Name shorter than 2 or longer than 60 characters |
| 403 | PERMISSION_DENIED | Caller lacks catalog.create |
| 409 | DUPLICATE_VALUE | Name or slug already used |
| 422 | FILE_NOT_READY · FILE_PURPOSE_MISMATCH | Logo not uploaded, or purpose not BRAND_LOGO |
Rules & side effects
- Audit
CREATE. The reseller apps see the brand once it has an active product (catalog.brands).
Used by BO-29
PATCH /admin/brands/{brandId}
admin.brands.crud (update) · Permission catalog.update · Apps BO · Release V1 · Requirements CAT-01
Purpose. Edits a brand, or restores an archived one.
Request
interface UpdateBrandBody { name?: string; logoFileId?: Uuid | null; description?: string | null; sortOrder?: number; status?: "ACTIVE" | "ARCHIVED"; version: number }{ "sortOrder": 1, "version": 1 }Response 200 OK — the updated Brand.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Invalid name |
| 403 | PERMISSION_DENIED | Caller lacks catalog.update |
| 404 | NOT_FOUND | Unknown brand |
| 409 | DUPLICATE_VALUE · VERSION_CONFLICT | Name taken · stale version |
Rules & side effects
- Archiving a brand (
status: ARCHIVED) hides it from brand filters; its products keep their own status (archive them explicitly). AuditUPDATE.
Used by BO-29
DELETE /admin/brands/{brandId}
admin.brands.crud (delete) · Permission catalog.delete · Apps BO · Release V1 · Requirements CAT-01
Purpose. Deletes a brand — or archives it when products reference it.
Response 200 OK
{ "id": "01928a01-9f0a-7b1c-8d2e-3f4a5b6c7d8e", "result": "ARCHIVED", "references": [{ "entityType": "Product", "count": 3 }] }result is DELETED (no product ever referenced the brand) or ARCHIVED.
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks catalog.delete |
| 404 | NOT_FOUND | Unknown brand |
Rules & side effects
- Audit
DELETE(orSTATUS_CHANGEwhen archived). Past orders and invoices keep the brand name in their snapshot.
Used by BO-29
Categories
GET /admin/categories
admin.categories.crud (list) · Permission catalog.view · Apps BO · Release V1 · Requirements CAT-02
Purpose. The category tree (categories and sub-categories) or a flat list.
Request
| Query | Type | Values / default | Notes |
|---|---|---|---|
format | string | tree (default) | flat | tree is not paginated |
status | string | ACTIVE | ARCHIVED; default both | |
q | string | min 2 chars | flat only |
Response 200 OK — format=tree
{
"items": [
{
"id": "01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "name": "Câbles", "slug": "cables", "parentId": null,
"path": [{ "id": "01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "name": "Câbles" }],
"sortOrder": 2, "status": "ACTIVE", "productCount": 186,
"attributeIds": ["0192b100-3c4d-7e5f-9a6b-7c8d9e0f1a2b", "0192b100-4d5e-7f6a-8b7c-8d9e0f1a2b3c", "0192b100-5e6f-7a7b-9c8d-9e0f1a2b3c4d"],
"createdAt": "2026-10-20T08:10:00Z", "updatedAt": "2026-10-20T08:10:00Z", "version": 1,
"children": [
{
"id": "01928b02-2b3c-7d4e-9f5a-6b7c8d9e0f12", "name": "Câbles USB-C", "slug": "cables-usb-c", "parentId": "01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01",
"path": [{ "id": "01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "name": "Câbles" }, { "id": "01928b02-2b3c-7d4e-9f5a-6b7c8d9e0f12", "name": "Câbles USB-C" }],
"sortOrder": 1, "status": "ACTIVE", "productCount": 74,
"attributeIds": ["0192b100-3c4d-7e5f-9a6b-7c8d9e0f1a2b", "0192b100-4d5e-7f6a-8b7c-8d9e0f1a2b3c", "0192b100-5e6f-7a7b-9c8d-9e0f1a2b3c4d", "0192b100-1a2b-7c3d-9e4f-5a6b7c8d9e01"],
"createdAt": "2026-10-20T08:11:00Z", "updatedAt": "2026-10-20T08:11:00Z", "version": 1, "children": []
}
]
}
],
"total": 31
}Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Unknown format |
| 403 | PERMISSION_DENIED | Caller lacks catalog.view |
Rules & side effects
- Read-only.
totalcounts all nodes.format=flatreturnsPage<Category>withoutchildren.
Used by BO-30 · BO-27 · BO-28 · BO-71
GET /admin/categories/{categoryId}
admin.categories.crud (get) · Permission catalog.view · Apps BO · Release V1 · Requirements CAT-02
Purpose. One category with its children and the attribute definitions that apply to it.
Response 200 OK — a Category with children and an extra attributes: AttributeDefinition[] (own and inherited).
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks catalog.view |
| 404 | NOT_FOUND | Unknown category |
Rules & side effects
- Read-only.
POST /admin/categories
admin.categories.crud (create) · Permission catalog.create · Apps BO · Release V1 · Requirements CAT-02
Purpose. Creates a category or a sub-category.
Request
interface CreateCategoryBody { name: string; parentId?: Uuid | null; iconFileId?: Uuid; sortOrder?: number }{ "name": "Câbles Lightning", "parentId": "01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "sortOrder": 2 }Response 201 Created — the Category; it inherits the parent's attributes.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Name shorter than 2 or longer than 60 characters |
| 403 | PERMISSION_DENIED | Caller lacks catalog.create |
| 404 | NOT_FOUND | Unknown parent |
| 409 | DUPLICATE_VALUE | Same name under the same parent |
| 422 | CATEGORY_DEPTH_EXCEEDED | The parent is already a sub-category (max depth 2: category › sub-category) |
Rules & side effects
- Audit
CREATE. Empty categories are not shown to resellers (catalog.categoriescounts visible products).
Used by BO-30
PATCH /admin/categories/{categoryId}
admin.categories.crud (update) · Permission catalog.update · Apps BO · Release V1 · Requirements CAT-02
Purpose. Renames, re-icons, archives or restores a category (moving it is done with reorder).
Request
interface UpdateCategoryBody { name?: string; iconFileId?: Uuid | null; sortOrder?: number; status?: "ACTIVE" | "ARCHIVED"; version: number }{ "name": "Câbles USB-C et Thunderbolt", "version": 1 }Response 200 OK — the updated Category.
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks catalog.update |
| 404 | NOT_FOUND | Unknown category |
| 409 | DUPLICATE_VALUE · VERSION_CONFLICT | Sibling with the same name · stale version |
| 409 | RESOURCE_IN_USE | Archiving a category that still has active products or active children |
Rules & side effects
- Audit
UPDATE.
Used by BO-30
DELETE /admin/categories/{categoryId}
admin.categories.crud (delete) · Permission catalog.delete · Apps BO · Release V1 · Requirements CAT-02
Purpose. Deletes an empty category.
Response 204 No Content
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks catalog.delete |
| 404 | NOT_FOUND | Unknown category |
| 409 | RESOURCE_IN_USE | It has sub-categories or products (any status) — details.references; move them or archive the category |
Rules & side effects
- Attribute definitions linked only to this category stay (they can be re-linked). Audit
DELETE.
Used by BO-30
PUT /admin/categories/order
admin.categories.reorder · Permission catalog.update · Apps BO · Release V1 · Requirements CAT-02
Purpose. Saves the drag-and-drop result of the category editor: order and parent of every node.
Request
interface ReorderCategoriesBody {
items: { id: Uuid; parentId: Uuid | null; sortOrder: number }[]; // the whole tree
}{
"items": [
{ "id": "01928b01-0a1b-7c2d-8e3f-4a5b6c7d8e9f", "parentId": null, "sortOrder": 1 },
{ "id": "01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "parentId": null, "sortOrder": 2 },
{ "id": "01928b02-2b3c-7d4e-9f5a-6b7c8d9e0f12", "parentId": "01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "sortOrder": 1 }
]
}Response 200 OK — the tree as in admin.categories.crud (format=tree).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | A category missing from items or listed twice, unknown id |
| 403 | PERMISSION_DENIED | Caller lacks catalog.update |
| 422 | CATEGORY_CYCLE | A node would be its own ancestor |
| 422 | CATEGORY_DEPTH_EXCEEDED | A node would sit under a sub-category, or a category with children would become a sub-category |
Rules & side effects
- All-or-nothing. Re-parenting changes the inherited attributes of the moved node: values of attributes that no longer apply stay stored but are ignored by filters, and the response lists them in
warnings: [{ categoryId, orphanAttributeKeys }]. - Audit one
UPDATErow per moved node.
Used by BO-30
Products
GET /admin/products
admin.products.list · Permission catalog.view · Apps BO · Release V1 · Requirements CAT-03
Purpose. The products table with status, brand, category, stock and price filters.
Request
| Query | Type | Values / default | Notes |
|---|---|---|---|
status | ProductStatus[] | repeatable | |
brandId | Uuid[] | repeatable | |
categoryId | Uuid[] | repeatable | Includes sub-categories |
availability | string | IN_STOCK | OUT_OF_STOCK | As resellers see it |
alertLevel | string[] | OK | LOW | OUT | Worst level among variants and warehouses |
visibility | string | EVERYONE | INCLUDE_ONLY | EXCLUDE | |
incomplete | boolean | true: cannot be published yet (completeness.publishable = false) | |
hasPromotion / isNew | boolean | ||
attr.<key> | string | e.g. attr.connector=USB_C, attr.power_w=20..65 | Attribute filters (ENUM: values separated by ,; NUMBER: min..max) |
priceDetailMin / priceDetailMax | Money | HT, on the default variant | |
q | string | min 2 chars | Name, reference, SKU, barcode (exact) |
sort | string | updatedAt (default), name, reference, publishedAt, available, priceDetail | |
order / page / pageSize | asc | desc (default desc) · 1 · 25 (max 200) |
Response 200 OK
{
"items": [
{
"id": "0192b215-6f70-7a81-9b2c-3d4e5f6a7b80",
"reference": "AF-A29",
"name": "Chargeur GaN 50 W 2 ports",
"brand": { "id": "01928a01-2b3c-7d4e-9f5a-6b7c8d9e0f12", "name": "ACEFAST" },
"category": { "id": "01928b01-0a1b-7c2d-8e3f-4a5b6c7d8e9f", "name": "Chargeurs", "parentId": null },
"status": "ACTIVE",
"imageUrl": "https://cdn.hoco.dz/p/af-a29/main-400.webp",
"variantsCount": 2,
"defaultVariant": { "id": "0192b215-7a8b-7c9d-8e0f-1a2b3c4d5e6f", "sku": "AF-A29-50W-WH", "priceSuperGros": 195000, "priceDemiGros": 210000, "priceDetail": 230000, "vatRate": 1900 },
"stock": { "physical": 46, "reserved": 40, "available": 6, "alertLevel": "LOW" },
"availability": "IN_STOCK",
"visibilityMode": "EVERYONE",
"isNew": true,
"hasPromotion": true,
"completeness": { "publishable": true, "missing": [] },
"publishedAt": "2026-10-22T09:00:00Z",
"updatedAt": "2026-11-02T14:31:00Z"
}
],
"total": 583,
"page": 1,
"pageSize": 25
}Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Unknown status, sort field or attribute key, malformed range |
| 403 | PERMISSION_DENIED | Caller lacks catalog.view |
Rules & side effects
- Read-only. Stock figures sum all warehouses (per-warehouse detail in
admin.stock.levels); quantities exist only in the back-office (STK-07).
Used by BO-27 · BO-31 · BO-50 (scope picker) · BO-40 (product picker)
POST /admin/products
admin.products.create · Permission catalog.create (+ pricing.update for price fields) · Apps BO · Release V1 · Requirements CAT-03
Purpose. Creates a product in DRAFT with its default variant.
Request
interface CreateProductBody {
reference: string; // unique per brand, 2–40, uppercase letters, digits and "-"
name: string; // 3–150
brandId: Uuid;
categoryId: Uuid; // a sub-category when the category has children
description?: string;
specs?: { label: string; value: string }[];
warranty?: { months: number; text?: string };
defaultVariant: {
sku: string; barcode?: string; name?: string;
attributes?: Record<string, string | number | boolean | string[]>;
priceSuperGros?: Money; priceDemiGros?: Money; priceDetail?: Money; // HT; need pricing.update
vatRate?: BasisPoints; // default pricing.defaultVatRate
recommendedPrice?: Money; purchasePrice?: Money;
minQty?: number; qtyStep?: number; maxQtyPerOrder?: number; weightGrams?: number;
};
}{
"reference": "HC-X14",
"name": "Câble USB-C vers USB-C 60 W 1 m",
"brandId": "01928a01-1a2b-7c3d-8e4f-5a6b7c8d9e01",
"categoryId": "01928b02-2b3c-7d4e-9f5a-6b7c8d9e0f12",
"description": "Câble tressé nylon, charge rapide PD jusqu'à 60 W, transfert de données 480 Mb/s.",
"specs": [{ "label": "Gaine", "value": "Nylon tressé" }, { "label": "Débit", "value": "480 Mb/s" }],
"warranty": { "months": 6 },
"defaultVariant": {
"sku": "HC-X14-CC-1M-BK", "barcode": "6931474761234", "name": "Noir · 1 m",
"attributes": { "connector": ["USB_C"], "cable_length_m": 1, "power_w": 60, "color": "BLACK" },
"priceSuperGros": 32000, "priceDemiGros": 35000, "priceDetail": 39000, "vatRate": 1900,
"recommendedPrice": 55000, "purchasePrice": 21000, "minQty": 5, "qtyStep": 5, "maxQtyPerOrder": 500, "weightGrams": 38
}
}Response 201 Created — the AdminProduct with status DRAFT, one variant, completeness: { "publishable": false, "missing": ["images"] }, version 1.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Bad reference format, value of the wrong type for an attribute, unknown ENUM code, qtyStep < 1 |
| 403 | PERMISSION_DENIED | Caller lacks catalog.create, or sends price fields without pricing.update — details: { required: "pricing.update", fields: ["priceDetail"] } |
| 404 | NOT_FOUND | Unknown brand or category |
| 409 | DUPLICATE_VALUE | Reference (per brand), SKU or barcode already used |
| 422 | PRICE_ORDER_INVALID | Prices not ordered priceSuperGros ≤ priceDemiGros ≤ priceDetail |
Rules & side effects
- A product is invisible to resellers until published (setStatus). Attribute keys must belong to the category's attributes.
- Audit
CREATEonProductandProductVariant; onePriceChangerow per price field set (before: null).
Used by BO-28
GET /admin/products/{productId}
admin.products.get · Permission catalog.view · Apps BO · Release V1 · Requirements CAT-03 · ACL-04
Purpose. The full product for the editor: data, images, variants with their three prices and attributes, visibility, completeness — purchase price and margins only if permitted.
Response 200 OK — AdminProduct
{
"id": "0192b210-4d5e-7f60-8a1b-2c3d4e5f6a70",
"reference": "HC-X14",
"name": "Câble USB-C vers USB-C 60 W 1 m",
"slug": "hoco-hc-x14-cable-usb-c-60w-1m",
"brand": { "id": "01928a01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "name": "HOCO" },
"category": { "id": "01928b02-2b3c-7d4e-9f5a-6b7c8d9e0f12", "name": "Câbles USB-C", "parentId": "01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "path": [{ "id": "01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01", "name": "Câbles" }, { "id": "01928b02-2b3c-7d4e-9f5a-6b7c8d9e0f12", "name": "Câbles USB-C" }] },
"description": "Câble tressé nylon, charge rapide PD jusqu'à 60 W, transfert de données 480 Mb/s.",
"specs": [{ "label": "Gaine", "value": "Nylon tressé" }, { "label": "Débit", "value": "480 Mb/s" }],
"warranty": { "months": 6 },
"status": "ACTIVE",
"publishedAt": "2026-10-21T10:00:00Z",
"isNew": true,
"images": [
{ "id": "0192b210-6a7b-7c8d-9e0f-1a2b3c4d5e6f", "file": { "id": "0192b210-7b8c-7d9e-8f0a-1b2c3d4e5f60", "url": "https://cdn.hoco.dz/p/hc-x14/main.webp", "thumbnailUrl": "https://cdn.hoco.dz/p/hc-x14/main-400.webp", "mimeType": "image/webp", "size": 186410, "purpose": "PRODUCT_IMAGE" }, "sortOrder": 1, "alt": "Câble HOCO X14 noir" }
],
"variants": [
{
"id": "0192b210-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"sku": "HC-X14-CC-1M-BK",
"barcode": "6931474761234",
"name": "Noir · 1 m",
"attributes": { "connector": ["USB_C"], "cable_length_m": 1, "power_w": 60, "color": "BLACK" },
"priceSuperGros": 32000,
"priceDemiGros": 35000,
"priceDetail": 39000,
"pricesTtc": { "superGros": 38080, "demiGros": 41650, "detail": 46410 },
"vatRate": 1900,
"recommendedPrice": 55000,
"purchasePrice": 21000,
"marginsBps": { "superGros": 3438, "demiGros": 4000, "detail": 4615 },
"minQty": 5,
"qtyStep": 5,
"maxQtyPerOrder": 500,
"weightGrams": 38,
"status": "ACTIVE",
"isDefault": true,
"stock": { "physical": 1240, "reserved": 160, "available": 1080, "alertLevel": "OK" },
"availability": "IN_STOCK",
"createdAt": "2026-10-21T09:40:00Z",
"updatedAt": "2026-10-28T11:02:00Z",
"version": 3
}
],
"defaultVariantId": "0192b210-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"visibility": { "mode": "EVERYONE", "segments": [], "resellers": [] },
"completeness": { "publishable": true, "missing": [] },
"activePromotions": [],
"createdAt": "2026-10-21T09:40:00Z",
"updatedAt": "2026-10-28T11:02:00Z",
"version": 5
}Field notes. purchasePrice and marginsBps — only with pricing.view; omitted otherwise (not null). Margins: (32 000 − 21 000) ÷ 32 000 = 34,38 %; (35 000 − 21 000) ÷ 35 000 = 40 %; (39 000 − 21 000) ÷ 39 000 = 46,15 %.
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks catalog.view |
| 404 | NOT_FOUND | Unknown product |
Rules & side effects
- Read-only.
PATCH /admin/products/{productId}
admin.products.update · Permission catalog.update · Apps BO · Release V1 · Requirements CAT-03
Purpose. Edits product data, specifications, warranty, category and the ordered image gallery.
Request
interface UpdateProductBody {
name?: string;
reference?: string; // only while no variant has been ordered
brandId?: Uuid;
categoryId?: Uuid;
description?: string;
specs?: { label: string; value: string }[]; // replaces the list
warranty?: { months: number; text?: string } | null;
images?: { id?: Uuid; fileId?: Uuid; sortOrder: number; alt?: string; variantId?: Uuid | null }[]; // full ordered gallery: keep by id, add by fileId; omitted ids are removed
defaultVariantId?: Uuid;
version: number;
}{
"images": [
{ "id": "0192b210-6a7b-7c8d-9e0f-1a2b3c4d5e6f", "sortOrder": 2 },
{ "fileId": "0193020a-1b2c-7d3e-8f4a-5b6c7d8e9f01", "sortOrder": 1, "alt": "Câble HOCO X14 enroulé" }
],
"version": 5
}Response 200 OK — the updated AdminProduct (version 6).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | More than 12 images, duplicate sortOrder, image variantId of another product |
| 403 | PERMISSION_DENIED | Caller lacks catalog.update |
| 404 | NOT_FOUND | Unknown product, brand, category or image |
| 409 | DUPLICATE_VALUE · VERSION_CONFLICT | Reference taken · stale version |
| 409 | RESOURCE_IN_USE | reference change after a variant was ordered |
| 422 | FILE_NOT_READY · FILE_PURPOSE_MISMATCH | Image not uploaded or not PRODUCT_IMAGE |
| 422 | PRODUCT_INCOMPLETE | The change would leave an ACTIVE product without image (unpublish first) |
Rules & side effects
- Images are public CDN files; the first by
sortOrderis the main image. Removed images are kept 30 days for orders' snapshots, then purged. - A category change re-checks attribute values: values of attributes that do not apply to the new category are kept but flagged in
warnings. - Audit
UPDATEwith the changed fields. Reseller catalog caches are invalidated (ETag change).
Used by BO-28
POST /admin/products/{productId}/status
admin.products.setStatus · Permission catalog.update (catalog.administer to restore an archived product) · Apps BO · Release V1 · Requirements CAT-04
Purpose. Publishes, hides, shows again, archives or restores a product.
Request
interface SetProductStatusBody { to: "ACTIVE" | "HIDDEN" | "ARCHIVED"; reason?: string; version: number }{ "to": "ACTIVE", "version": 1 }Response 200 OK — the AdminProduct with its new status (ACTIVE, or OUT_OF_STOCK when every variant has 0 available).
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks catalog.update (or catalog.administer for ARCHIVED → HIDDEN) |
| 404 | NOT_FOUND | Unknown product |
| 409 | PRODUCT_TRANSITION_NOT_ALLOWED | Move not in Lifecycles › Product — e.g. to: OUT_OF_STOCK (system only), ARCHIVED → ACTIVE |
| 409 | VERSION_CONFLICT | Stale version |
| 422 | PRODUCT_INCOMPLETE | Publishing without the minimum data of cahier §15.4: brand, reference, name, category, description, ≥ 1 image, VAT rate, ≥ 1 active variant with SKU and the three prices, values for every requiredToPublish attribute — details: { missing: [] } |
Rules & side effects
DRAFT/HIDDEN→ACTIVEsetspublishedAton first publication (drives « Nouveautés »);ACTIVE/OUT_OF_STOCK→HIDDEN; any →ARCHIVED;ARCHIVED→HIDDEN(restore).OUT_OF_STOCK↔ACTIVEis automatic from stock.- Hidden or archived products disappear from catalog, search, favorites and promotions at the next read; cart lines become blocking issues
PRODUCT_UNAVAILABLE. - Audit
STATUS_CHANGEwith the reason. - Notifications: none by default (a « Nouveauté » push is a promotion or announcement decision).
PUT /admin/products/{productId}/visibility
admin.products.visibility · Permission catalog.update · Apps BO · Release V1 · Requirements CAT-05
Purpose. Sets who may see and order a product: everyone, only some segments or resellers, or everyone except them (cahier §4.3 « visibilité par revendeur ou segment »).
Request
interface ProductVisibilityBody {
mode: "EVERYONE" | "INCLUDE_ONLY" | "EXCLUDE";
segmentIds: Uuid[]; // ≤ 50
resellerIds: Uuid[]; // ≤ 500
version: number;
}{ "mode": "INCLUDE_ONLY", "segmentIds": ["01929a10-1b2c-7d3e-8f4a-5b6c7d8e9f01"], "resellerIds": [], "version": 6 }Response 200 OK — { "productId": "…", "mode": "INCLUDE_ONLY", "segments": [{ "id": "01929a10-1b2c-7d3e-8f4a-5b6c7d8e9f01", "name": "Grands comptes Ouest" }], "resellers": [], "audienceCount": 64, "version": 7 }
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | INCLUDE_ONLY/EXCLUDE with empty lists, EVERYONE with non-empty lists |
| 403 | PERMISSION_DENIED | Caller lacks catalog.update |
| 404 | NOT_FOUND | Unknown product, segment or reseller |
| 409 | VERSION_CONFLICT | Stale version |
Rules & side effects
- Stored as
ProductVisibilityRulerows; applied by every reseller-facing read (catalog, search, facets, product page, cart, checkout) and by assisted orders. Guests see onlyEVERYONEproducts. - Audit
UPDATEwith the before/after audience.
Used by BO-28
Variants and prices
POST /admin/products/{productId}/variants
admin.variants.crud (create) · Permission catalog.update (+ pricing.update for price fields) · Apps BO · Release V1 · Requirements CAT-03 · PRC-01
Purpose. Adds a variant (colour, length, power…) to a product, with its SKU, barcode, attribute values, three prices and quantity limits.
Request
interface CreateVariantBody {
sku: string; // 3–60, uppercase, digits, "-"
barcode?: string;
name: string;
attributes: Record<string, string | number | boolean | string[]>;
priceSuperGros: Money; priceDemiGros: Money; priceDetail: Money; // HT
vatRate?: BasisPoints;
recommendedPrice?: Money; // TTC
purchasePrice?: Money; // HT
minQty?: number; // default 1
qtyStep?: number; // default 1
maxQtyPerOrder?: number;
weightGrams?: number;
status?: "ACTIVE" | "HIDDEN"; // default ACTIVE
alertThreshold?: number; // initial stock alert threshold (default stock.lowThresholdDefault)
priceChangeReason?: string;
}{
"sku": "HC-X14-CC-2M-BK", "barcode": "6931474761241", "name": "Noir · 2 m",
"attributes": { "connector": ["USB_C"], "cable_length_m": 2, "power_w": 60, "color": "BLACK" },
"priceSuperGros": 38000, "priceDemiGros": 41000, "priceDetail": 45000, "vatRate": 1900,
"recommendedPrice": 65000, "purchasePrice": 25000, "minQty": 5, "qtyStep": 5
}Response 201 Created — the AdminVariant (pricesTtc: { superGros: 45220, demiGros: 48790, detail: 53550 }, stock zero, availability OUT_OF_STOCK, version 1).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Bad SKU, attribute not defined for the category or of the wrong type, maxQtyPerOrder < minQty |
| 403 | PERMISSION_DENIED | Caller lacks catalog.update, or pricing.update for the price fields |
| 404 | NOT_FOUND | Unknown product |
| 409 | DUPLICATE_VALUE | SKU or barcode already used; another variant of the product has exactly the same attribute values |
| 422 | PRICE_ORDER_INVALID | priceSuperGros ≤ priceDemiGros ≤ priceDetail not respected |
Rules & side effects
StockLevelrows are created at 0 in every active warehouse. A product inACTIVEstaysACTIVE(the new variant is simply out of stock).- Audit
CREATE;PriceChangerows for the initial prices (source: MANUAL).
Used by BO-28
PATCH /admin/products/{productId}/variants/{variantId}
admin.variants.crud (update) · Permission catalog.update (+ pricing.update for price fields) · Apps BO · Release V1 · Requirements CAT-03 · PRC-01
Purpose. Edits a variant — including its prices, whose every change is historised.
Request
interface UpdateVariantBody extends Partial<Omit<CreateVariantBody, "alertThreshold">> {
status?: "ACTIVE" | "HIDDEN" | "ARCHIVED";
isDefault?: boolean;
priceChangeReason?: string; // stored on the PriceChange rows (recommended for price changes)
version: number;
}{ "priceDemiGros": 36000, "priceDetail": 40000, "priceChangeReason": "Hausse fournisseur de 5 % au 1er novembre.", "version": 3 }Response 200 OK — the updated AdminVariant (pricesTtc.demiGros 42 840, pricesTtc.detail 47 600, version 4).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | As for creation |
| 403 | PERMISSION_DENIED | Missing catalog.update or pricing.update — details.fields lists the refused price fields |
| 404 | NOT_FOUND | Unknown product or variant |
| 409 | DUPLICATE_VALUE | SKU or barcode already used |
| 409 | RESOURCE_IN_USE | SKU change on an ordered variant |
| 409 | VARIANT_LAST_ACTIVE | Hiding or archiving the last active variant of an ACTIVE product (hide the product instead) |
| 409 | VERSION_CONFLICT | Stale version |
| 422 | PRICE_ORDER_INVALID | Price order not respected |
Rules & side effects
- New prices apply from the next pricing: carts are re-priced at their next read (issue
PRICE_CHANGED), submitted orders keep their snapshot; an order waiting for confirmation shows the difference in its price control (admin.orders.get). - One
PriceChangerow per changed price field (PRICE_SUPER_GROS,PRICE_DEMI_GROS,PRICE_DETAIL,RECOMMENDED_PRICE,PURCHASE_PRICE,VAT_RATE) with before/after, actor and reason; auditUPDATE. - Notifications: none (price changes reach resellers through the catalog).
DELETE /admin/products/{productId}/variants/{variantId}
admin.variants.crud (delete) · Permission catalog.update · Apps BO · Release V1 · Requirements CAT-03
Purpose. Deletes a variant that was never ordered or moved in stock; otherwise archives it.
Response 200 OK — { "id": "0192b210-8c9d-7e0f-9a1b-2c3d4e5f6a7b", "result": "ARCHIVED", "references": [{ "entityType": "OrderItem", "count": 12 }] } (result: DELETED or ARCHIVED)
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks catalog.update |
| 404 | NOT_FOUND | Unknown product or variant |
| 409 | VARIANT_LAST_ACTIVE | Last active variant of an ACTIVE product |
| 409 | RESOURCE_IN_USE | Physical stock is not 0 in some warehouse (adjust or transfer first) |
Rules & side effects
- Audit
DELETEorSTATUS_CHANGE. Price history is kept for archived variants. - An archived variant answers
status: ARCHIVED(SRV-42 — stored apart fromHIDDEN); both are inactive and not orderable, and archived variants leave the price grid (admin.variants.prices). APATCHwithstatus: ACTIVEorHIDDENrestores it.
Used by BO-28
PUT /admin/variants/{variantId}/quantity-tiers
admin.variants.tiers · Permission pricing.update · Apps BO · Release Removed from V1 · Requirements PRC-02
Dropped from V1 (decision of 17 Oct 2026)
404 NOT_FOUND); endpoints.json still lists it and should be updated. Volume incentives in V1 are promotions with a minimum quantity (admin.promotions.crud).Purpose. Reserved: per-variant quantity tiers, should HOCO ask for them again.
Request — reserved contract: { "tiers": [{ "minQty": 50, "discountBps": 300 }], "version": 4 }.
Response — not served in V1.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Always in V1 (route not mounted) |
Rules & side effects
- None in V1.
QuantityTieris not part of the V1 data model.
Used by — (no V1 screen)
GET /admin/variants/{variantId}/price-history
admin.variants.priceHistory · Permission pricing.view · Apps BO · Release V1 · Requirements PRC-01 · AUD-02
Purpose. The history of every price of a variant (cahier §4.3, §8 « historique des modifications »).
Request
| Query | Type | Values / default | Notes |
|---|---|---|---|
field | string[] | PRICE_SUPER_GROS | PRICE_DEMI_GROS | PRICE_DETAIL | RECOMMENDED_PRICE | PURCHASE_PRICE | VAT_RATE | PriceChangeField, repeatable |
from / to | string (date) | ||
page / pageSize | number | 1 / 25 (max 200) | Sorted by changedAt desc |
Response 200 OK
{
"items": [
{ "id": "01930212-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "field": "PRICE_DETAIL", "before": 39000, "after": 40000, "changedAt": "2026-11-01T07:30:00Z", "changedBy": { "id": "01929e55-2c3d-7e4f-9a1b-2c3d4e5f6a7b", "displayName": "Nadia Belkacem" }, "reason": "Hausse fournisseur de 5 % au 1er novembre.", "source": "MANUAL" },
{ "id": "01930212-2b3c-7d4e-9f5a-6b7c8d9e0f1a", "field": "PRICE_DEMI_GROS", "before": 35000, "after": 36000, "changedAt": "2026-11-01T07:30:00Z", "changedBy": { "id": "01929e55-2c3d-7e4f-9a1b-2c3d4e5f6a7b", "displayName": "Nadia Belkacem" }, "reason": "Hausse fournisseur de 5 % au 1er novembre.", "source": "MANUAL" }
],
"total": 8,
"page": 1,
"pageSize": 25
}Field notes. source: MANUAL, IMPORT (product import, with the import id in importId), SYSTEM (VAT migration). Amounts are HT except RECOMMENDED_PRICE (TTC); VAT_RATE values are basis points.
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks pricing.view |
| 404 | NOT_FOUND | Unknown variant |
Rules & side effects
- Read-only;
PriceChangerows are append-only.
GET /admin/variants/prices
admin.variants.prices · Permission catalog.view (costs: pricing.view + finance.view) · Apps BO · Release V1 · Requirements PRC-02 · PRC-01 · Added in contract 1.1.0 (SRV-41)
Purpose. The price grid of BO-31: one row per variant with its three customer-type prices, its VAT, recommended price and — for callers who may see costs — its purchase price and margins, plus the latest price change.
Request
| Query | Type | Values / default | Notes |
|---|---|---|---|
page / pageSize | number | 1 / 25 (max 200) | |
q | string | ≥ 2 characters | Product name or reference, variant SKU, barcode (exact) |
brandId / categoryId | Uuid[] | repeatable | The category includes its sub-categories |
status | ProductStatus[] | repeatable; default every status but ARCHIVED | Product status |
variantStatus | string[] | ACTIVE | HIDDEN | |
missing | string | price | purchasePrice | A selling price at 0 / no purchase price |
marginBelow | number | basis points | Détail margin below the value — 403 without pricing.view + finance.view |
changedSince | string (date) | A PriceChange on or after the day (Africa/Algiers) | |
sort | string | productName (default) | sku | priceSuperGros | priceDemiGros | priceDetail | lastChangedAt | |
order | string | asc (default) | desc |
Response 200 OK
{
"items": [
{
"variantId": "0192e0b1-6a7b-7c8d-9e0f-1a2b3c4d5e6f", "productId": "0192e0b1-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
"productName": "Chargeur rapide 20 W USB-C", "productReference": "ACF-A21", "productStatus": "ACTIVE",
"variantName": "Noir", "sku": "ACF-A21-BK", "status": "ACTIVE",
"brand": { "id": "0192d001-…", "name": "ACEFAST" }, "category": { "id": "0192d101-…", "name": "Chargeurs secteur" },
"priceSuperGros": 38000, "priceDemiGros": 42000, "priceDetail": 48000, "vatRate": 1900, "recommendedPrice": 69000,
"purchasePrice": 30000, "marginsBps": { "superGros": 2105, "demiGros": 2857, "detail": 3750 },
"lastChange": { "changedAt": "2026-11-01T07:30:00Z", "changedBy": { "id": "01929e55-…", "displayName": "Nadia Belkacem" } },
"updatedAt": "2026-11-01T07:30:00Z", "version": 1793518200000
}
],
"total": 412, "page": 1, "pageSize": 25
}Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Unknown sort or filter value |
| 403 | PERMISSION_DENIED | Caller lacks catalog.view; marginBelow without pricing.view + finance.view |
Used by BO-31
PUT /admin/variants/prices
admin.variants.bulkPrices · Permission catalog.update + pricing.update (purchase price: also finance.view) · Apps BO · Release V1 · Requirements PRC-02 · PRC-01 · AUD-02 · Added in contract 1.1.0 (SRV-41)
Purpose. Saves a whole price grid in one call: the changed prices of up to 500 variants, each with its version, under one reason.
Request
{
"reason": "Hausse fournisseur de 5 % au 1er novembre.",
"items": [
{ "variantId": "0192e0b1-6a7b-7c8d-9e0f-1a2b3c4d5e6f", "version": 1793518200000, "priceDemiGros": 44100, "priceDetail": 50400 },
{ "variantId": "0192e0b1-7b8c-7d9e-8f0a-1b2c3d4e5f60", "version": 1793518200000, "priceSuperGros": 39900 }
]
}Each item carries only the prices that change: priceSuperGros, priceDemiGros, priceDetail, recommendedPrice (TTC), purchasePrice. The VAT rate is edited on the variant.
Response 200 OK
{
"updated": 1,
"priceChanges": 2,
"items": [ { "variantId": "0192e0b1-6a7b-7c8d-9e0f-1a2b3c4d5e6f", "priceDemiGros": 44100, "priceDetail": 50400, "…": "a VariantPriceRow, re-read" } ],
"failed": [ { "variantId": "0192e0b1-7b8c-7d9e-8f0a-1b2c3d4e5f60", "sku": "ACF-A21-WH", "code": "PRICE_ORDER_INVALID", "message": "Prices must be ordered super-gros ≤ demi-gros ≤ détail" } ]
}Errors (the whole call)
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | No item, more than 500, a variant twice, no price in any item, reason shorter than 3 characters |
| 403 | PERMISSION_DENIED | Caller lacks catalog.update or pricing.update (finance.view for purchasePrice) — details: { required, fields } |
Per-variant failures (in failed, the other variants are saved): NOT_FOUND (unknown variant), VERSION_CONFLICT (stale version), PRICE_ORDER_INVALID (super-gros ≤ demi-gros ≤ détail broken after the change), VALIDATION_FAILED (a selling price at 0).
Rules & side effects
- Each variant is saved in its own transaction (optimistic lock on
version): onePriceChangerow per changed price with thereasonand the caller — the same rows asadmin.variants.crud— the product version bumped, and an auditUPDATEonProductVariant. - A drop of a customer-type price of
smart.priceDropMinPercentor more emitsprice.dropped(SN-21), as for a single edit. - Prices apply to the next carts; placed orders keep their snapshot.
Used by BO-31
Attributes
Attribute definitions make the advanced reseller filters possible (power, connector, cable length, colour, capacity, Bluetooth version…, CAT-12, CAT-13). A definition is linked to categories; it applies to their sub-categories; each variant stores its values (VariantAttributeValue). Filters and counts are served to the apps by catalog.facets.
GET /admin/attribute-definitions
admin.attributes.crud (list) · Permission catalog.view · Apps BO · Release V1 · Requirements CAT-13
Purpose. Lists attribute definitions, optionally those applying to one category.
Request
| Query | Type | Values / default | Notes |
|---|---|---|---|
categoryId | Uuid | Definitions applying to it (own and inherited) | |
status | string | ACTIVE (default) | ARCHIVED | |
filterable | boolean | ||
q | string | min 2 chars | Key or French label |
sort / order / page / pageSize | sortOrder (default), key · asc · 1 · 50 (max 200) |
Response 200 OK
{
"items": [
{
"id": "0192b100-1a2b-7c3d-9e4f-5a6b7c8d9e01", "key": "power_w", "label": { "fr": "Puissance", "ar": "القدرة", "en": "Power" },
"type": "NUMBER", "unit": "W", "decimals": 0, "multiValue": false,
"categoryIds": ["01928b01-0a1b-7c2d-8e3f-4a5b6c7d8e9f", "01928b02-2b3c-7d4e-9f5a-6b7c8d9e0f12"],
"filterable": true, "filterWidget": "RANGE", "requiredToPublish": true, "sortOrder": 1, "status": "ACTIVE",
"variantCount": 311, "createdAt": "2026-10-23T08:00:00Z", "updatedAt": "2026-10-23T08:00:00Z", "version": 1
},
{
"id": "0192b100-3c4d-7e5f-9a6b-7c8d9e0f1a2b", "key": "connector", "label": { "fr": "Connecteur", "en": "Connector" },
"type": "ENUM", "multiValue": true,
"values": [
{ "code": "USB_C", "label": { "fr": "USB-C" }, "sortOrder": 1, "active": true },
{ "code": "LIGHTNING", "label": { "fr": "Lightning" }, "sortOrder": 2, "active": true },
{ "code": "MICRO_USB", "label": { "fr": "Micro-USB" }, "sortOrder": 3, "active": true },
{ "code": "USB_A", "label": { "fr": "USB-A" }, "sortOrder": 4, "active": true }
],
"categoryIds": ["01928b01-1a2b-7c3d-8e4f-5a6b7c8d9e01"],
"filterable": true, "filterWidget": "CHECKBOX", "requiredToPublish": true, "sortOrder": 2, "status": "ACTIVE",
"variantCount": 402, "createdAt": "2026-10-23T08:05:00Z", "updatedAt": "2026-10-24T10:12:00Z", "version": 2
}
],
"total": 9,
"page": 1,
"pageSize": 50
}Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks catalog.view |
| 404 | NOT_FOUND | Unknown categoryId |
Rules & side effects
- Read-only.
Used by BO-71 · BO-28 (variant attribute form)
GET /admin/attribute-definitions/{attributeId}
admin.attributes.crud (get) · Permission catalog.view · Apps BO · Release V1 · Requirements CAT-13
Purpose. One definition, with the distribution of its values (for clean-up before editing allowed values).
Response 200 OK — an AttributeDefinition plus valueCounts, e.g. { "USB_C": 288, "LIGHTNING": 97, "MICRO_USB": 41, "USB_A": 112 }.
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks catalog.view |
| 404 | NOT_FOUND | Unknown definition |
Rules & side effects
- Read-only.
Used by BO-71
POST /admin/attribute-definitions
admin.attributes.crud (create) · Permission catalog.create · Apps BO · Release V1 · Requirements CAT-13
Purpose. Defines a filterable attribute for one or more categories.
Request
interface CreateAttributeBody {
key: string; // ^[a-z][a-z0-9_]{1,39}$, unique
label: { fr: string; ar?: string; en?: string };
type: "NUMBER" | "ENUM" | "BOOLEAN" | "TEXT";
unit?: string; decimals?: number;
values?: { code: string; label: { fr: string; ar?: string; en?: string }; sortOrder?: number }[]; // ENUM: 1–100 values, codes UPPER_SNAKE_CASE
multiValue?: boolean; // ENUM only
categoryIds: Uuid[]; // ≥ 1
filterable?: boolean; // default true (TEXT: always false)
filterWidget?: "CHECKBOX" | "RANGE" | "TOGGLE" | "NONE";
requiredToPublish?: boolean; // default false
minValue?: number; maxValue?: number; // NUMBER only, min ≤ max (contract 1.1.0)
showOnProductSheet?: boolean; // default true (contract 1.1.0)
sortOrder?: number;
}{
"key": "capacity_mah", "label": { "fr": "Capacité", "en": "Capacity" }, "type": "NUMBER", "unit": "mAh", "decimals": 0,
"minValue": 1000, "maxValue": 50000, "showOnProductSheet": true,
"categoryIds": ["01928b01-3c4d-7e5f-9a6b-7c8d9e0f1a2b"], "filterable": true, "filterWidget": "RANGE", "requiredToPublish": true
}The update body takes the same fields; minValue / maxValue accept null to clear a bound. A variant value outside the range is refused when a variant is created or edited and in a product import (400 VALIDATION_FAILED, field code OUT_OF_RANGE on attributes.<key>). showOnProductSheet: false keeps the attribute in the filters and the variant choice; the reseller product detail flags it with onProductSheet: false so the sheet leaves it out of the specifications.
Response 201 Created — the AttributeDefinition (variantCount 0, version 1).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Bad key, values on a non-ENUM type, widget incompatible with the type (RANGE on ENUM, TOGGLE on NUMBER), TEXT marked filterable, a range on a non-NUMBER type or minValue > maxValue |
| 403 | PERMISSION_DENIED | Caller lacks catalog.create |
| 404 | NOT_FOUND | Unknown category |
| 409 | DUPLICATE_VALUE | Key already used; duplicate ENUM code |
Rules & side effects
requiredToPublishapplies to products published from now on; already active products lacking the value are listed byadmin.products.list?incomplete=truebut stay published.- Audit
CREATEonAttributeDefinition. Facet caches of the categories are invalidated.
Used by BO-71
PATCH /admin/attribute-definitions/{attributeId}
admin.attributes.crud (update) · Permission catalog.update · Apps BO · Release V1 · Requirements CAT-13
Purpose. Edits labels, allowed values, categories, filter settings, or archives a definition.
Request
interface UpdateAttributeBody {
label?: { fr: string; ar?: string; en?: string };
unit?: string | null; decimals?: number;
values?: { code: string; label: { fr: string; ar?: string; en?: string }; sortOrder?: number; active?: boolean }[]; // full list; codes cannot be removed while used — deactivate them
categoryIds?: Uuid[];
filterable?: boolean; filterWidget?: "CHECKBOX" | "RANGE" | "TOGGLE" | "NONE";
requiredToPublish?: boolean; sortOrder?: number;
status?: "ACTIVE" | "ARCHIVED";
version: number;
}{ "values": [
{ "code": "USB_C", "label": { "fr": "USB-C" }, "sortOrder": 1 },
{ "code": "LIGHTNING", "label": { "fr": "Lightning" }, "sortOrder": 2 },
{ "code": "MICRO_USB", "label": { "fr": "Micro-USB" }, "sortOrder": 3 },
{ "code": "USB_A", "label": { "fr": "USB-A" }, "sortOrder": 4 },
{ "code": "MAGSAFE", "label": { "fr": "MagSafe" }, "sortOrder": 5 }
], "version": 2 }Response 200 OK — the updated AttributeDefinition.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | As for creation; key and type are immutable |
| 403 | PERMISSION_DENIED | Caller lacks catalog.update |
| 404 | NOT_FOUND | Unknown definition or category |
| 409 | RESOURCE_IN_USE | Removing an ENUM code that variants use — details: { code, variantCount } |
| 409 | VERSION_CONFLICT | Stale version |
Rules & side effects
- Archiving hides the filter from the apps and keeps stored values. Audit
UPDATE; facet caches invalidated.
Used by BO-71
DELETE /admin/attribute-definitions/{attributeId}
admin.attributes.crud (delete) · Permission catalog.delete · Apps BO · Release V1 · Requirements CAT-13
Purpose. Deletes an unused definition (otherwise archive it).
Response 204 No Content
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks catalog.delete |
| 404 | NOT_FOUND | Unknown definition |
| 409 | RESOURCE_IN_USE | At least one variant carries a value — archive instead |
Rules & side effects
- Audit
DELETE.
Used by BO-71
Price lists and agreements (V2)
Not in V1 (decision of 17 Oct 2026)
404 NOT_FOUND). endpoints.json still tags them V1 and describes them as segment price lists; it should be updated. BO-31 becomes, in V1, the three-price grid built on admin.products.list and admin.variants.crud.V2 baseline: an agreement (PriceList, type: AGREEMENT) binds one or more resellers to specific unit prices (PriceListItem: variant + unit price HT, or a discount in basis points on the customer-type price), with validFrom/validTo and a priority. Resolution in V2: active agreement of the reseller → customer-type price → promotions on top.
interface PriceList { // V2
id: Uuid;
name: string;
type: "AGREEMENT" | "LEVEL"; // V2: per-reseller agreement, or a loyalty-level list
resellers: ResellerSummary[]; // AGREEMENT
loyaltyLevels?: LoyaltyLevel[]; // LEVEL
validFrom?: string; validTo?: string; // YYYY-MM-DD, inclusive
priority: number; // higher wins when several apply
status: "ACTIVE" | "INACTIVE";
itemCount: number;
createdAt: Iso; updatedAt: Iso; version: number;
}GET /admin/price-lists
admin.priceLists.crud (list) · Permission pricing.view · Apps BO · Release V2 · Requirements PRC-03
V2
Purpose. Lists agreements and level price lists.
Request — filters type, status, resellerId, validAt (date), q; sort name | validFrom | priority; page / pageSize.
Response 200 OK — Page<PriceList>, e.g. { "items": [{ "id": "0194a001-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "name": "Accord Phone Store El Bahia 2027", "type": "AGREEMENT", "resellers": [{ "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" }], "validFrom": "2027-01-01", "validTo": "2027-12-31", "priority": 10, "status": "ACTIVE", "itemCount": 42, "createdAt": "2026-12-15T09:00:00Z", "updatedAt": "2026-12-15T09:00:00Z", "version": 1 }], "total": 1, "page": 1, "pageSize": 25 }.
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks pricing.view |
| 404 | NOT_FOUND | V1 (route not mounted) |
Rules & side effects
- Read-only.
Used by BO-31 (V2)
GET /admin/price-lists/{priceListId}
admin.priceLists.crud (get) · Permission pricing.view · Apps BO · Release V2 · Requirements PRC-03
V2
Purpose. One price list with a page of its items (?page, ?pageSize, ?q on SKU or name).
Response 200 OK — the PriceList plus items: Page<{ variantId, sku, productName, customerTypePrice: Money, unitPrice?: Money, discountBps?: number }>.
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks pricing.view |
| 404 | NOT_FOUND | Unknown price list (always in V1) |
Rules & side effects
- Read-only.
Used by BO-31 (V2)
POST /admin/price-lists
admin.priceLists.crud (create) · Permission pricing.create · Apps BO · Release V2 · Requirements PRC-03
V2
Purpose. Creates an agreement or a level price list.
Request — { name, type, resellerIds?, loyaltyLevels?, validFrom?, validTo?, priority }, e.g. { "name": "Accord Phone Store El Bahia 2027", "type": "AGREEMENT", "resellerIds": ["0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a"], "validFrom": "2027-01-01", "validTo": "2027-12-31", "priority": 10 }.
Response 201 Created — the PriceList.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | validTo before validFrom, no reseller on an agreement |
| 403 | PERMISSION_DENIED | Caller lacks pricing.create |
| 409 | DUPLICATE_VALUE | Name taken |
Rules & side effects
- Audit
CREATE;PriceChangerows (PRICE_LIST_ITEM) for each item later set.
Used by BO-31 (V2)
PATCH /admin/price-lists/{priceListId}
admin.priceLists.crud (update) · Permission pricing.update · Apps BO · Release V2 · Requirements PRC-03
V2
Purpose. Edits validity, priority, assigned resellers or status of a price list.
Request — partial of the creation body plus status and version.
Response 200 OK — the updated PriceList.
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks pricing.update |
| 404 | NOT_FOUND | Unknown price list |
| 409 | VERSION_CONFLICT | Stale version |
Rules & side effects
- Audit
UPDATE; carts of the affected resellers re-priced at their next read.
Used by BO-31 (V2)
DELETE /admin/price-lists/{priceListId}
admin.priceLists.crud (delete) · Permission pricing.delete · Apps BO · Release V2 · Requirements PRC-03
V2
Purpose. Deletes a price list never used by an order; otherwise deactivates it.
Response 200 OK — { "id": "…", "result": "DELETED" } or "INACTIVE".
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks pricing.delete |
| 404 | NOT_FOUND | Unknown price list |
Rules & side effects
- Audit
DELETEorSTATUS_CHANGE.
Used by BO-31 (V2)
PUT /admin/price-lists/{priceListId}/items
admin.priceLists.items · Permission pricing.update · Apps BO · Release V2 · Requirements PRC-03
V2
Purpose. Bulk upsert of a price list's items.
Request — { "mode": "MERGE" | "REPLACE", "items": [{ "sku": "AF-A29-50W-WH", "unitPrice": 200000 }, { "sku": "HC-X14-CC-1M-BK", "discountBps": 500 }], "removeSkus": [], "version": 1 } — at most 2 000 items; each item has exactly one of unitPrice (HT) or discountBps.
Response 200 OK — { "upserted": 2, "removed": 0, "itemCount": 44, "version": 2 }
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Unknown SKU, both or neither of unitPrice / discountBps — all-or-nothing, details.fields per index |
| 403 | PERMISSION_DENIED | Caller lacks pricing.update |
| 404 | NOT_FOUND | Unknown price list |
| 409 | VERSION_CONFLICT | Stale version |
Rules & side effects
- One
PriceChangerow (PRICE_LIST_ITEM) per changed item; auditUPDATE.
Used by BO-31 (V2)
Import and export
POST /admin/imports/products
admin.products.import · Permission catalog.administer (+ pricing.update to import price columns) · Apps BO · Release V1 · Requirements CAT-06
Purpose. Imports or updates products and variants from an uploaded Excel/CSV file: a dry run first produces a row-by-row report, then an apply run writes the valid rows (cahier §4.3 « Import et export des produits via Excel/CSV », §13.2 initial import). The job is an ImportJob of type PRODUCTS, polled with admin.imports.get.
Request
interface ProductImportBody {
mode: "DRY_RUN" | "APPLY";
fileId?: Uuid; // DRY_RUN: completed upload, purpose IMPORT_FILE
dryRunId?: Uuid; // APPLY: the successful dry run to apply
options?: {
updateExisting?: boolean; // default true — rows whose SKU exists update the variant (and its product)
createMissingBrands?: boolean; // default false
createMissingCategories?: boolean; // default false
targetStatus?: "DRAFT" | "ACTIVE"; // default DRAFT for new products; ACTIVE only when complete
images?: "URLS" | "NONE"; // default URLS: download image_urls (https, ≤ 5 MB each)
};
}{ "mode": "DRY_RUN", "fileId": "01930300-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "options": { "updateExisting": true, "targetStatus": "DRAFT" } }Template columns (first sheet of an XLSX, or CSV UTF-8 with ; or ,; header row required; * = required; amounts in DA with , or . decimals, converted to centimes):
| Column | Example | Notes |
|---|---|---|
brand* | HOCO | Existing brand name (or created with createMissingBrands) |
category* | Câbles > Câbles USB-C | Path with > |
reference* | HC-X14 | Groups rows of the same product |
name* | Câble USB-C vers USB-C 60 W 1 m | |
description | ||
specs | Gaine=Nylon tressé; Débit=480 Mb/s | label=value pairs separated by ; |
warranty_months | 6 | |
sku* | HC-X14-CC-1M-BK | Natural key of the row |
barcode | 6931474761234 | |
variant_name | Noir · 1 m | |
attr:<key> | attr:power_w = 60, attr:connector = USB_C,LIGHTNING | One column per attribute key of the category |
price_super_gros_ht* · price_demi_gros_ht* · price_detail_ht* | 320,00 · 350,00 · 390,00 | Need pricing.update |
recommended_price_ttc | 550,00 | |
purchase_price_ht | 210,00 | Needs pricing.update |
vat_rate | 19 | Percent; default pricing.defaultVatRate |
min_qty · qty_step · max_qty_per_order · weight_g | 5 · 5 · 500 · 38 | |
status | DRAFT | DRAFT, ACTIVE or HIDDEN |
image_urls | https://…/1.jpg, https://…/2.jpg | Comma-separated |
Response 202 Accepted — the ImportJob
{
"id": "01930300-2b3c-7d4e-9f5a-6b7c8d9e0f1a",
"type": "PRODUCTS",
"mode": "DRY_RUN",
"status": "QUEUED",
"file": { "id": "01930300-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "url": "https://files.hoco.dz/s/catalogue-hoco-nov.xlsx?sig=…", "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "size": 812400, "purpose": "IMPORT_FILE" },
"options": { "updateExisting": true, "createMissingBrands": false, "createMissingCategories": false, "targetStatus": "DRAFT", "images": "URLS" },
"createdBy": { "id": "01929e66-3d4e-7f5a-8b6c-7d8e9f0a1b2c", "displayName": "Sofiane Hamdi" },
"createdAt": "2026-11-04T07:30:00Z"
}When it finishes, admin.imports.get returns e.g. summary: { "totalRows": 1240, "validRows": 1198, "warningRows": 30, "errorRows": 12, "toCreate": 1105, "toUpdate": 123, "unchanged": 0 } and rows such as { "row": 57, "status": "ERROR", "action": "CREATE", "key": "BF-BH20-WH", "messages": [{ "column": "price_detail_ht", "code": "PRICE_ORDER_INVALID", "message": "Le prix détail doit être ≥ au prix demi-gros." }] }.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | fileId with APPLY, dryRunId with DRY_RUN |
| 403 | PERMISSION_DENIED | Caller lacks catalog.administer; price columns present without pricing.update |
| 404 | NOT_FOUND | Unknown file or dry run |
| 409 | IMPORT_DRY_RUN_EXPIRED | The dry run is older than 24 h |
| 409 | IMPORT_ALREADY_APPLIED | The dry run was already applied |
| 422 | IMPORT_FILE_INVALID | Unreadable file, missing required columns, more than 5 000 rows — details: { missingColumns, maxRows } |
| 422 | IMPORT_DRY_RUN_REQUIRED | APPLY on a dry run that did not succeed |
| 422 | FILE_NOT_READY · FILE_PURPOSE_MISMATCH | Upload not completed or not IMPORT_FILE |
Rules & side effects
- The dry run validates every row exactly like the single operations (references, SKUs, barcodes, attribute values, price order, quantity rules) and writes nothing.
APPLYre-validates and writesVALIDandWARNINGrows only, in batches of 200; rows that became invalid areSKIPPED. - Prices imported write
PriceChangerows withsource: IMPORT; stock is not imported here (stock import). - Audit
IMPORTon apply (counts insummary). Notifications:import.finished→ requester (in-app + email); SSEimport.finished.
Used by BO-32
POST /admin/products/export
admin.products.export · Permission catalog.export · Apps BO · Release V1 · Requirements CAT-06 · ANA-05
Purpose. Exports the filtered catalog — one row per variant, in the import template's columns so the file can be edited and re-imported.
Request
interface ExportProductsBody {
format: "XLSX" | "CSV" | "PDF"; // PDF = printable price catalogue (no purchase price)
filters: Record<string, unknown>; // GET /admin/products query
columns?: string[]; // default: the import template columns the caller may see
includeStock?: boolean; // adds physical / reserved / available per warehouse
}{ "format": "XLSX", "filters": { "brandId": ["01928a01-2b3c-7d4e-9f5a-6b7c8d9e0f12"], "status": ["ACTIVE", "OUT_OF_STOCK"] }, "includeStock": true }Response 202 Accepted — an ExportJob with "source": { "kind": "LIST", "key": "products" }, "status": "QUEUED".
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Unknown column or filter |
| 403 | PERMISSION_DENIED | Caller lacks catalog.export; purchase_price_ht requested without pricing.view |
| 422 | EXPORT_TOO_LARGE | Above 100 000 rows (2 000 for PDF) |
| 429 | RATE_LIMITED | Already 3 running exports |
Rules & side effects
purchase_price_htis included only for holders ofpricing.view(omitted from the default columns otherwise). AuditEXPORT. Notification and SSEexport.readyto the requester.
CRM
Back-office operations on resellers and prospects — list and map, 360° record, creation with duplicate checks, customer type, lifecycle, assignment, validation queue, timeline, documents, tasks, segments and export.
Stock
Back-office stock operations — levels per variant and warehouse, the movement journal, receipts, adjustments, alert thresholds, inventory sessions and warehouses.