---
id: ATS-COM-CAT-015
scope: CAP-COM-CAT-015
product_area: commerce
module: catalog
revision: 2
status: proposed
source_sha:
  services-commerce: c8b66a98e2490617aec38ba5c8165dd9bd0a8f9b
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
scenarios:
  - id: ATS-COM-CAT-015-01
    title: Normalized codes cannot duplicate another promotion’s code
    level: api
    proves:
      - BR-CAP-COM-CAT-015-01
      - INV-DOM-COM-CAT-005-06
    preconditions: A promotion already owns EARLY25
    steps: Create a spaced case variant and the same code under another promotion
    expected: Both attempts report occupied identity while display spelling remains separately retained
    negative: true
    data_needs: none
    bound_to:
      - TST-COM-CAT-092
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses “EARLY25”
      beside “early 25”, 409 code_taken
    category: invariant
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses “EARLY25”
        beside “early 25”, 409 code_taken
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses a code
        another promotion of the tenant has, 409 code_taken
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#keeps the code as
        typed for display and stores it normalised (J12)
    step_evidence:
      - do: Create a spaced case variant and the same code under another promotion
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses
          “EARLY25” beside “early 25”, 409 code_taken
    assertions:
      - expect: Both attempts report occupied identity while display spelling remains separately retained
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses
          “EARLY25” beside “early 25”, 409 code_taken
  - id: ATS-COM-CAT-015-02
    title: Automatic promotions refuse code issuance
    level: api
    proves:
      - BR-CAP-COM-CAT-015-02
    preconditions: An automatic promotion exists
    steps: Create a code and request a generated batch
    expected: Both operations fail
    negative: true
    data_needs: none
    bound_to:
      - TST-COM-CAT-096
      - TST-COM-CAT-091
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses a code, 409
      automatic_promotion_has_no_codes
    category: validation
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses a code, 409
        automatic_promotion_has_no_codes
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-code-issuances.e2e.spec.js#refuses
        an automatic promotion 409
    step_evidence:
      - do: Create a code and request a generated batch
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses a code, 409
          automatic_promotion_has_no_codes
    assertions:
      - expect: Both operations fail
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses a code, 409
          automatic_promotion_has_no_codes
  - id: ATS-COM-CAT-015-03
    title: Code intervals cannot exceed promotion dates
    level: api
    proves:
      - BR-CAP-COM-CAT-015-03
      - INV-DOM-COM-CAT-005-07
    preconditions: The promotion operates during March
    steps: Start a code in February and end one in April
    expected: Both intervals are refused
    negative: true
    data_needs: none
    bound_to:
      - TST-COM-CAT-092
      - TST-COM-CAT-132
    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
    category: boundary
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses a window
        ending after the promotion’s, 400 window_exceeds_promotion
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses a window
        starting before the promotion’s, 400 window_exceeds_promotion
      - >-
        services-commerce:modules/promotions/tests/unit/pricing/validation.spec.js#never widens it:
        a code that ends later does not outlive its promotion
    step_evidence:
      - do: Start a code in February and end one in April
        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
    assertions:
      - expect: Both intervals are refused
        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: ATS-COM-CAT-015-04
    title: Code normalization precedes character validation
    level: unit
    proves:
      - BR-CAP-COM-CAT-015-04
      - BR-CAP-COM-CAT-015-05
    preconditions: Creation and entry accept candidate code text
    steps: 'Try punctuation, sharp-s, emoji, full-width letters and sixty-five characters'
    expected: Unsupported characters and excessive length fail; full-width letters normalize and succeed
    negative: true
    data_needs: none
    bound_to:
      - TST-COM-CAT-125
      - TST-COM-CAT-126
      - TST-COM-CAT-092
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/unit/pricing/code-charset.spec.js#refuses a slash,
      a percent sign, ß, an accent, punctuation, NUL, emoji and full-width punctuation (D-REC-12)
    category: validation
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/unit/pricing/code-charset.spec.js#refuses a
        slash, a percent sign, ß, an accent, punctuation, NUL, emoji and full-width punctuation
        (D-REC-12)
      - >-
        services-commerce:modules/promotions/tests/unit/pricing/code-charset.spec.js#accepts 64
        characters and refuses 65, spaces not counted
      - >-
        services-commerce:modules/promotions/tests/unit/pricing/code-normalization.spec.js#resolves
        the full-width ＥＡＲＬＹ２５ to EARLY25 (NFKC)
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#creates a
        full-width code as its normalised form, kept as typed for display, and refuses SPRING26
        beside it (D-REC-12)
    step_evidence:
      - do: 'Try punctuation, sharp-s, emoji, full-width letters and sixty-five characters'
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/unit/pricing/code-charset.spec.js#refuses a
          slash, a percent sign, ß, an accent, punctuation, NUL, emoji and full-width punctuation
          (D-REC-12)
    assertions:
      - expect: Unsupported characters and excessive length fail; full-width letters normalize and succeed
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/unit/pricing/code-charset.spec.js#refuses a
          slash, a percent sign, ß, an accent, punctuation, NUL, emoji and full-width punctuation
          (D-REC-12)
  - id: ATS-COM-CAT-015-05
    title: Single-use configuration rejects conflicting capacity
    level: api
    proves:
      - BR-CAP-COM-CAT-015-06
    preconditions: Code creation selects single use
    steps: Create a default single-use code and one requesting five uses
    expected: The default permits one use and the conflicting request fails
    negative: true
    data_needs: none
    bound_to:
      - TST-COM-CAT-092
      - TST-COM-CAT-120
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#makes a single-use
      code one use in all
    category: validation
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#makes a
        single-use code one use in all
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses a
        single-use code with another limit, 400
      - >-
        services-commerce:modules/promotions/tests/unit/issuance/issuance-request-csv.spec.js#refuses
        a single-use code with max_uses 5
    step_evidence:
      - do: Create a default single-use code and one requesting five uses
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#makes a
          single-use code one use in all
    assertions:
      - expect: The default permits one use and the conflicting request fails
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#makes a
          single-use code one use in all
  - id: ATS-COM-CAT-015-06
    title: Purchaser restrictions act independently of ownership
    level: unit
    proves:
      - BR-CAP-COM-CAT-015-07
    preconditions: 'Codes carry contact, organization and owner fixtures'
    steps: Assess subjects inside and outside restriction lists
    expected: >-
      Listed subjects qualify, unlisted subjects receive appropriate refusals and ownership adds no
      restriction
    negative: true
    data_needs: none
    bound_to:
      - TST-COM-CAT-132
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/unit/pricing/validation.spec.js#admits a purchaser
      among a code’s contact restrictions
    category: rule
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/unit/pricing/validation.spec.js#admits a
        purchaser among a code’s contact restrictions
      - >-
        services-commerce:modules/promotions/tests/unit/pricing/validation.spec.js#refuses an
        organization outside a code’s organization-only restrictions with the organization message
      - >-
        services-commerce:modules/promotions/tests/unit/pricing/validation.spec.js#does not restrict
        by owner
    step_evidence:
      - do: Assess subjects inside and outside restriction lists
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/unit/pricing/validation.spec.js#admits a
          purchaser among a code’s contact restrictions
    assertions:
      - expect: >-
          Listed subjects qualify, unlisted subjects receive appropriate refusals and ownership adds
          no restriction
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/unit/pricing/validation.spec.js#admits a
          purchaser among a code’s contact restrictions
  - id: ATS-COM-CAT-015-07
    title: Recorded usage protects the code’s lower limit
    level: api
    proves:
      - BR-CAP-COM-CAT-015-08
      - INV-DOM-COM-CAT-005-08
    preconditions: The code has already been used three times
    steps: Reduce its limit to two
    expected: The change fails
    negative: true
    data_needs: Planted redemptions
    bound_to:
      - TST-COM-CAT-092
    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
    category: invariant
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses a limit
        below the uses made, 409 limit_below_uses
    step_evidence:
      - do: Reduce its limit to two
        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
    assertions:
      - expect: The change fails
        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: ATS-COM-CAT-015-08
    title: Revocation ends the code lifecycle
    level: api
    proves:
      - BR-CAP-COM-CAT-015-09
      - INV-DOM-COM-CAT-005-09
    preconditions: The code starts active
    steps: 'Disable, enable and revoke it, then request enablement'
    expected: Supported transitions are recorded and revival fails
    negative: true
    data_needs: none
    bound_to:
      - TST-COM-CAT-092
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#disables an active
      code, enables a disabled one and revokes it
    category: transition
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#disables an
        active code, enables a disabled one and revokes it
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses each
        operation from a state it does not move, 409 invalid_transition
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#keeps a revoked
        code revoked: no operation moves it again
    step_evidence:
      - do: 'Disable, enable and revoke it, then request enablement'
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#disables an
          active code, enables a disabled one and revokes it
    assertions:
      - expect: Supported transitions are recorded and revival fails
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#disables an
          active code, enables a disabled one and revokes it
  - id: ATS-COM-CAT-015-09
    title: Generation validates template capacity and quantity
    level: unit
    proves:
      - BR-CAP-COM-CAT-015-10
    preconditions: Issuance requests can vary templates and counts
    steps: 'Use too few wildcards, invalid spacing, insufficient space and out-of-range counts'
    expected: Each invalid request returns a readable refusal
    negative: true
    data_needs: none
    bound_to:
      - TST-COM-CAT-118
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/unit/issuance/code-template.spec.js#refuses fewer
      than 6 wildcards in plain words
    category: boundary
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/unit/issuance/code-template.spec.js#refuses fewer
        than 6 wildcards in plain words
      - >-
        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
      - >-
        services-commerce:modules/promotions/tests/unit/issuance/code-template.spec.js#accepts
        quantities 1 and 5000 and refuses 0, 5001, 1.5 and text
      - >-
        services-commerce:modules/promotions/tests/unit/issuance/code-template.spec.js#has 8 digits,
        23 letters and 31 either, without 0, 1, I, L and O
    step_evidence:
      - do: 'Use too few wildcards, invalid spacing, insufficient space and out-of-range counts'
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/unit/issuance/code-template.spec.js#refuses
          fewer than 6 wildcards in plain words
    assertions:
      - expect: Each invalid request returns a readable refusal
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/unit/issuance/code-template.spec.js#refuses
          fewer than 6 wildcards in plain words
  - id: ATS-COM-CAT-015-10
    title: The first code establishes cancellation-release defaults
    level: api
    proves:
      - BR-CAP-COM-CAT-015-11
    preconditions: Two new promotions have no codes
    steps: Create a personal single-use first code and a shared first code
    expected: The personal case enables and records release; the shared case leaves release disabled
    negative: false
    data_needs: none
    bound_to:
      - TST-COM-CAT-106
    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
    category: rule
    binding_evidence:
      - >-
        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
      - >-
        services-commerce:modules/promotions/tests/integration/services/persistence/promotion-codes.integration.spec.js#leaves
        it off when the first code is shared, a personal one with several uses, or an unrestricted
        single-use one
    step_evidence:
      - do: Create a personal single-use first code and a shared first code
        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
    assertions:
      - expect: The personal case enables and records release; the shared case leaves release disabled
        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
  - id: ATS-COM-CAT-015-11
    title: Issuance resumes missing work and retains failed progress
    level: unit
    proves:
      - BR-CAP-COM-CAT-015-12
      - AS-JNY-COM-CAT-004-04
      - INV-DOM-COM-CAT-005-14
    preconditions: A batch requests twelve hundred codes
    steps: 'Run, interrupt and resume generation, repeat the request and force persistent conflicts'
    expected: >-
      Generation completes in chunks, resumes missing codes, reuses the batch and retains partial
      work on failure
    negative: false
    data_needs: none
    bound_to:
      - TST-COM-CAT-119
      - TST-COM-CAT-091
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/unit/issuance/issuance-job.spec.js#makes 1,200
      codes in chunks of 500, 500 and 200 and completes
    category: failure
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/unit/issuance/issuance-job.spec.js#makes 1,200
        codes in chunks of 500, 500 and 200 and completes
      - >-
        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
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-code-issuances.e2e.spec.js#answers
        the same batch for a repeat under the same key and makes no second batch
      - >-
        services-commerce:modules/promotions/tests/unit/issuance/issuance-job.spec.js#fails with a
        plain message, keeping the codes made, when conflicts never stop
    step_evidence:
      - do: 'Run, interrupt and resume generation, repeat the request and force persistent conflicts'
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/unit/issuance/issuance-job.spec.js#makes 1,200
          codes in chunks of 500, 500 and 200 and completes
    assertions:
      - expect: >-
          Generation completes in chunks, resumes missing codes, reuses the batch and retains
          partial work on failure
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/unit/issuance/issuance-job.spec.js#makes 1,200
          codes in chunks of 500, 500 and 200 and completes
  - id: ATS-COM-CAT-015-12
    title: Exported cells cannot become spreadsheet formulas
    level: unit
    proves:
      - BR-CAP-COM-CAT-015-13
    preconditions: Code or note values begin with formula characters
    steps: Render the CSV export
    expected: Formula-like cells gain a protective leading quote
    negative: false
    data_needs: none
    bound_to:
      - TST-COM-CAT-120
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/unit/issuance/issuance-request-csv.spec.js#prefixes
      cells starting with = + - @ with a single quote
    category: boundary
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/unit/issuance/issuance-request-csv.spec.js#prefixes
        cells starting with = + - @ with a single quote
      - >-
        services-commerce:modules/promotions/tests/unit/issuance/issuance-request-csv.spec.js#quotes
        cells with commas, quotes and line breaks
    step_evidence:
      - do: Render the CSV export
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/unit/issuance/issuance-request-csv.spec.js#prefixes
          cells starting with = + - @ with a single quote
    assertions:
      - expect: Formula-like cells gain a protective leading quote
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/unit/issuance/issuance-request-csv.spec.js#prefixes
          cells starting with = + - @ with a single quote
  - id: ATS-COM-CAT-015-13
    title: Code reads cannot authorize creation
    level: api
    proves:
      - BR-CAP-COM-CAT-015-14
    preconditions: The principal has promotion-read permission
    steps: Inspect codes and attempt to create one
    expected: Reading succeeds and creation fails permission checks
    negative: true
    data_needs: Read-exclusively persona
    bound_to:
      - TST-COM-CAT-095
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotions-security.e2e.spec.js#reads every
      read route
    category: permission
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions-security.e2e.spec.js#reads every
        read route
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions-security.e2e.spec.js#is refused
        every write, 403 permission_missing
    step_evidence:
      - do: Inspect codes and attempt to create one
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotions-security.e2e.spec.js#reads every
          read route
    assertions:
      - expect: Reading succeeds and creation fails permission checks
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotions-security.e2e.spec.js#reads every
          read route
  - id: ATS-COM-CAT-015-14
    title: Code creation and generation remain discoverable in the console
    level: browser
    proves:
      - AS-JNY-COM-CAT-004-05
    preconditions: Mock promotion data supports browser editing
    steps: 'Add a limited annotated code, generate a batch and inspect All Codes'
    expected: >-
      Creation opens the code page, the batch appears in issuance history and All Codes locates the
      code
    negative: false
    data_needs: Console mock
    bound_to:
      - TST-COM-CAT-008
    provenance: test
    source: >-
      react-console:e2e/mock/promotions.mock.spec.ts#a code is added in the code editor with a limit
      and a note, and lands on its page
    category: rule
    binding_evidence:
      - >-
        react-console:e2e/mock/promotions.mock.spec.ts#a code is added in the code editor with a
        limit and a note, and lands on its page
      - >-
        react-console:e2e/mock/promotions.mock.spec.ts#Generate Many starts a job that the Issuances
        tab lists
      - >-
        react-console:e2e/mock/promotions.mock.spec.ts#All Codes lists the codes of every promotion
        and narrows them by search
    step_evidence:
      - do: 'Add a limited annotated code, generate a batch and inspect All Codes'
        provenance: test
        source: >-
          react-console:e2e/mock/promotions.mock.spec.ts#a code is added in the code editor with a
          limit and a note, and lands on its page
    assertions:
      - expect: >-
          Creation opens the code page, the batch appears in issuance history and All Codes locates
          the code
        provenance: test
        source: >-
          react-console:e2e/mock/promotions.mock.spec.ts#a code is added in the code editor with a
          limit and a note, and lands on its page
  - id: ATS-COM-CAT-015-15
    title: Code state changes promotion launch readiness
    category: interaction
    level: api
    proves:
      - BR-CAP-COM-CAT-014-10
      - BR-CAP-COM-CAT-015-09
    preconditions: A coded draft has one active code.
    steps: >-
      Disable its code and attempt launch; enable the code and launch; revoke it and assess a new
      claim.
    expected: >-
      Launch without an active code refuses; launch after enablement succeeds and the revoked claim
      refuses.
    negative: false
    data_needs: Run-owned workspace and records; fixed clock where dates are exercised.
    bound_to: []
    provenance: proposed
    source:
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses to activate a
        coded promotion with no active code, 409 no_active_code
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#keeps a revoked
        code revoked: no operation moves it again
    exercises:
      - CAP-COM-CAT-014
      - CAP-COM-CAT-015
    workflows:
      - WF-COM-CAT-022
    step_evidence:
      - do: >-
          Disable its code and attempt launch; enable the code and launch; revoke it and assess a
          new claim.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses to
            activate a coded promotion with no active code, 409 no_active_code
          - >-
            services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#keeps a
            revoked code revoked: no operation moves it again
    assertions:
      - expect: >-
          Launch without an active code refuses; launch after enablement succeeds and the revoked
          claim refuses.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses to
            activate a coded promotion with no active code, 409 no_active_code
          - >-
            services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#keeps a
            revoked code revoked: no operation moves it again
  - id: ATS-COM-CAT-015-16
    title: Code restrictions change the checkout result
    category: interaction
    level: api
    proves:
      - BR-CAP-COM-CAT-015-07
      - BR-CAP-COM-CAT-016-11
      - BR-CAP-COM-CAT-016-13
    preconditions: A valid code restricts a purchaser and has a stated eligibility rule.
    steps: 'Assess a listed subject, an unrelated subject and an anonymous subject.'
    expected: >-
      Only the permitted qualifying subject receives the benefit; other results retain safe refusal
      messages.
    negative: false
    data_needs: Run-owned workspace and records; fixed clock where dates are exercised.
    bound_to: []
    provenance: proposed
    source:
      - >-
        services-commerce:modules/promotions/tests/unit/pricing/validation.spec.js#does not restrict
        by owner
      - >-
        services-commerce:modules/promotions/tests/unit/pricing/validation.spec.js#answers the first
        refusal in the order of 10.3: an inactive code before an exhausted one
      - >-
        services-commerce:modules/promotions/tests/unit/pricing/validation.spec.js#fails closed on
        an indeterminate verdict, for must_match and for must_not_match alike
    exercises:
      - CAP-COM-CAT-015
      - CAP-COM-CAT-016
    workflows: []
    step_evidence:
      - do: 'Assess a listed subject, an unrelated subject and an anonymous subject.'
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/tests/unit/pricing/validation.spec.js#does not
            restrict by owner
          - >-
            services-commerce:modules/promotions/tests/unit/pricing/validation.spec.js#answers the
            first refusal in the order of 10.3: an inactive code before an exhausted one
          - >-
            services-commerce:modules/promotions/tests/unit/pricing/validation.spec.js#fails closed
            on an indeterminate verdict, for must_match and for must_not_match alike
    assertions:
      - expect: >-
          Only the permitted qualifying subject receives the benefit; other results retain safe
          refusal messages.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/tests/unit/pricing/validation.spec.js#does not
            restrict by owner
          - >-
            services-commerce:modules/promotions/tests/unit/pricing/validation.spec.js#answers the
            first refusal in the order of 10.3: an inactive code before an exhausted one
          - >-
            services-commerce:modules/promotions/tests/unit/pricing/validation.spec.js#fails closed
            on an indeterminate verdict, for must_match and for must_not_match alike
  - id: ATS-COM-CAT-015-17
    title: Releasing usage restores capacity without erasing terms
    category: interaction
    level: api
    proves:
      - BR-CAP-COM-CAT-015-08
      - BR-CAP-COM-CAT-015-11
      - BR-CAP-COM-CAT-017-04
      - BR-CAP-COM-CAT-017-05
    preconditions: A personal single-use first code has one applied redemption.
    steps: >-
      Attempt to lower its limit; release with override and a reason; read counts and frozen
      allocations.
    expected: >-
      The invalid limit change refuses; release restores capacity while retaining the discount
      evidence.
    negative: false
    data_needs: Run-owned workspace and records; fixed clock where dates are exercised.
    bound_to: []
    provenance: proposed
    source:
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses a limit
        below the uses made, 409 limit_below_uses
      - >-
        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
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-redemptions.e2e.spec.js#refuses a
        release without a reason, 400
      - >-
        services-commerce:docs/milestones/plans/COMM-M1-plan.md#D13. Restore a personal code when
        its order is cancelled?
    exercises:
      - CAP-COM-CAT-015
      - CAP-COM-CAT-017
    workflows: []
    step_evidence:
      - do: >-
          Attempt to lower its limit; release with override and a reason; read counts and frozen
          allocations.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses a
            limit below the uses made, 409 limit_below_uses
          - >-
            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
          - >-
            services-commerce:modules/promotions/tests/e2e/promotion-redemptions.e2e.spec.js#refuses
            a release without a reason, 400
          - >-
            services-commerce:docs/milestones/plans/COMM-M1-plan.md#D13. Restore a personal code
            when its order is cancelled?
    assertions:
      - expect: >-
          The invalid limit change refuses; release restores capacity while retaining the discount
          evidence.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#refuses a
            limit below the uses made, 409 limit_below_uses
          - >-
            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
          - >-
            services-commerce:modules/promotions/tests/e2e/promotion-redemptions.e2e.spec.js#refuses
            a release without a reason, 400
          - >-
            services-commerce:docs/milestones/plans/COMM-M1-plan.md#D13. Restore a personal code
            when its order is cancelled?
  - id: ATS-COM-CAT-015-18
    title: Promotion code lifecycle is exercised across every declared move
    category: transition
    level: api
    proves:
      - BR-CAP-COM-CAT-015-09
      - BR-CAP-COM-CAT-015-12
    preconditions: >-
      Independent run-owned records start in each declared origin state; guarded variants omit one
      required condition at a time.
    steps: >-
      For each transition case, establish the origin, apply its trigger with the guard satisfied,
      read the resulting state and side effects, then repeat on a separate record with a required
      guard absent.
    expected: >-
      Every allowed case reaches its stated destination and retains its declared side effects;
      failed guards preserve the originating record except for the separately documented partial
      offer action.
    negative: true
    data_needs: Run-owned workspace and records; fixed clock where dates are exercised.
    bound_to: []
    provenance: proposed
    source:
      - >-
        rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-008-promotion-code-lifecycle.md#STATE-COM-CAT-008
      - >-
        services-commerce:modules/promotions/services/persistence/promotion-codes.ts:PROMOTION_CODE_TRANSITIONS
      - >-
        services-commerce:modules/promotions/services/persistence/promotion-codes.ts:PROMOTION_CODE_TRANSITIONS
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#keeps a revoked
        code revoked: no operation moves it again
      - >-
        services-commerce:modules/promotions/services/persistence/promotion-codes.ts:PROMOTION_CODE_TRANSITIONS
    state_model: STATE-COM-CAT-008
    transition_cases:
      - from: active
        to: disabled
        trigger: Staff suspend code claims.
        guard: The caller must hold campaign management authority.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/services/persistence/promotion-codes.ts:PROMOTION_CODE_TRANSITIONS
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-008-promotion-code-lifecycle.md#STATE-COM-CAT-008
      - from: disabled
        to: active
        trigger: Staff restore suspended code claims.
        guard: The caller must hold campaign management authority.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/services/persistence/promotion-codes.ts:PROMOTION_CODE_TRANSITIONS
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-008-promotion-code-lifecycle.md#STATE-COM-CAT-008
      - from: active
        to: revoked
        trigger: Staff permanently revoke the code.
        guard: The caller must hold campaign management authority.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#keeps a
            revoked code revoked: no operation moves it again
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-008-promotion-code-lifecycle.md#STATE-COM-CAT-008
      - from: disabled
        to: revoked
        trigger: Staff permanently revoke the code.
        guard: The caller must hold campaign management authority.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/services/persistence/promotion-codes.ts:PROMOTION_CODE_TRANSITIONS
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-008-promotion-code-lifecycle.md#STATE-COM-CAT-008
    step_evidence:
      - do: >-
          For each transition case, establish the origin, apply its trigger with the guard
          satisfied, read the resulting state and side effects, then repeat on a separate record
          with a required guard absent.
        provenance: proposed
        source:
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-008-promotion-code-lifecycle.md#STATE-COM-CAT-008
          - >-
            services-commerce:modules/promotions/services/persistence/promotion-codes.ts:PROMOTION_CODE_TRANSITIONS
          - >-
            services-commerce:modules/promotions/services/persistence/promotion-codes.ts:PROMOTION_CODE_TRANSITIONS
          - >-
            services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#keeps a
            revoked code revoked: no operation moves it again
          - >-
            services-commerce:modules/promotions/services/persistence/promotion-codes.ts:PROMOTION_CODE_TRANSITIONS
    assertions:
      - expect: >-
          Every allowed case reaches its stated destination and retains its declared side effects;
          failed guards preserve the originating record except for the separately documented partial
          offer action.
        provenance: proposed
        source:
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-008-promotion-code-lifecycle.md#STATE-COM-CAT-008
          - >-
            services-commerce:modules/promotions/services/persistence/promotion-codes.ts:PROMOTION_CODE_TRANSITIONS
          - >-
            services-commerce:modules/promotions/services/persistence/promotion-codes.ts:PROMOTION_CODE_TRANSITIONS
          - >-
            services-commerce:modules/promotions/tests/e2e/promotion-codes.e2e.spec.js#keeps a
            revoked code revoked: no operation moves it again
          - >-
            services-commerce:modules/promotions/services/persistence/promotion-codes.ts:PROMOTION_CODE_TRANSITIONS
  - id: ATS-COM-CAT-015-19
    title: Code issuance batch is exercised across every declared move
    category: transition
    level: api
    proves:
      - BR-CAP-COM-CAT-015-09
      - BR-CAP-COM-CAT-015-12
    preconditions: >-
      Independent run-owned records start in each declared origin state; guarded variants omit one
      required condition at a time.
    steps: >-
      For each transition case, establish the origin, apply its trigger with the guard satisfied,
      read the resulting state and side effects, then repeat on a separate record with a required
      guard absent.
    expected: >-
      Every allowed case reaches its stated destination and retains its declared side effects;
      failed guards preserve the originating record except for the separately documented partial
      offer action.
    negative: true
    data_needs: Run-owned workspace and records; fixed clock where dates are exercised.
    bound_to: []
    provenance: proposed
    source:
      - >-
        rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-009-code-issuance-batch.md#STATE-COM-CAT-009
      - 'services-commerce:modules/promotions/services/persistence/promotion-code-issuances.ts'
      - >-
        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
      - 'services-commerce:modules/promotions/services/issuance/issuance-job.ts'
      - 'services-commerce:modules/promotions/services/issuance/issuance-workflow.ts'
      - 'services-commerce:modules/promotions/services/issuance/issuance-workflow.ts:readCodeIssuance'
    state_model: STATE-COM-CAT-009
    transition_cases:
      - from: requested
        to: running
        trigger: The invoked worker begins generation.
        guard: The batch must remain requested or running.
        provenance: proposed
        source:
          - 'services-commerce:modules/promotions/services/persistence/promotion-code-issuances.ts'
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-009-code-issuance-batch.md#STATE-COM-CAT-009
      - from: running
        to: completed
        trigger: The final required codes commit.
        guard: The saved code count must reach the requested quantity.
        provenance: proposed
        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
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-009-code-issuance-batch.md#STATE-COM-CAT-009
      - from: running
        to: failed
        trigger: Generation encounters an error or reaches its conflict retry bound.
        guard: No additional condition applies.
        provenance: proposed
        source:
          - 'services-commerce:modules/promotions/services/issuance/issuance-job.ts'
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-009-code-issuance-batch.md#STATE-COM-CAT-009
      - from: requested
        to: failed
        trigger: Worker invocation fails.
        guard: No additional condition applies.
        provenance: proposed
        source:
          - 'services-commerce:modules/promotions/services/issuance/issuance-workflow.ts'
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-009-code-issuance-batch.md#STATE-COM-CAT-009
      - from: running
        to: running
        trigger: 'A retry, repeated request or stale-batch read invokes generation again.'
        guard: The batch must remain requested or running.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/services/issuance/issuance-workflow.ts:readCodeIssuance
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-009-code-issuance-batch.md#STATE-COM-CAT-009
    step_evidence:
      - do: >-
          For each transition case, establish the origin, apply its trigger with the guard
          satisfied, read the resulting state and side effects, then repeat on a separate record
          with a required guard absent.
        provenance: proposed
        source:
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-009-code-issuance-batch.md#STATE-COM-CAT-009
          - 'services-commerce:modules/promotions/services/persistence/promotion-code-issuances.ts'
          - >-
            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
          - 'services-commerce:modules/promotions/services/issuance/issuance-job.ts'
          - 'services-commerce:modules/promotions/services/issuance/issuance-workflow.ts'
          - >-
            services-commerce:modules/promotions/services/issuance/issuance-workflow.ts:readCodeIssuance
    assertions:
      - expect: >-
          Every allowed case reaches its stated destination and retains its declared side effects;
          failed guards preserve the originating record except for the separately documented partial
          offer action.
        provenance: proposed
        source:
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-009-code-issuance-batch.md#STATE-COM-CAT-009
          - 'services-commerce:modules/promotions/services/persistence/promotion-code-issuances.ts'
          - >-
            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
          - 'services-commerce:modules/promotions/services/issuance/issuance-job.ts'
          - 'services-commerce:modules/promotions/services/issuance/issuance-workflow.ts'
          - >-
            services-commerce:modules/promotions/services/issuance/issuance-workflow.ts:readCodeIssuance
evidence:
  - 'Pinned service inputs: test sources in scenario bindings'
  - 'Pinned console inputs: console sources in scenario bindings'
  - >-
    rhythm-v2-docs:specs/commerce/catalog/planning/capabilities/CAP-COM-CAT-015-give-out-codes-and-say-who-may-use-them.md#CAP-COM-CAT-015
  - >-
    rhythm-v2-docs:specs/commerce/catalog/design/workflows/WF-COM-CAT-013-generate-a-batch-of-promotion-codes.md#WF-COM-CAT-013
  - >-
    rhythm-v2-docs:specs/commerce/catalog/design/workflows/WF-COM-CAT-022-save-a-promotion-with-its-first-code.md#WF-COM-CAT-022
product_intent: proposed
coverage:
  rule: covered
  transition: covered
  validation: covered
  permission: covered
  edge: 'gap: FND-COM-CAT-069'
  failure: covered
  boundary: covered
  concurrency: 'gap: FND-COM-CAT-069'
  invariant: covered
  interaction: covered
coverage_notes:
  edge: >-
    No complete edge scenario is established by this back-fill; the finding records the missing
    proof.
  concurrency: Concurrent execution has no complete scenario; sequential retry is insufficient proof.
permission_coverage:
  - key: commerce.promotions.read
    coverage: 'gap: FND-COM-CAT-069'
    reason: >-
      Existing permission examples cover selected personas, not the independent allow and deny
      matrix for every named key.
    provenance: unknown
    source:
      - 'rhythm-v2-docs:specs/commerce/catalog/FINDINGS.md#FND-COM-CAT-069'
  - key: commerce.promotions.manage
    coverage: 'gap: FND-COM-CAT-069'
    reason: >-
      Existing permission examples cover selected personas, not the independent allow and deny
      matrix for every named key.
    provenance: unknown
    source:
      - 'rhythm-v2-docs:specs/commerce/catalog/FINDINGS.md#FND-COM-CAT-069'
---

# Issue promotion codes with controlled access: Capability Test Specification

## Coverage

| Rule | Proof Scenario or Gap |
|---|---|
| `BR-CAP-COM-CAT-015-01` | `ATS-COM-CAT-015-01` |
| `BR-CAP-COM-CAT-015-02` | `ATS-COM-CAT-015-02` |
| `BR-CAP-COM-CAT-015-03` | `ATS-COM-CAT-015-03` |
| `BR-CAP-COM-CAT-015-04` | `ATS-COM-CAT-015-04` |
| `BR-CAP-COM-CAT-015-05` | `ATS-COM-CAT-015-04` |
| `BR-CAP-COM-CAT-015-06` | `ATS-COM-CAT-015-05` |
| `BR-CAP-COM-CAT-015-07` | `ATS-COM-CAT-015-06`, `ATS-COM-CAT-015-16` |
| `BR-CAP-COM-CAT-015-08` | `ATS-COM-CAT-015-07`, `ATS-COM-CAT-015-17` |
| `BR-CAP-COM-CAT-015-09` | `ATS-COM-CAT-015-08`, `ATS-COM-CAT-015-15`, `ATS-COM-CAT-015-18`, `ATS-COM-CAT-015-19` |
| `BR-CAP-COM-CAT-015-10` | `ATS-COM-CAT-015-09` |
| `BR-CAP-COM-CAT-015-11` | `ATS-COM-CAT-015-10`, `ATS-COM-CAT-015-17` |
| `BR-CAP-COM-CAT-015-12` | `ATS-COM-CAT-015-11`, `ATS-COM-CAT-015-18`, `ATS-COM-CAT-015-19` |
| `BR-CAP-COM-CAT-015-13` | `ATS-COM-CAT-015-12` |
| `BR-CAP-COM-CAT-015-14` | `ATS-COM-CAT-015-13` |

## Interactions With Related Capabilities

Code state changes promotion launch readiness. Code restrictions change the checkout result. Releasing usage restores capacity without erasing terms.

Coverage describes specified proof. A binding names the existing assertions that contribute to it; proposed designs remain unexecuted.
