Products Service
Overview
Products represent sellable items in the Catalog module. They aggregate core product data together with pricing, stock and dynamic properties.
The Products service is a central integration point between:
- Catalog
- Inventory
- Pricing
- Sales
All product operations are tenant-aware and scoped to the current tenant.
Endpoints
GET
/api/v1/Products
Returns a paginated, filterable list of products for the current tenant.
Query parameters:
page- page number (1-based, default: 1)pageSize- items per page (default: 20, max: 100)sortBy- field to sort by:sku,title,createdAt,brand,pricesortDesc- sort descending (default: false)search- search across SKU, title, descriptionisActive- filter by active status (true/false/null for all)brandId- filter by brand IDcategoryId/categorySlug- filter by categorysupplierId- filter by suppliercolourId/sizeId- filter by colour/size variantminPrice/maxPrice- price range filterpriceListId- price list used for price filteringinStock- only products with stock (true/false)
Behavior:
- Returns paginated
PagedResponse<ProductModel>including per product:brand— full brand object{ Id, Name, Code }(no separate brand request needed)unitPrice— product-level base pricestock— product-level stock listproductVariants— each variant includes its ownunitPriceandstock(perProductVariantId)- product properties, images, SEO, dimension types
- Uses batch loading to avoid N+1 query issues
Errors:
- Returns
404if no products exist
Authorization:
[AllowAnonymous]— no token required
GET
/api/v1/Products/{productId}/related
Returns all related products for a given product.
Route parameters:
productId(long, required) - product identifier
Response:
List<ProductModel>
Authorization:
[AllowAnonymous]— no token required
GET
/api/v1/Products/sku/{sku}
Returns a single product identified by its SKU.
Behavior:
- Looks up product by
SKU - Loads related stock and properties
- Returns
404if product does not exist
Authorization:
- Requires Bearer Token
POST
/api/v1/Products
Creates one or more new products.
Request body:
- Array of
UploadProductModel
Behavior:
- Supports bulk product creation
- Each product is validated individually
- Duplicate SKUs are skipped
- Brands are resolved by name:
- Existing brands are reused
- Missing brands are created automatically
- Empty brand defaults to "Unknown brand"
- Creates:
- Product record
- Initial price
- Initial stock (if provided)
- Product properties (if provided)
- All operations are executed in a single transaction
- Successful creation writes audit log entries
Errors:
- Returns
400if no valid products are provided
Authorization:
- Requires Bearer Token
POST
/api/v1/Products/import
Imports products from a CSV file.
Request:
multipart/form-data- CSV file upload
Behavior:
- Validates CSV structure and content
- Supports large files via batch processing (
1000) - Automatically splits records into:
- new products
- existing products (updates)
- Performs:
- product creation
- product updates
- pricing updates
- stock updates
- property updates
- Partial failures are logged
- Import completes even if some rows are invalid
Errors:
- Returns
400if import - - CSV parsing errors are logged with row number
Authorization:
- Requires Bearer Token
PUT
/api/v1/Products/{id}
Updates an existing product.
Request:
- Product ID is taken from the route
- Request body is a list of
UploadProductModel(single-item list is expected for ID-based update)
Behavior:
- Updates product core fields:
- name
- description
- summary
- tax code
- active flag
- Updates pricing if
UnitPriceis provided - Updates or creates stock entries per warehouse
- Updates or creates product properties
- Changes are applied selectively (field-by-field comparison)
- All updates run inside a transaction
- Audit logs are written only if changes occur
Errors:
- Returns
400if update - or product does not exist
Authorization:
- Requires Bearer Token
DELETE
/api/v1/Products/{id}
Deletes a product by its identifier.
Behavior:
- Performs a hard delete
- Removes:
- product
- stock items
- product prices
- product property values
- Successful deletion writes an audit log entry
Errors:
- Returns
400if product does not exist or deletion -
Authorization:
- Requires Bearer Token
Models
ProductModel
Returned by GET endpoints.
Fields:
Id (type: long)— internal identifierSKU (type: string?)— product SKUTitle (type: string?)— product nameSummary (type: string?)— short descriptionDescription (type: string?)— full descriptionBrand (type: BrandModel?)— resolved brand object{ Id, Name, Code }TaxCodeId (type: long?)— FK to tax codeUnitOfMeasureId (type: long?)— FK to unit of measureWeightKg (type: decimal?)— weight in kilogramsSupplierEntityId (type: long?)— FK to the supplier entity this product belongs toIsActive (type: bool)— whether the product is activeIsRestricted (type: bool)— whether the product requires access permissionIsStockReq (type: bool)— whether stock is required before saleDefaultCustomisationNotes (type: string?)— default customisation instructions for this product (applies to all customers unless overridden byNotes)CreatedAt (type: DateTime)— creation timestampProperties (type: List<ProductPropertyItemModel>)— dynamic propertiesImages (type: List<ProductImageModel>?)— product imagesVariants (type: List<VariantResponseModel>)— product variantsNotes (type: EntityProductNotesModel?)— customer-specific customisation and packing notes; populated only when the request includes anentityIdand a matching record exists inEntityProductNotes.nullif no record exists for this product/entity combination.
UploadProductModel
Used for POST (create) and PUT (update) operations.
Fields:
Id (type: long)— required for PUT; set from route on updateCode (type: string?)— product SKUName (type: string?)— product nameDescription (type: string?)— full descriptionSummary (type: string?)— short descriptionIsActive (type: bool?)— active flagBrandId (type: long?)— FK to brand (takes precedence overBrandname if both are provided)Brand (type: string?)— brand name; resolved to existing brand or created automatically. Defaults to"Unknown brand"if emptyTaxCode (type: long)— FK to tax codeWarehouse (type: long?)— warehouse ID for initial stockQuantityOnHand (type: decimal?)— initial stock quantityReorderLevel (type: decimal?)— reorder thresholdQuantityAllocated (type: decimal?)— allocated quantityLocation (type: string?)— bin/shelf locationUnitPrice (type: decimal?)— initial product priceParentId (type: long)— parent product ID (0 = top-level)SupplierEntityId (type: long)— supplier entity FK (0 = unassigned)ProductProperties (type: Dictionary<string, string>)— dynamic key/value properties
Notes
- Validation is enforced in the service layer
- Product updates are transactional
- Bulk operations are optimized for large datasets
- Batch size:
1000 - Large dataset threshold:
1000 - All state-changing operations are audited
- Internal errors are logged and not exposed to clients