---
id: DOM-COM-CAT-005
title: Promotion
realises: CON-COM-CAT-005
product_area: commerce
module: catalog
revision: 1
status: draft
design_intent: proposed
implementation_evidence: partially-observed
source_sha:
  services-commerce: c8b66a98e2490617aec38ba5c8165dd9bd0a8f9b
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
owned_by: SVC-COM-CAT-promotions
readable_by:
  - SVC-COM-CAT-recommendations
lifecycle: STATE-COM-CAT-007
custom_fields: none
identity: >-
  A generated internal key identifies the promotion; a workspace number seed supplies its displayed promotion
  number. Names are unique ignoring case. Codes have workspace-unique keys after Unicode compatibility
  normalization, whitespace removal and uppercasing, while their entered spelling is retained. Redemptions,
  allocations and history have generated keys; issuance can derive its key from the caller’s idempotency key.
attributes:
  - name: kind
    kind: enum
    required: true
    computed: false
    notes: >-
      The claim method is automatic or code-based. Automatic campaigns have no codes, and existing codes block
      conversion to automatic.
  - name: names and number
    kind: text
    required: true
    computed: false
    notes: >-
      The internal name identifies the campaign to staff; the public name defaults to it and the number
      appears alongside it.
  - name: offer
    kind: collection
    required: true
    computed: false
    notes: >-
      Benefits use one to twenty area-specific parts, or a single order-total or shipping benefit; these
      shapes cannot be mixed.
  - name: combining rules
    kind: collection
    required: true
    computed: false
    notes: >-
      Combining with audience prices defaults on. Exclusivity defaults on, automatic priority defaults to one
      hundred, and optional caps and qualifying-line minimums constrain the discount.
  - name: 'window, currency and business unit'
    kind: collection
    required: false
    computed: false
    notes: 'Dates, currency for monetary benefits and optional unit scope constrain application.'
  - name: eligibility
    kind: reference
    required: false
    computed: false
    notes: Automatic promotions may require an eligibility match or non-match for a selected subject.
  - name: usage limits and counters
    kind: collection
    required: false
    computed: false
    notes: >-
      Total and party-specific ceilings compare with maintained counters; counter changes leave the edit
      version unchanged.
  - name: cancellation release
    kind: enum
    required: true
    computed: false
    notes: >-
      A retained choice controls release after full cancellation. The first personal single-use code enables
      it unless an explicit choice already exists.
  - name: status and economic lock
    kind: enum
    required: true
    computed: false
    notes: >-
      Status follows preparation, launch, pause, end and archive; first redemption stamps the separate
      protected-terms lock.
  - name: codes
    kind: collection
    required: false
    computed: false
    notes: >-
      Codes combine claim spelling, usage mode and limits, parent-bounded dates, optional listing owner,
      permitted people or organizations, eligibility, notes and active, disabled or revoked status.
  - name: issuance batches
    kind: collection
    required: false
    computed: false
    notes: >-
      A generation request combines a wildcard template, one to five thousand codes and shared defaults. Its
      batch reports requested, running, completed or failed status with generated count and failure
      explanation.
  - name: redemptions and allocations
    kind: collection
    required: false
    computed: false
    notes: >-
      Each promotion has one redemption per order, retaining terms, status and discount. Line allocations
      distinguish direct from proportional shares.
  - name: checkout evaluation
    kind: collection
    required: false
    computed: false
    notes: >-
      Each preview replaces the checkout’s full candidate assessment, including applied, rejected and
      superseded results.
  - name: history
    kind: collection
    required: true
    computed: false
    notes: >-
      Changes and lifecycle, issuance, redemption and release actions append attributed promotion-history
      entries.
  - name: performance
    kind: computed
    required: false
    computed: true
    notes: >-
      Reads derive capacity, discount and financial totals per currency, together with recent checkout refusal
      counts.
relationships:
  - to: DOM-COM-CAT-004
    cardinality: one promotion to many priced lines
    ownership: Promotions take the audience or Standard amount supplied by pricing as their discount basis.
    cross_service: false
  - to: DOM-COM-CAT-003
    cardinality: many parts to many products and categories
    ownership: Merchandise criteria choose products and shelves; excluded products never enter discount assessment.
    cross_service: false
  - to: DOM-COM-CAT-006
    cardinality: one code to many offer rules
    ownership: Recommendation acceptance can enter the code referenced by its rule on a checkout.
    cross_service: false
invariants:
  - id: INV-DOM-COM-CAT-005-01
    statement: Automatic promotions cannot hold codes; code issuance requires a code-claimed promotion.
    enforced_at:
      - service
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses to make a draft with a
      code automatic, 409 automatic_promotion_has_no_codes
  - id: INV-DOM-COM-CAT-005-02
    statement: >-
      An offer contains either one to twenty parts or a promotion-level benefit, with parts required for
      percentage and per-unit offers.
    enforced_at:
      - api
      - service
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses parts together with an
      order-total offer, 400 promotion_invalid in plain words
  - id: INV-DOM-COM-CAT-005-03
    statement: Promotion names cannot duplicate another workspace name in a different letter case.
    enforced_at:
      - service
      - database
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses a name another promotion
      of the tenant has, whatever its case, 409 promotion_name_taken
  - id: INV-DOM-COM-CAT-005-04
    statement: The service permits only declared lifecycle moves and records each move in promotion history.
    enforced_at:
      - service
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses every operation from a
      state it does not move, 409 invalid_transition
  - id: INV-DOM-COM-CAT-005-05
    statement: First redemption locks the protected economic fields against later edits.
    enforced_at:
      - service
      - database
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotion-redemptions.e2e.spec.js#names the column and
      offers to end the promotion and duplicate it
  - id: INV-DOM-COM-CAT-005-06
    statement: Normalized code keys cannot duplicate any code in the workspace.
    enforced_at:
      - service
      - database
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses “EARLY25” beside
      “early 25”, 409 code_taken
  - id: INV-DOM-COM-CAT-005-07
    statement: Code dates must remain within the parent promotion’s dates.
    enforced_at:
      - service
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses a window ending after
      the promotion’s, 400 window_exceeds_promotion
  - id: INV-DOM-COM-CAT-005-08
    statement: Configured limits cannot fall below the corresponding uses already counted.
    enforced_at:
      - service
      - database
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses a limit below the
      uses made, 409 limit_below_uses
  - id: INV-DOM-COM-CAT-005-09
    statement: A revoked code has no outgoing status change.
    enforced_at:
      - service
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#keeps a revoked code revoked:
      no operation moves it again
  - id: INV-DOM-COM-CAT-005-10
    statement: Conditional use updates prevent concurrent placements from exceeding configured capacity.
    enforced_at:
      - service
      - database
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/integration/services/persistence/redemption-concurrency.integration.spec.js#applies
      exactly 100 and answers the other 100 code_exhausted
  - id: INV-DOM-COM-CAT-005-11
    statement: >-
      An order has at most one redemption per promotion, and committed discount totals equal their line
      allocations.
    enforced_at:
      - database
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/integration/db/redemptions.table.integration.spec.js#refuses
      at COMMIT a redemption whose discount is not its allocations’ sum, naming the redemption
  - id: INV-DOM-COM-CAT-005-12
    statement: 'Frozen terms, amounts and allocations cannot be changed, and released uses cannot become applied again.'
    enforced_at:
      - database
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/integration/db/redemptions.table.integration.spec.js#refuses
      moving a released redemption back to applied, SQLSTATE RHC04
  - id: INV-DOM-COM-CAT-005-13
    statement: >-
      Redemption references retain their promotion, code and order; retirement uses lifecycle controls rather
      than deletion.
    enforced_at:
      - database
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/integration/db/redemptions.table.integration.spec.js#refuses
      deleting a promotion a redemption names, by promotion_redemptions_promotion_fk (RESTRICT)
  - id: INV-DOM-COM-CAT-005-14
    statement: A resumed issuance job generates only the codes still missing from its requested count.
    enforced_at:
      - service
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/unit/issuance/issuance-job.spec.js#continues from the codes
      that exist after a crash, making only the missing ones
physical:
  - promotions
  - promotion_parts
  - promotion_events
  - promotion_codes
  - promotion_code_restrictions
  - promotion_code_issuances
  - checkout_promotions
  - promotion_redemptions
  - order_line_discounts
evidence:
  - >-
    services-commerce at c8b66a98: modules/promotions/db/*.table.ts; services/persistence/promotions.ts,
    promotion-codes.ts, promotion-code-issuances.ts, promotion-redemptions.ts, promotion-statistics.ts,
    checkout-promotions.ts; services/promotions/promotion-workflow.ts, promotion-rules.ts;
    services/pricing/**.
  - >-
    services-commerce at c8b66a98: docs/milestones/COMM-M1-set-up-what-you-sell.md (D12, D12-F1 to F4, D13);
    decisions/COMM-M1-D12-evaluation-algorithm.md; COMM-M1b-promotions-and-codes.md (D-R1 to D-R3).
  - 'react-console at baf6dae3: components/commerce/promotions/**, lib/commerce/promotions/live.ts.'
---

# Promotion

## What It Models

A promotion describes a marketing benefit, claimed automatically or by code. It combines offer terms, qualification and stacking choices, dates, usage limits and lifecycle. Codes, generation batches, redemptions, allocation records and history belong to the same concept.

## Identity and Attributes

Promotions have an internal key, displayed number and case-insensitive unique name. Codes are looked up by normalized workspace-wide keys. Redemptions retain the terms at placement and allocate the recorded discount to order lines.

## Relationships

Pricing supplies the audience and Standard bases. Merchandise criteria select eligible product lines, and purchasing settings may exclude products from discounts. Recommendation actions can enter a code on a checkout.

## Invariants

Offer shape, code kind and parent windows are validated. Protected terms lock on first use. Conditional counter updates enforce limits during placement, and commit checks tie discount totals to allocations. Release restores capacity while retaining the original monetary records.

## How It Is Stored and Shared

Promotions owns campaigns, codes, batches, evaluations, history and redemptions. Orders calls its placement and release functions within order transactions; Accounting handles financial documents. Release also writes order history, without publishing an external event. No custom fields are declared.

## Open Design Questions

The first code establishes a promotion-wide cancellation-release default, which can affect later codes of another kind. The reversed status has no writer. Only merchandise has a registered promotion-area provider.
