Skip to content

Product Variants Guide

Overview

The product variants system allows you to manage multiple variations of a single product (e.g., T-shirts in different sizes and colors) through a unified Master Product system. This guide explains how to create, manage, and work with product variants using Domain-Driven Design (DDD) principles.

Architecture Concepts

Master Product vs Product

The system uses a two-tier architecture:

  • Master Product: The template/source of truth that defines variant configuration and default properties

    • NOT directly purchasable
    • Defines variant attributes (e.g., Size: S, M, L | Color: Red, Blue)
    • Stores base price, cost, weight, dimensions, and images
    • Can have 0 or more Product variants
  • Product: The actual purchasable SKU

    • Always belongs to a Master Product
    • Can inherit properties from Master Product or override them
    • Has a specific variant selection (e.g., Size: M, Color: Red)
    • Manages its own inventory and stock

Key Characteristics

  1. Aggregate Root Pattern: Master Product is the aggregate root that controls all variant operations
  2. Property Inheritance: Products inherit base properties unless explicitly overridden
  3. Variant Configuration: Immutable value object defining allowed attribute combinations
  4. Type Safety: Strong typing with Value Objects (Money, VariantConfiguration, VariantAttribute)

Variant Configuration

Creating a Variant Configuration

Variant configuration defines what attributes variants can have and their possible values. You set this when creating a product via the REST API.

Endpoint: POST /api/v1/operations/products

Request Body:

json
{
  "name": "Cotton T-Shirt",
  "sku": "TSHIRT-MASTER",
  "type": "master",
  "selling_price": 19.99,
  "cost_price": 10.00,
  "currency": "USD",
  "variant_attributes": {
    "attributes": [
      {
        "name": "Size",
        "values": ["S", "M", "L", "XL"]
      },
      {
        "name": "Color",
        "values": ["Red", "Blue", "Green"]
      }
    ],
    "allowCombinations": true
  }
}

Response (201 Created):

json
{
  "message": "Product created successfully",
  "data": {
    "id": 1,
    "name": "Cotton T-Shirt",
    "sku": "TSHIRT-MASTER",
    "type": "master",
    "selling_price": 19.99,
    "variant_attributes": {
      "attributes": [
        {
          "name": "Size",
          "values": ["S", "M", "L", "XL"]
        },
        {
          "name": "Color",
          "values": ["Red", "Blue", "Green"]
        }
      ],
      "allowCombinations": true
    }
  }
}

What This Creates:

  • A master product that can have 4 sizes × 3 colors = 12 possible variant combinations
  • No actual product variants yet (use the Generate Variants endpoint to create them)

Variant Configuration Properties

PropertyTypeRequiredDescription
attributesarray✅ YesArray of variant attribute objects (e.g., Size, Color)
attributes[].namestring✅ YesAttribute name (e.g., "Size", "Color", "Material")
attributes[].valuesarray✅ YesArray of possible values (e.g., ["S", "M", "L"])
allowCombinationsbooleanNoWhether to allow all attribute combinations (default: true)

Updating Variant Configuration

You can update the variant configuration using the update endpoint:

Endpoint: PUT /api/v1/operations/products/{id}

Request Body:

json
{
  "variant_attributes": {
    "attributes": [
      {
        "name": "Size",
        "values": ["XS", "S", "M", "L", "XL", "XXL"]
      },
      {
        "name": "Color",
        "values": ["Red", "Blue", "Green", "Black"]
      }
    ],
    "allowCombinations": true
  }
}

Note: Updating variant configuration does not automatically regenerate existing variants. You must manually delete and regenerate if needed.

API Endpoints

All variant endpoints are under /api/v1/operations/master-products/{master}/variants

1. Generate All Variants

Automatically creates all possible variant combinations.

Endpoint: POST /api/v1/operations/master-products/{master}/variants/generate

Request Body:

json
{
  "auto_generate_sku": true,
  "sku_prefix": "TSHIRT",
  "initial_stock": 10
}

Response (201 Created):

json
{
  "message": "Generated 12 variant(s) successfully",
  "data": [
    {
      "id": 101,
      "master_product_id": 1,
      "sku": "TSHIRT-S-RED",
      "name": "Cotton T-Shirt - S / Red",
      "variant_selection": {
        "Size": "S",
        "Color": "Red"
      },
      "selling_price": 19.99,
      "cost_price": 10.00,
      "currency": "USD",
      "stock_quantity": 10,
      "status": "draft"
    }
    // ... 11 more variants
  ],
  "meta": {
    "total_generated": 12
  }
}

Business Logic:

  • Skips combinations that already exist
  • Inherits base_price, base_cost_price, base_weight from Master Product
  • Auto-generates SKU if not provided: {slug}-{Size}-{Color}
  • All variants start in "draft" status

Validation Rules:

  • Master Product must have variant configuration
  • Cannot generate if Master Product has no variant_attributes
  • Generates only missing combinations (idempotent)

2. Create Single Variant

Creates a specific variant with custom properties.

Endpoint: POST /api/v1/operations/master-products/{master}/variants

Request Body:

json
{
  "variant_selection": {
    "Size": "M",
    "Color": "Blue"
  },
  "sku": "CUSTOM-M-BLUE",
  "selling_price": 24.99,
  "cost_price": 12.00,
  "stock_quantity": 50,
  "weight": 0.35
}

Response (201 Created):

json
{
  "message": "Variant created successfully",
  "data": {
    "id": 102,
    "master_product_id": 1,
    "sku": "CUSTOM-M-BLUE",
    "name": "Cotton T-Shirt - M / Blue",
    "variant_selection": {
      "Size": "M",
      "Color": "Blue"
    },
    "selling_price": 24.99,
    "cost_price": 12.00,
    "currency": "USD",
    "weight": 0.35,
    "stock_quantity": 50,
    "status": "draft",
    "overrides": ["price", "weight"]
  }
}

Validation Rules:

php
[
    'variant_selection' => ['required', 'array', 'min:1'],
    'variant_selection.*' => ['required', 'string'],
    'sku' => ['sometimes', 'string', 'max:100', 'unique:operations_products'],
    'selling_price' => ['sometimes', 'nullable', 'numeric', 'min:0', 'max:9999999.99'],
    'cost_price' => ['sometimes', 'nullable', 'numeric', 'min:0', 'max:9999999.99'],
    'stock_quantity' => ['sometimes', 'integer', 'min:0', 'max:999999'],
    'weight' => ['sometimes', 'nullable', 'numeric', 'min:0', 'max:99999.9999'],
]

Business Logic:

  • Validates variant_selection matches Master Product configuration
  • Prevents duplicate variant combinations
  • Auto-generates name: {master.name} - {Size} / {Color}
  • Tracks which properties are overridden in overrides field

Error Responses:

422 Unprocessable Entity - Invalid variant selection:

json
{
  "message": "Failed to create variant",
  "error": "Invalid value 'XXL' for attribute 'Size'. Allowed values: S, M, L, XL"
}

422 Unprocessable Entity - Duplicate combination:

json
{
  "message": "Failed to create variant",
  "error": "A variant with this combination already exists"
}

3. List All Variants

Retrieves all variants for a Master Product.

Endpoint: GET /api/v1/operations/master-products/{master}/variants

Query Parameters:

  • page: Page number (default: 1)
  • per_page: Items per page (default: 50)
  • status: Filter by status (active, draft, discontinued)
  • include: Relationships to include (category, supplier, inventory)

Response (200 OK):

json
{
  "data": [
    {
      "id": 101,
      "sku": "TSHIRT-S-RED",
      "name": "Cotton T-Shirt - S / Red",
      "variant_selection": {
        "Size": "S",
        "Color": "Red"
      },
      "selling_price": 19.99,
      "stock_quantity": 45,
      "status": "active"
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 50,
    "total": 12,
    "last_page": 1
  }
}

4. Get Available Variant Values

Shows which variant combinations are still available (not yet created).

Endpoint: GET /api/v1/operations/master-products/{master}/variants/available

Response (200 OK):

json
{
  "data": {
    "Size": {
      "used_values": ["S", "M"],
      "available_values": ["L", "XL"]
    },
    "Color": {
      "used_values": ["Red", "Blue"],
      "available_values": ["Green"]
    }
  },
  "meta": {
    "total_combinations": 12,
    "used_combinations": 6,
    "available_combinations": 6
  }
}

Use Case: Display to users which combinations can still be created.


5. Bulk Update Variant Prices

Updates selling price for multiple variants at once.

Endpoint: POST /api/v1/operations/master-products/{master}/variants/bulk-update-prices

Request Body:

json
{
  "action": "set_price",
  "price": 24.99,
  "filters": {
    "variant_selection": {
      "Size": "L"
    }
  }
}

Alternative Actions:

Percentage increase:

json
{
  "action": "increase_percentage",
  "percentage": 10,
  "filters": {
    "variant_selection": {
      "Color": "Red"
    }
  }
}

Response (200 OK):

json
{
  "message": "Updated prices for 3 variants",
  "data": {
    "updated_count": 3,
    "affected_variants": [101, 102, 103]
  }
}

6. Bulk Update Variant Stock

Updates stock quantity for multiple variants.

Endpoint: POST /api/v1/operations/master-products/{master}/variants/bulk-update-stock

Request Body:

json
{
  "action": "set_quantity",
  "quantity": 100,
  "filters": {
    "variant_selection": {
      "Size": "M"
    }
  }
}

Response (200 OK):

json
{
  "message": "Updated stock for 3 variants",
  "data": {
    "updated_count": 3,
    "total_stock_added": 300
  }
}

7. Delete All Variants

Deletes all variants from a Master Product.

Endpoint: DELETE /api/v1/operations/master-products/{master}/variants

Request Body:

json
{
  "reason": "Updating product configuration"
}

Response (200 OK):

json
{
  "message": "Deleted 12 variants successfully",
  "data": {
    "deleted_count": 12
  }
}

Business Rules:

  • CANNOT delete variants with active inventory
  • Requires a reason for audit trail
  • Logs deletion to application logs

Error Response (422):

json
{
  "message": "Cannot delete variants with active inventory. Found 5 variants with inventory."
}

Property Inheritance

Products can inherit properties from their Master Product or override them.

Inheritable Properties

PropertyMaster FieldProduct FieldDefault Behavior
Pricebase_priceselling_priceInherit if NULL
Costbase_cost_pricecost_priceInherit if NULL
Weightbase_weightweightInherit if NULL
Dimensionsbase_dimensionsdimensionsInherit if NULL
CurrencycurrencycurrencyInherit if NULL
ImagesimagesimagesMerge both

Inheritance Example

Master Product:

json
{
  "id": 1,
  "name": "Cotton T-Shirt",
  "base_price": 19.99,
  "base_cost_price": 10.00,
  "base_weight": 0.30,
  "currency": "USD"
}

Product (inheriting):

json
{
  "id": 101,
  "master_product_id": 1,
  "selling_price": null,  // Will inherit 19.99
  "cost_price": null,     // Will inherit 10.00
  "weight": null          // Will inherit 0.30
}

Product (overriding):

json
{
  "id": 102,
  "master_product_id": 1,
  "selling_price": 24.99,  // Override: Premium size
  "cost_price": 12.00,     // Override
  "weight": 0.35,          // Override: Larger size
  "overrides": ["price", "cost_price", "weight"]
}

Checking Overrides

When you retrieve a product via the API, the response includes which properties are overridden:

bash
GET /api/v1/operations/products/102

Response:

json
{
  "data": {
    "id": 102,
    "master_product_id": 1,
    "selling_price": 24.99,
    "cost_price": 12.00,
    "weight": 0.35,
    "overrides": ["price", "cost_price", "weight"],
    "effective_values": {
      "price": 24.99,
      "cost_price": 12.00,
      "weight": 0.35
    }
  }
}

Business Rules & Validation

Variant Creation Rules

  1. Master Product must have variant configuration

    • variant_attributes field must be set
    • Configuration must have at least one attribute
  2. Variant selection must match configuration

    • All required attributes must be present
    • All values must be valid for their attributes
    • No extra attributes allowed
  3. No duplicate combinations

    • Each variant selection must be unique within a Master Product
    • Comparison is based on the complete combination
  4. SKU uniqueness

    • SKUs must be unique across all products (tenant-wide)
    • Auto-generated SKUs follow pattern: {master.slug}-{attr1}-{attr2}

Deletion Rules

  1. Cannot delete variants with inventory

    • Checks all warehouses for stock records
    • Must transfer/remove inventory first
  2. Requires audit reason

    • Deletion reason logged for compliance
    • Helps track why variants were removed

Price & Cost Rules

  1. Monetary values use Money Value Object

    • Prevents floating-point precision errors
    • Stored as cents in database
    • Currency awareness enforced
  2. Validation limits

    • Min: 0.00 (cannot be negative)
    • Max: 9,999,999.99
    • Decimal precision: 2 places

Domain Model Reference

MasterProduct Model

File: app/Modules/Operations/Domain/Models/MasterProduct.php

Key Methods:

php
// Variant detection
hasVariants(): bool                    // Check if variants enabled
isSingleProduct(): bool                // Check if standalone product
isEmpty(): bool                        // Check if no products exist

// Variant operations (Aggregate Root)
addVariant(array $selection, array $overrides): Product
generateAllVariants(array $defaults): Collection
updateVariantConfiguration(VariantConfiguration $config, bool $regenerate): void
deleteAllVariants(string $reason): int

// Read-only access
getVariants(): Collection
getAvailableVariantValues(): array

// Aggregations
getTotalStock(): int
getLowestPrice(): ?Money
getHighestPrice(): ?Money
getPriceRange(): array

Product Model

File: app/Modules/Operations/Domain/Models/Product.php

Key Methods:

php
// Variant detection
isVariant(): bool                      // Has sibling variants
isStandalone(): bool                   // Only product of master

// Property inheritance
getPrice(): Money                      // Get effective price (with inheritance)
getCostPrice(): ?Money                 // Get effective cost
getWeight(): ?float                    // Get effective weight
getEffectiveDimensions(): ?ProductDimensions
getCurrency(): string
getAllImages(): array                  // Merge master + product images

// Override management
isOverridden(string $property): bool
overridePrice(float $price): void
clearPriceOverride(): void

VariantConfiguration Value Object

File: app/Modules/Operations/Domain/ValueObjects/VariantConfiguration.php

Properties:

  • attributes: Array of VariantAttribute objects
  • allowCombinations: Whether to allow all combinations

Methods:

php
fromArray(array $data): self
toArray(): array
getAttributes(): array
getAttribute(string $name): ?VariantAttribute
isValidCombination(array $values): bool
getTotalCombinations(): int
getAllCombinations(): array            // Cartesian product

VariantAttribute Value Object

File: app/Modules/Operations/Domain/ValueObjects/VariantAttribute.php

Properties:

  • name: Attribute name (e.g., "Size")
  • values: Array of possible values (e.g., ["S", "M", "L"])

Methods:

php
fromArray(array $data): self
getName(): string
getValues(): array
hasValue(string $value): bool
getValueCount(): int

Best Practices

1. Use Variant Creation Endpoints

Always create variants through the proper API endpoints:

bash
# ✅ CORRECT: Use variant creation endpoint
POST /api/v1/operations/master-products/1/variants
{
  "variant_selection": {
    "Size": "L",
    "Color": "Blue"
  },
  "selling_price": 24.99
}

# ❌ WRONG: Don't try to create products directly as variants
POST /api/v1/operations/products
{
  "master_product_id": 1,
  "variant_selection": {"Size": "L", "Color": "Blue"}
}
# This bypasses business rules and validation

2. Leverage Property Inheritance

Don't override unless necessary:

bash
# ✅ CORRECT: Inherit common properties (no overrides)
POST /api/v1/operations/master-products/1/variants
{
  "variant_selection": {
    "Size": "M",
    "Color": "Red"
  }
  # Inherits base_price, base_cost, base_weight from master
}

# Only override when different
POST /api/v1/operations/master-products/1/variants
{
  "variant_selection": {
    "Size": "XL",
    "Color": "Red"
  },
  "selling_price": 24.99  # Premium size override
}

3. Validate Before Generation

Check how many combinations will be created:

bash
# Get available variant values and total combinations
GET /api/v1/operations/master-products/1/variants/available

Response:

json
{
  "meta": {
    "total_combinations": 12,
    "used_combinations": 0,
    "available_combinations": 12
  }
}

If total_combinations > 100, consider whether you really need all combinations or if you should create variants selectively.

4. Use Bulk Operations

For multiple updates, use bulk endpoints:

bash
# ✅ CORRECT: Single request to update multiple variants
POST /api/v1/operations/master-products/1/variants/bulk-update-prices
{
  "action": "increase_percentage",
  "percentage": 10
}

# ❌ INEFFICIENT: Multiple individual requests
PUT /api/v1/operations/products/101 {"selling_price": 21.99}
PUT /api/v1/operations/products/102 {"selling_price": 21.99}
PUT /api/v1/operations/products/103 {"selling_price": 21.99}
# ... (inefficient for many variants)

5. Use Proper Number Formats

Always use numeric values for prices in the API:

bash
# ✅ CORRECT: Numeric values
POST /api/v1/operations/master-products/1/variants
{
  "variant_selection": {"Size": "M"},
  "selling_price": 19.99,
  "cost_price": 10.50
}

# ❌ WRONG: String values
{
  "selling_price": "19.99",  # Don't use strings
  "cost_price": "$10.50"      # Don't include currency symbols
}

The API automatically handles the Money Value Object conversion internally.


Common Use Cases

Use Case 1: Apparel with Size & Color

Step 1: Create Master Product with Variant Configuration

bash
POST /api/v1/operations/products
{
  "name": "Cotton T-Shirt",
  "sku": "TSHIRT-MASTER",
  "type": "master",
  "selling_price": 19.99,
  "cost_price": 10.00,
  "currency": "USD",
  "variant_attributes": {
    "attributes": [
      {
        "name": "Size",
        "values": ["S", "M", "L", "XL"]
      },
      {
        "name": "Color",
        "values": ["Red", "Blue", "Green"]
      }
    ],
    "allowCombinations": true
  }
}

Step 2: Generate All Variant Combinations

bash
POST /api/v1/operations/master-products/1/variants/generate
{
  "auto_generate_sku": true,
  "sku_prefix": "TSHIRT",
  "initial_stock": 10
}

Result: Creates 4 sizes × 3 colors = 12 variants

Use Case 2: Electronics with Storage & Color

Step 1: Create Master Product

bash
POST /api/v1/operations/products
{
  "name": "Smartphone X",
  "sku": "PHONE-X-MASTER",
  "type": "master",
  "currency": "USD",
  "variant_attributes": {
    "attributes": [
      {
        "name": "Storage",
        "values": ["64GB", "128GB", "256GB"]
      },
      {
        "name": "Color",
        "values": ["Black", "White", "Blue"]
      }
    ]
  }
}

Step 2: Create Variants with Different Pricing

bash
# 64GB Black - Base model
POST /api/v1/operations/master-products/1/variants
{
  "variant_selection": {
    "Storage": "64GB",
    "Color": "Black"
  },
  "selling_price": 699.00
}

# 128GB Black - Mid-tier
POST /api/v1/operations/master-products/1/variants
{
  "variant_selection": {
    "Storage": "128GB",
    "Color": "Black"
  },
  "selling_price": 799.00
}

# 256GB Black - Premium
POST /api/v1/operations/master-products/1/variants
{
  "variant_selection": {
    "Storage": "256GB",
    "Color": "Black"
  },
  "selling_price": 999.00
}

Use Case 3: Furniture with Custom Dimensions

Step 1: Create Master Product

bash
POST /api/v1/operations/products
{
  "name": "Office Desk",
  "sku": "DESK-MASTER",
  "type": "master",
  "selling_price": 299.00,
  "currency": "USD",
  "variant_attributes": {
    "attributes": [
      {
        "name": "Size",
        "values": ["Small", "Medium", "Large"]
      },
      {
        "name": "Finish",
        "values": ["Oak", "Walnut", "Black"]
      }
    ]
  }
}

Step 2: Create Variant with Custom Dimensions

bash
POST /api/v1/operations/master-products/1/variants
{
  "variant_selection": {
    "Size": "Small",
    "Finish": "Oak"
  },
  "dimensions": {
    "length": 120,
    "width": 60,
    "height": 75,
    "dimension_unit": "cm"
  },
  "weight": 25.5
}

Troubleshooting

Issue: "Variants not enabled"

Error: Cannot add variant to master product without variant configuration

Solution: Create or update the product with variant_attributes:

bash
PUT /api/v1/operations/products/1
{
  "variant_attributes": {
    "attributes": [
      {
        "name": "Size",
        "values": ["S", "M", "L"]
      }
    ]
  }
}

Issue: "Invalid variant selection"

Error: Invalid value 'XXL' for attribute 'Size'. Allowed values: S, M, L, XL

Solution: Check available values first, then use only configured values:

bash
# Check available values
GET /api/v1/operations/master-products/1/variants/available

# Create variant with valid value
POST /api/v1/operations/master-products/1/variants
{
  "variant_selection": {
    "Size": "L"  # ✅ Valid value
  }
}

Issue: "Duplicate variant"

Error: A variant with this combination already exists

Solution: Check existing variants before creating:

bash
# List existing variants
GET /api/v1/operations/master-products/1/variants

# Create only if combination doesn't exist
POST /api/v1/operations/master-products/1/variants
{
  "variant_selection": {
    "Size": "M",
    "Color": "Blue"
  }
}

Issue: "Cannot delete variants with inventory"

Error: Found 5 variants with inventory

Solution: Adjust inventory to zero first, then delete:

bash
# Option 1: Transfer inventory to another variant
POST /api/v1/operations/inventory/transfer
{
  "product_id": 101,
  "from_warehouse_id": 1,
  "to_warehouse_id": 1,
  "quantity": 50,
  "reason": "Consolidating before deletion"
}

# Option 2: Adjust inventory to zero
POST /api/v1/operations/inventory/101/adjust
{
  "quantity": -50,
  "reason": "Clearing before deletion"
}

# Then delete variants
DELETE /api/v1/operations/master-products/1/variants
{
  "reason": "Reconfiguring product"
}

Documentation for SynthesQ CRM/ERP Platform