Markdowns Module
Seasonal/clearance price markdown management for retail products.
Domain Concepts
Plan
A top-level container (e.g., "Spring Clearance 2026") that groups related markdown waves. Plans can be activated/deactivated and cannot be deleted while they have active waves.
Wave
A markdown event within a plan. Each wave defines:
- Method:
percent(percentage off) oramount(fixed amount off) - Value: The discount amount/percentage
- Mode:
replace(override price) orcompound(stack on existing markdowns) - Reason: Business reason (
seasonal_clearance,slow_moving,damaged, etc.) - Stage: Optional lifecycle stage (
initial,mid,final,clearance) - Floor price: Minimum allowed price after markdown
- Min margin %: Minimum gross margin threshold
Wave Item
An individual product variant affected by a wave. Created by expanding the wave's scope. Contains pre-computed prices, margins, and flags (below cost, excluded).
Wave Location
Tracks per-location activation schedule. Locations can be activated at different times (staggered rollout).
Scope
Defines which products are affected. Supports four scope types:
variant— specific product variant UUIDsproduct— all active variants of specified productscategory— all active variants in specified categoriescollection— all active variants in specified collections
Scopes are expanded into wave items at creation time. Items include deduplication — a variant appearing in multiple scopes is only included once.
Architecture
markdowns/
├── markdowns.module.ts
├── domain/
│ ├── markdowns-repository.domain.ts # Repository port
│ ├── markdown-history-repository.domain.ts
│ ├── markdowns-service.domain.ts # Service interface
│ └── markdowns.constants.ts # Status enums, sortable keys
├── application/
│ ├── markdowns.service.ts # Business logic
│ └── processors/
│ └── markdown-activation.processor.ts # BullMQ scheduled activation
├── infrastructure/
│ ├── markdowns.repository.ts # Kysely implementation
│ └── markdown-history.repository.ts
└── interfaces/
├── markdown-plans.controller.ts
├── markdown-waves.controller.ts
├── markdown-history.controller.ts
├── markdown-reports.controller.ts
└── dtos/
├── create-markdown-plan.dto.ts
├── update-markdown-plan.dto.ts
├── create-markdown-wave.dto.ts
├── update-markdown-wave.dto.ts
└── activate-markdown-wave.dto.ts
Main Flows
1. Create and Activate Markdown
POST /markdown-plans → Create plan
POST /markdown-waves/plans/:id → Create wave (scope expands to items)
POST /markdown-waves/:id/preview → Review impact
POST /markdown-waves/:id/activate → Apply prices
2. Scheduled Activation
A BullMQ processor runs every minute. It finds pending wave locations whose activate_at has passed and activates them. If a location's business location is deactivated, the wave location is skipped.
3. Reversal
POST /markdown-waves/:id/reverse → Restore original prices
Active locations are reversed, pending locations are skipped. History entries record the reversal.
4. Export
GET /markdown-waves/:id/export?format=csv → Download CSV
Enriched with SKU, product name, variant label, inventory on-hand, value impact.
API Endpoints
Plans (/markdown-plans)
| Method | Path | Description |
|---|---|---|
| POST | /markdown-plans | Create plan |
| GET | /markdown-plans | List plans (paginated) |
| GET | /markdown-plans/:id | Get plan by ID |
| PATCH | /markdown-plans/:id | Update plan |
| DELETE | /markdown-plans/:id | Delete plan |
Waves (/markdown-waves)
| Method | Path | Description |
|---|---|---|
| POST | /markdown-waves/plans/:planId | Create wave |
| GET | /markdown-waves/plans/:planId | List waves by plan |
| GET | /markdown-waves/:id | Get wave (with locations + scope) |
| PATCH | /markdown-waves/:id | Update wave (draft only) |
| DELETE | /markdown-waves/:id | Delete wave (draft/scheduled only) |
| POST | /markdown-waves/:id/activate | Activate wave |
| POST | /markdown-waves/:id/preview | Preview impact |
| GET | /markdown-waves/:id/export | Export CSV |
| POST | /markdown-waves/:id/reverse | Reverse wave |
| GET | /markdown-waves/variants/:variantId/active | Active markdown for variant |
History (/markdown-history)
| Method | Path | Description |
|---|---|---|
| GET | /markdown-history/variant/:variantId | Variant markdown history |
Reports (/markdowns/reports)
| Method | Path | Description |
|---|---|---|
| GET | /markdowns/reports/performance | Performance report |
All endpoints require businessId as a query parameter and Bearer token authentication.
Wave Lifecycle
draft → scheduled → active → reversed
↓
(skipped if location inactive)
- draft: Wave is being configured. Items and locations can be modified.
- scheduled: Wave is activated but some locations have future
activate_atdates. - active: All locations are active. Markdown prices are in effect.
- reversed: Markdown has been rolled back. Original prices restored.
Design Decisions
- Scope expansion at creation time: Wave items are materialized when the wave is created, not at activation. This allows preview without side effects.
- Per-location scheduling: Different locations can activate at different times (staggered rollout for multi-store retail).
- Floor price protection: Items priced below the floor are automatically excluded from the wave.
- Below-cost confirmation: Activation requires explicit confirmation if items would be priced below their average cost.
- Price cache invalidation: Activating or reversing a wave invalidates the business price cache.