Skip to main content

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) or amount (fixed amount off)
  • Value: The discount amount/percentage
  • Mode: replace (override price) or compound (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 UUIDs
  • product — all active variants of specified products
  • category — all active variants in specified categories
  • collection — 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)

MethodPathDescription
POST/markdown-plansCreate plan
GET/markdown-plansList plans (paginated)
GET/markdown-plans/:idGet plan by ID
PATCH/markdown-plans/:idUpdate plan
DELETE/markdown-plans/:idDelete plan

Waves (/markdown-waves)

MethodPathDescription
POST/markdown-waves/plans/:planIdCreate wave
GET/markdown-waves/plans/:planIdList waves by plan
GET/markdown-waves/:idGet wave (with locations + scope)
PATCH/markdown-waves/:idUpdate wave (draft only)
DELETE/markdown-waves/:idDelete wave (draft/scheduled only)
POST/markdown-waves/:id/activateActivate wave
POST/markdown-waves/:id/previewPreview impact
GET/markdown-waves/:id/exportExport CSV
POST/markdown-waves/:id/reverseReverse wave
GET/markdown-waves/variants/:variantId/activeActive markdown for variant

History (/markdown-history)

MethodPathDescription
GET/markdown-history/variant/:variantIdVariant markdown history

Reports (/markdowns/reports)

MethodPathDescription
GET/markdowns/reports/performancePerformance 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_at dates.
  • active: All locations are active. Markdown prices are in effect.
  • reversed: Markdown has been rolled back. Original prices restored.

Design Decisions

  1. Scope expansion at creation time: Wave items are materialized when the wave is created, not at activation. This allows preview without side effects.
  2. Per-location scheduling: Different locations can activate at different times (staggered rollout for multi-store retail).
  3. Floor price protection: Items priced below the floor are automatically excluded from the wave.
  4. Below-cost confirmation: Activation requires explicit confirmation if items would be priced below their average cost.
  5. Price cache invalidation: Activating or reversing a wave invalidates the business price cache.