Skip to content

PRD: Promotion setup & lifecycle

ModuleCampaignPRD IDPRD-PRM-001
StatusReady to devFEATPRM
EpicPlaneBANA-1492
Date2026-03-23Versionv1.0
Packages@nx/pricingURDPRM
SurfaceClient · Owner/Mgr
OwnerPhát Nguyễn

TL;DR

A merchant builds a promotion in one operation - discount method (percentage or fixed), eligibility/source/target conditions, validity window, usage limit - then runs it through a clear DRAFT → ACTIVATED → DEACTIVATED/EXPIRED/ARCHIVED lifecycle. Each promotion stays inside its own merchant, so every storefront owns its offers end to end.

1. Context & Problem

Merchants want targeted discounts to drive sales, but pricing only knew fares and their conditions - no promotion concept, no discount method, no lifecycle. A merchant had no way to author a structured discount, say who and what it applies to, cap its usage, or schedule when it runs.

This increment makes promotions a first-class pricing concept. The condition is generalized so fares and promotions share one eligibility/source/target model; pairing a promotion with a discount method gives the merchant full create/edit/remove control, a validity window, a usage limit, and a status-driven lifecycle. Each promotion stays inside the merchant's own merchant, so every storefront owns its own catalogue of offers.

2. Goals & Non-Goals

Goals

  • A promotion paired with its discount method as first-class pricing concepts, created with method and conditions in one atomic operation.
  • A discount method on the promotion: FIXED / PERCENTAGE, with a target type and an allocation strategy.
  • A reusable condition for eligibility, source, and target rules - shared with fares.
  • A status-driven lifecycle defaulting to DRAFT, covering DRAFT → ACTIVATED → DEACTIVATED / EXPIRED / ARCHIVED.
  • A validity window and a usage-limit field per promotion (the limit is checked/incremented at apply-time, which ships with APP).
  • Promotion types STANDARD and BUY_GET.
  • Per-merchant scoping - each promotion belongs to one merchant.

Non-Goals

3. Success Metrics

MetricTarget / signal
Atomicity100% of promotion creates persist promotion + method + conditions together, or none on error
Lifecycle integrityEvery promotion sits in exactly one status from the standard lifecycle set, defaulting to DRAFT (ordered-transition validation is not yet enforced - status changes go through a generic update)
Tenancy isolationA merchant only ever sees and mutates promotions in its own merchant
Setup coverageMerchants can configure percentage and fixed offers with eligibility/target conditions without engineering help

4. Personas & Use Cases

PersonaGoal in this feature
OwnerCreate, activate, and archive any promotion for the merchant
ManagerAuthor and manage promotions - method, conditions, validity, usage limit

Core scenarios: create a promotion with its method and conditions in one step → set validity window and usage limit → activate it → later deactivate, let it expire, or archive it.

5. User Stories

  • As a manager, I want to create a promotion with its discount method and conditions in one step, so the offer is fully defined in a single operation.
  • As a manager, I want to choose a FIXED or PERCENTAGE method with a target type and allocation, so the discount behaves the way the campaign needs.
  • As a manager, I want to define eligibility, source, and target conditions, so the promotion only qualifies the right customers and products.
  • As a manager, I want to set a validity window and a usage limit, so the offer runs for a fixed period and cannot be over-redeemed.
  • As an owner, I want to move a promotion through its lifecycle (activate, deactivate, expire, archive), so I control when an offer is live.
  • As an owner, I want promotions confined to my own merchant, so my offers never leak across storefronts.

6. Functional Requirements

#RequirementURD ref
FR-1Create a promotion with method and conditions in one atomic operation (all or none on error)URD-PRM-001
FR-2Lifecycle statuses DRAFT → ACTIVATED → DEACTIVATED / EXPIRED / ARCHIVED, defaulting to DRAFT (status field built; ordered-transition guard not yet implemented for promotions)URD-PRM-002
FR-3Discount method FIXED / PERCENTAGE, with target type and allocation strategyURD-PRM-001
FR-4Define eligibility, source, and target conditions via the shared conditionURD-PRM-003
FR-5Promotion types STANDARD and BUY_GETURD-PRM-001
FR-6Validity-window fields on the promotionURD-PRM-002
FR-7Usage-limit field per promotion, with limit-check and atomic-increment helpers; runtime enforcement happens at apply-time and is not yet liveURD-PRM-004
FR-8Per-merchant scoping - each promotion belongs to one merchant, with lookups by merchant and statusURD-PRM-001

Full requirement text and acceptance criteria live in the Campaign URD. This PRD references them rather than restating them.

7. Non-Functional Requirements

AreaRequirement
Data integrityPromotion, method, and conditions are created together atomically - partial failures don't leave a half-written promotion
Lifecycle validityStatus defaults to DRAFT and uses the standard lifecycle statuses (no custom states); an ordered-transition state machine is not yet in place (unlike fare sets, which guard transitions)
Tenancy & authzAll operations confined to the user's own merchant; gated by promotion permissions
ReuseThe condition is shared between fares and promotions - one condition model, two consumers
CascadeRemoving a promotion cascades to its method and conditions
i18nUser-facing labels/statuses are bilingual (English + Vietnamese)

8. UX & Flows

Key screens (in the merchant admin app): the promotion list view, plus promotion create/edit for the method, conditions, validity window, and usage limit.

9. Data & Domain

ConceptRole
PromotionThe promotion document - type, status, validity window, usage limit, owning merchant
Discount methodThe discount method - FIXED/PERCENTAGE, target type, allocation strategy
ConditionShared eligibility/source/target condition (reused by fares and promotions)

Conceptual only - full schema and invariants in the pricing domain model and the promotion docs.

10. Dependencies & Assumptions

Depends on

  • Shared condition - generalized from the fare condition so promotions and fares share one condition model.
  • Products (Products) - target conditions reference products and categories.
  • Customer (Customer) - eligibility conditions reference customer segments.

Assumptions

  • A promotion is expected to have a valid method before it activates (constraint C-01); this is not yet enforced in code - activation is a plain status update with no method guard.
  • Each merchant owns its own promotions; every promotion is tied to a merchant.

11. Risks & Open Questions

Risk / questionMitigation / status
Promotion/method/condition writes could diverge on partial failureCreated atomically as one unit - all or none
Generalizing the fare condition into a shared condition could break fare consumersShared condition verified against both fare and promotion usage
Promotions configured but not yet applied at checkoutAccepted - discount application is the separate APP feature (P2); discount computation is out of scope here
Activating a promotion without a methodConstraint C-01 (method required before activation) is specified but not yet enforced - no activation guard exists today; to be added with the apply-time path

12. Release Plan & Launch Criteria

AspectPlan
PhaseP1 (foundation) - see URD feature catalog
RolloutAll merchants; no feature flag
MigrationNew promotion and discount-method records; the fare condition generalized into a shared condition; per-merchant ownership added to promotions
Launch criteriaAtomic create persists promotion + method + conditions; status defaults to DRAFT and status updates apply; per-merchant isolation verified; client list renders
MonitoringPromotion count per merchant, create error rate, lifecycle transition errors

13. FAQ

Are a promotion's method and conditions created separately? No - promotion, method, and conditions are created together in one atomic operation; on error none are persisted.

What discount methods are supported? FIXED and PERCENTAGE, each with a target type and an allocation strategy.

What is the lifecycle? A promotion starts in DRAFT and moves through ACTIVATED → DEACTIVATED / EXPIRED / ARCHIVED.

Does activating a promotion apply discounts at checkout? Not yet - setup and the lifecycle status field are live, but auto-apply/coupon at checkout is the separate Discount Application feature (APP, P2). Discount computation is present in the codebase but not enabled, so computing discounts, enforcing usage limits, and guarding activation by method are all out of scope here.

Can a promotion be shared across merchants? No - each promotion is confined to one merchant; a merchant only sees and mutates its own.

References

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.