Product Variants
Feature deep dive. Service identity and async surface live in Commerce Overview and API Events.
A ProductVariant represents a specific sellable configuration of a product (e.g. size, color). Each variant has its own ProductInfo, identifiers (SKU, barcode), optional pricing data, and MetaLinks (images).
Overview
| Property | Value |
|---|---|
| Entities | ProductVariant, ProductInfo, ProductIdentifier, MetaLink |
| Base path | /product-variants (live spec: /v1/api/commerce/doc/openapi.json) |
| Structural type | ProductVariant.type — see Domain Model §4.1 |
| Pricing | catalog stores pricing data; computation lives in @nx/pricing |
Entity Model
See Domain Model §3.2. Field tables are maintained there.
REST Endpoints
Full reference: live OpenAPI at
/v1/api/commerce/doc/openapi.json. Custom routes:
| Method | Path | Purpose |
|---|---|---|
POST | /product-variants/aggregate | Create variant + info + identifiers + MetaLinks |
PATCH | /product-variants/{id}/aggregate | Update variant + info + identifiers atomically |
Identifier resolution
GET /product-variants/{id} resolves by:
- Direct ID match
slugfield match
ProductVariantService
File: src/services/product-variant.service.ts
Methods
| Method | Purpose |
|---|---|
findByIdentifier({ identifier, filter?, transaction? }) | Resolve by ID or slug |
createAggregate({ data, transaction? }) | Create variant + info + identifiers + MetaLinks |
updateByIdProductVariantAggregate({ id, data, transaction? }) | Update variant + info + identifiers |
createAggregate() request
{
// ProductVariantInsertSchema fields
productId: string;
status?: string;
// Aggregate-specific
info: ProductInfoInsertSchema; // required — { name: { en, vi }, description: { en, vi } }
pricing?: PricingSchema; // optional — variant-level pricing
metaLinks?: MetaLinkUpdateSchema[]; // optional — images/assets (upserted by ID)
barcode?: string; // optional — creates ProductIdentifier (scheme: BARCODE)
sku?: string; // optional — creates ProductIdentifier (scheme: SKU)
}What it does:
- Creates the
ProductVariantrow with metadata{ merchantId, categoryId }from parent Product - Creates
ProductInfofor the variant - Creates
ProductIdentifierwith schemeSYSTEM(auto-generated) - If
skuprovided: createsProductIdentifierwith schemeSKU - If
barcodeprovided: createsProductIdentifierwith schemeBARCODE - Upserts
MetaLinkrecords if provided
updateByIdProductVariantAggregate() request
{
// ProductVariantUpdateSchema fields (partial)
info?: ProductInfoInsertSchema; // partial update
pricing?: PricingSchema;
barcode?: string; // upserts BARCODE identifier
sku?: string; // upserts SKU identifier
}Events
| Channel | Signal | When | Consumers |
|---|---|---|---|
| CDC (Debezium) | public.ProductVariant | on row write | Inventory (seed InventoryItem for STOCKABLE_SET types), Pricing (fare init from pricing data), Search (index) |
Commerce does not application-emit
product-variant.created/.updatedKafka topics — the variant-update Kafka enqueue (_enqueueProductVariantUpdated) is commented out inproduct-variant.service.ts. Propagation is CDC-driven. See API Events.
Deletion
Handled by DeletionPolicyService.deleteProductVariantById():
| Condition | Behavior |
|---|---|
| Variant has sale history (orders) | HTTP 400 — archive instead |
Last variant of the product + canDeleteLastVariant=false | HTTP 409 |
| Otherwise | Deleted |
There is no generateCombinations(), cartesianProduct(), or automatic variant generation endpoint. Variants are created one at a time via the aggregate endpoint.
Related Pages
| Page | Description |
|---|---|
| Commerce Overview | Service identity + catalog |
| Domain Model | Variant field tables + ProductVariantTypes |
| Products | Parent product management |
| Pricing in Commerce | Variant pricing data flow |
| ADR-0003 | Variant type discriminator model |