---
id: CAP-COM-CAT-015
title: Issue promotion codes with controlled access
feature: FEAT-COM-CAT-04
product_area: commerce
module: catalog
revision: 1
status: draft
product_intent: proposed
implementation_evidence: observed
source_sha:
  services-commerce: c8b66a98e2490617aec38ba5c8165dd9bd0a8f9b
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
intent_sources:
  - >-
    services-commerce:docs/milestones/COMM-M1b-promotions-and-codes.md#Decisions
    (D-R2)
  - 'services-commerce:docs/milestones/COMM-M1b-promotions-and-codes.md#Scope'
  - 'services-commerce:docs/milestones/COMM-M1-set-up-what-you-sell.md#D13'
  - >-
    services-commerce:docs/milestones/plans/COMM-M1-plan.md#4. Assumptions (A22,
    A23)
purpose: >-
  Coordinators distribute code claims with identity, qualification and usage
  controls.
outcomes:
  - Claim restrictions identify the intended permitted purchasers.
  - Batch issuance supplies unique claims with protected spreadsheet export.
concepts:
  - CON-COM-CAT-005
actors:
  - ACT-COM-002
  - ACT-COM-001
  - ACT-COM-004
surfaces:
  - console
  - api
  - job
permissions:
  - key: commerce.promotions.read
    where_checked: api
    scope: tenant
  - key: commerce.promotions.manage
    where_checked: api
    scope: tenant
rules:
  - id: BR-CAP-COM-CAT-015-01
    proved_by: [ATS-COM-CAT-015-01, TST-COM-CAT-092]
    statement: >-
      Code identity is workspace-wide across promotions after case and space
      normalization, while the originally entered spelling remains available for
      display.
    kind: validation
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses
      “EARLY25” beside “early 25”, 409 code_taken
    origin: >-
      COMM-M1 plan A22 (DEF-COM-0817, Q03)
      (docs/milestones/plans/COMM-M1-plan.md)
  - id: BR-CAP-COM-CAT-015-02
    proved_by: [ATS-COM-CAT-015-02, TST-COM-CAT-091, TST-COM-CAT-096]
    statement: Only code-claimed promotions accept individual or generated codes.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses
      a code, 409 automatic_promotion_has_no_codes
    origin: COMM-M1b D-R2 (docs/milestones/COMM-M1b-promotions-and-codes.md)
  - id: BR-CAP-COM-CAT-015-03
    proved_by: [ATS-COM-CAT-015-03, TST-COM-CAT-092, TST-COM-CAT-132]
    statement: A code’s effective interval must fit within its promotion’s interval.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    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
    origin: >-
      COMM-M1 D12 Q6
      (docs/milestones/decisions/COMM-M1-D12-evaluation-algorithm.md)
  - id: BR-CAP-COM-CAT-015-04
    proved_by: [ATS-COM-CAT-015-04, TST-COM-CAT-092, TST-COM-CAT-125, TST-COM-CAT-126]
    statement: >-
      After width and space normalization, codes accept one to sixty-four ASCII
      letters, digits, hyphens or underscores and reject other characters during
      creation and entry.
    kind: validation
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses
      a code with a slash or a percent sign, 400, naming code and saying the
      sentence (D-REC-6)
    origin: none
    note: >-
      Character validation and normalization recommendations await Product
      confirmation.
  - id: BR-CAP-COM-CAT-015-05
    proved_by: [ATS-COM-CAT-015-04, TST-COM-CAT-092, TST-COM-CAT-125, TST-COM-CAT-126]
    statement: >-
      Checkout entry uses the same normalization as stored-code lookup,
      including full-width forms and spaces.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/unit/pricing/code-normalization.spec.js#resolves
      the full-width ＥＡＲＬＹ２５ to EARLY25 (NFKC)
    origin: none
  - id: BR-CAP-COM-CAT-015-06
    proved_by: [ATS-COM-CAT-015-05, TST-COM-CAT-092, TST-COM-CAT-120]
    statement: >-
      Single-use codes impose one total use and one use per customer and reject
      conflicting limits.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses
      a single-use code with another limit, 400
    origin: COMM-M1b D-R2 (docs/milestones/COMM-M1b-promotions-and-codes.md)
  - id: BR-CAP-COM-CAT-015-07
    proved_by: [ATS-COM-CAT-015-06, ATS-COM-CAT-015-16, TST-COM-CAT-132]
    statement: >-
      Codes may restrict contacts and organizations or apply their own
      eligibility rule; their listed owner organizes the code without
      restricting redemption.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/unit/pricing/validation.spec.js#does
      not restrict by owner
    origin: COMM-M1b D-R2 (docs/milestones/COMM-M1b-promotions-and-codes.md)
  - id: BR-CAP-COM-CAT-015-08
    proved_by: [ATS-COM-CAT-015-07, ATS-COM-CAT-015-17, ATS-COM-CAT-017-10, TST-COM-CAT-092]
    statement: Staff cannot reduce code limits below recorded usage.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    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
    origin: none
  - id: BR-CAP-COM-CAT-015-09
    proved_by: [ATS-COM-CAT-014-13, ATS-COM-CAT-015-08, ATS-COM-CAT-015-15, ATS-COM-CAT-015-18, ATS-COM-CAT-015-19, TST-COM-CAT-092]
    statement: >-
      Codes can move between active and disabled and from either to permanently
      revoked, with transitions retained in promotion history.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#keeps
      a revoked code revoked: no operation moves it again
    origin: none
  - id: BR-CAP-COM-CAT-015-10
    proved_by: [ATS-COM-CAT-015-09, TST-COM-CAT-118]
    statement: >-
      Generation templates use digit, letter and mixed wildcards excluding
      ambiguous characters, require six wildcards, permit one to five thousand
      codes and require a possibility space ten thousand times the requested
      count.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/unit/issuance/code-template.spec.js#refuses
      the same template for 27 codes, saying what it can make and what was asked
    origin: COMM-M1b scope item 1 (docs/milestones/COMM-M1b-promotions-and-codes.md)
  - id: BR-CAP-COM-CAT-015-11
    proved_by: [ATS-COM-CAT-015-10, ATS-COM-CAT-015-17, ATS-COM-CAT-017-10, TST-COM-CAT-106]
    statement: >-
      Unless staff override it, a promotion enables cancellation release when
      its first code is personal and single-use.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/integration/services/persistence/promotion-codes.integration.spec.js#turns
      release on cancellation on when the first code is personal and single-use,
      and says so in the history
    origin: COMM-M1 D13 (docs/milestones/COMM-M1-set-up-what-you-sell.md)
  - id: BR-CAP-COM-CAT-015-12
    proved_by: [ATS-COM-CAT-015-11, ATS-COM-CAT-015-18, ATS-COM-CAT-015-19, TST-COM-CAT-091, TST-COM-CAT-119]
    statement: >-
      Code issuance starts asynchronously, reuses the batch for a repeated
      request, resumes missing codes after interruption and retains completed
      codes with a readable failure message.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    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
    origin: none
  - id: BR-CAP-COM-CAT-015-13
    proved_by: [ATS-COM-CAT-015-12, TST-COM-CAT-120]
    statement: >-
      CSV exports prefix cells beginning with formula characters so opening the
      file cannot interpret those cells as formulas.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/unit/issuance/issuance-request-csv.spec.js#prefixes
      cells starting with = + - @ with a single quote
    origin: none
  - id: BR-CAP-COM-CAT-015-14
    proved_by: [ATS-COM-CAT-015-13, TST-COM-CAT-095]
    statement: >-
      Code management and issuance require management permission, while code,
      redemption and batch inspection require read permission.
    kind: permission
    enforced_at:
      - api
      - console
    must_prove: true
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotions-security.e2e.spec.js#is
      refused every write, 403 permission_missing
    origin: COMM-M1 plan A15 (promotions D-06) (docs/milestones/plans/COMM-M1-plan.md)
scenarios:
  - id: SC-CAP-COM-CAT-015-01
    given: A promotion already owns EARLY25
    when: Create its spaced case variant on another promotion
    then: Workspace-wide normalized identity prevents the duplicate
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses
      a code another promotion of the tenant has, 409 code_taken
  - id: SC-CAP-COM-CAT-015-02
    given: A generated code batch has completed
    when: Export its codes
    then: The response is a downloadable CSV with a header and the batch’s code rows
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotion-code-issuances.e2e.spec.js#exports
      the codes as text/csv: a header and 25 lines
depends_on:
  - CAP-COM-CAT-014
related:
  - CAP-COM-CAT-016
  - CAP-COM-CAT-017
journeys:
  - JNY-COM-CAT-004
open_questions:
  - OQ-COM-CAT-004
evidence:
  - >-
    Pinned service inputs: modules/promotions/aws/api/promotion-codes.routes.ts,
    promotion-code-issuances.routes.ts and schemas;
    services/persistence/promotion-codes.ts, promotion-code-issuances.ts;
    services/issuance/**; services/pricing/code-charset.ts;
    db/promotion-codes.table.ts, promotion-code-restrictions.table.ts,
    promotion-code-issuances.table.ts; aws/logic (the issuance worker); tests
    (codes e2e, issuances e2e, persistence, charset, normalisation, template,
    job, csv)
  - >-
    Pinned console inputs:
    components/commerce/promotions/{code-editor,code-page,codes-tab,all-codes-list,generate-codes-dialog,generate-codes-form,issuances-tab}.tsx,
    lib/commerce/promotions/** and tests, e2e/mock/promotions.mock.spec.ts
controls: [TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-AZ-005, TC-AZ-006, TC-TEN-001, TC-TEN-002, TC-TEN-003, TC-CRY-001, TC-CRY-004, TC-DAT-001, TC-DAT-002, TC-DAT-003, TC-DAT-004, TC-DAT-005, TC-LOG-001, TC-LOG-002, TC-LOG-003, TC-LOG-004, TC-LOG-005, TC-PI-001, TC-PI-002, TC-PI-003, TC-PI-005, TC-PRV-001, TC-PRV-002]
exercised_by: [FAS-COM-CAT-04-01, FAS-COM-CAT-04-02, FAS-COM-CAT-04-03, FAS-COM-CAT-04-04, FAS-COM-CAT-04-05]
suites: [TST-COM-CAT-091, TST-COM-CAT-092, TST-COM-CAT-095, TST-COM-CAT-096, TST-COM-CAT-106, TST-COM-CAT-118, TST-COM-CAT-119, TST-COM-CAT-120, TST-COM-CAT-125, TST-COM-CAT-126, TST-COM-CAT-132]
---

# Issue promotion codes with controlled access

## Purpose

Coordinators distribute code claims with identity, qualification and usage controls.

## How It Works

Staff add individual codes to a code-claimed promotion or request a generated batch. Restrictions and eligibility govern permitted claims independently of a code’s listed owner. Saving checks normalized identity, dates and capacity. Generated batches run in the background and remain available for progress inspection and export. Later actions disable, enable or revoke codes with history retained. An interrupted batch resumes its remaining work, and a terminal failure preserves the codes already made. The product should confirm character policy and mixed personal/shared cancellation defaults.

## Relationships

The prepare and control a promotion’s lifecycle capability supplies related configuration or assessment used in this work.

The choose the best permitted promotional result capability supplies related configuration or assessment used in this work.

The retain and review promotion usage capability supplies related configuration or assessment used in this work.
