---
id: ATS-COM-CAT-014
scope: CAP-COM-CAT-014
product_area: commerce
module: catalog
revision: 2
status: proposed
source_sha:
  services-commerce: c8b66a98e2490617aec38ba5c8165dd9bd0a8f9b
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
scenarios:
  - id: ATS-COM-CAT-014-01
    title: Promotion kind changes respect existing codes and lifecycle
    level: api
    proves:
      - BR-CAP-COM-CAT-014-01
      - INV-DOM-COM-CAT-005-01
    preconditions: A coded draft and active automatic promotion exist
    steps: 'Convert the coded draft, change the active kind and add a code to the automatic promotion'
    expected: Each prohibited operation fails
    negative: true
    data_needs: none
    bound_to:
      - TST-COM-CAT-096
    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
    category: validation
    binding_evidence:
      - >-
        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
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#makes a draft with no
        code automatic, and refuses the change once it is active, 409 kind_not_editable
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses a code, 409
        automatic_promotion_has_no_codes
    step_evidence:
      - do: 'Convert the coded draft, change the active kind and add a code to the automatic promotion'
        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
    assertions:
      - expect: Each prohibited operation fails
        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: ATS-COM-CAT-014-02
    title: Offer validation rejects incompatible or excessive parts
    level: unit
    proves:
      - BR-CAP-COM-CAT-014-02
      - BR-CAP-COM-CAT-014-03
    preconditions: A promotion creation request is available
    steps: >-
      Combine parts with an order offer, supply twenty-one parts, omit percentage parts, exceed one
      hundred percent and omit per-unit currency
    expected: Each invalid offer receives a validation refusal
    negative: true
    data_needs: none
    bound_to:
      - TST-COM-CAT-133
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/unit/promotions/promotion-parts-rules.spec.js#refuses
      parts together with a promotion-level kind
    category: boundary
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/unit/promotions/promotion-parts-rules.spec.js#refuses
        parts together with a promotion-level kind
      - >-
        services-commerce:modules/promotions/tests/unit/promotions/promotion-parts-rules.spec.js#refuses
        twenty-one parts, 400 promotion_invalid
      - >-
        services-commerce:modules/promotions/tests/unit/promotions/promotion-parts-rules.spec.js#refuses
        percent_off as the promotion’s own kind without parts
      - >-
        services-commerce:modules/promotions/tests/unit/promotions/promotion-parts-rules.spec.js#refuses
        a percentage above 100 %, naming the part
      - >-
        services-commerce:modules/promotions/tests/unit/promotions/promotion-parts-rules.spec.js#refuses
        a per-unit part without a currency
    step_evidence:
      - do: >-
          Combine parts with an order offer, supply twenty-one parts, omit percentage parts, exceed
          one hundred percent and omit per-unit currency
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/unit/promotions/promotion-parts-rules.spec.js#refuses
          parts together with a promotion-level kind
    assertions:
      - expect: Each invalid offer receives a validation refusal
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/unit/promotions/promotion-parts-rules.spec.js#refuses
          parts together with a promotion-level kind
  - id: ATS-COM-CAT-014-03
    title: Selling areas validate part targets
    level: api
    proves:
      - BR-CAP-COM-CAT-014-03
    preconditions: The merchandise provider is registered
    steps: Select an unknown area and a nonmerchandise product
    expected: Both part requests identify their invalid target
    negative: true
    data_needs: none
    bound_to:
      - TST-COM-CAT-096
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses an area nobody
      offers, 400 promotion_invalid naming the part
    category: validation
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses an area nobody
        offers, 400 promotion_invalid naming the part
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses a product that
        is not merchandise, 400 promotion_invalid naming the part and the product
    step_evidence:
      - do: Select an unknown area and a nonmerchandise product
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses an area
          nobody offers, 400 promotion_invalid naming the part
    assertions:
      - expect: Both part requests identify their invalid target
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses an area
          nobody offers, 400 promotion_invalid naming the part
  - id: ATS-COM-CAT-014-04
    title: Promotion validation returns all detected problems
    level: api
    proves:
      - BR-CAP-COM-CAT-014-04
    preconditions: 'A request contains an invalid cap, interval and currency'
    steps: Submit the request once
    expected: One refusal reports every detected issue
    negative: true
    data_needs: none
    bound_to:
      - TST-COM-CAT-096
      - TST-COM-CAT-128
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#names every problem of a
      sloppy promotion in one 400 promotion_invalid, the zero cap among them (Q07, journey 0203)
    category: validation
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#names every problem of
        a sloppy promotion in one 400 promotion_invalid, the zero cap among them (Q07, journey 0203)
      - >-
        services-commerce:modules/promotions/tests/unit/pricing/promotion-shape.spec.js#refuses a
        stated cap of zero, as the database does, and admits a cap of one and a minimum of zero
        (Q07)
    step_evidence:
      - do: Submit the request once
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#names every problem
          of a sloppy promotion in one 400 promotion_invalid, the zero cap among them (Q07, journey
          0203)
    assertions:
      - expect: One refusal reports every detected issue
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#names every problem
          of a sloppy promotion in one 400 promotion_invalid, the zero cap among them (Q07, journey
          0203)
  - id: ATS-COM-CAT-014-05
    title: Code and automatic eligibility have different homes
    level: api
    proves:
      - BR-CAP-COM-CAT-014-05
    preconditions: Both promotion kinds can be created
    steps: Set promotion eligibility on each and include legacy scope
    expected: >-
      Code-promotion eligibility fails, automatic eligibility succeeds and legacy scope has no
      effect
    negative: true
    data_needs: none
    bound_to:
      - TST-COM-CAT-096
      - TST-COM-CAT-133
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses a
      promotion-level eligibility rule on a code-claimed promotion, 400 in the code’s words
    category: validation
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses a
        promotion-level eligibility rule on a code-claimed promotion, 400 in the code’s words
      - >-
        services-commerce:modules/promotions/tests/unit/promotions/promotion-parts-rules.spec.js#admits
        the rule on an automatic promotion
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#accepts a scope of any
        members and ignores it: never an error, nothing stored (Revision 2)
    step_evidence:
      - do: Set promotion eligibility on each and include legacy scope
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses a
          promotion-level eligibility rule on a code-claimed promotion, 400 in the code’s words
    assertions:
      - expect: >-
          Code-promotion eligibility fails, automatic eligibility succeeds and legacy scope has no
          effect
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses a
          promotion-level eligibility rule on a code-claimed promotion, 400 in the code’s words
  - id: ATS-COM-CAT-014-06
    title: Audience combination defaults on and can be changed
    level: api
    proves:
      - BR-CAP-COM-CAT-014-06
    preconditions: A new promotion omits the combination setting
    steps: Create it and replace the setting
    expected: The default is enabled and the replacement is readable
    negative: false
    data_needs: none
    bound_to:
      - TST-COM-CAT-096
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#defaults "combine with
      audience prices" to yes when the body leaves it out (D12)
    category: rule
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#defaults "combine with
        audience prices" to yes when the body leaves it out (D12)
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#changes "combine with
        audience prices" and reads it back (D12)
    step_evidence:
      - do: Create it and replace the setting
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#defaults "combine
          with audience prices" to yes when the body leaves it out (D12)
    assertions:
      - expect: The default is enabled and the replacement is readable
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#defaults "combine
          with audience prices" to yes when the body leaves it out (D12)
  - id: ATS-COM-CAT-014-07
    title: Lifecycle transitions validate states and retain reasons
    level: api
    proves:
      - BR-CAP-COM-CAT-014-09
      - AS-JNY-COM-CAT-004-01
    preconditions: Fixtures cover each promotion state
    steps: Exercise valid transitions and unsupported moves
    expected: Valid moves appear in history and invalid moves identify their states
    negative: true
    data_needs: none
    bound_to:
      - TST-COM-CAT-096
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#pauses an active
      promotion, resumes a paused one, and activates a paused one
    category: transition
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#pauses an active
        promotion, resumes a paused one, and activates a paused one
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#ends an active
        promotion, and archives an ended one and a draft
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#names both states in
        the refusal
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#records every
        transition on the history, newest first, with the reason given
    step_evidence:
      - do: Exercise valid transitions and unsupported moves
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#pauses an active
          promotion, resumes a paused one, and activates a paused one
    assertions:
      - expect: Valid moves appear in history and invalid moves identify their states
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#pauses an active
          promotion, resumes a paused one, and activates a paused one
  - id: ATS-COM-CAT-014-08
    title: Activation requires operational configuration
    level: api
    proves:
      - BR-CAP-COM-CAT-014-10
    preconditions: 'Drafts lack active codes, parts or selected eligibility rules'
    steps: Attempt activation of each
    expected: Each refusal identifies its missing prerequisite
    negative: true
    data_needs: none
    bound_to:
      - TST-COM-CAT-096
    provenance: test
    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
    category: validation
    binding_evidence:
      - >-
        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/promotions.e2e.spec.js#refuses to activate a
        promotion whose parts were removed, 409 offer_required
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses activating an
        effect with no rule chosen yet, 409 eligibility_rule_required
    step_evidence:
      - do: Attempt activation of each
        provenance: test
        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
    assertions:
      - expect: Each refusal identifies its missing prerequisite
        provenance: test
        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
  - id: ATS-COM-CAT-014-09
    title: Launch authority is independent of preparation authority
    level: api
    proves:
      - BR-CAP-COM-CAT-014-11
      - AS-JNY-COM-CAT-004-02
    preconditions: Separate callers hold manage and activate permissions
    steps: 'Attempt preparation, pause, activation and resumption'
    expected: 'Management cannot launch or resume, and activation authority alone cannot pause'
    negative: true
    data_needs: Personas by key
    bound_to:
      - TST-COM-CAT-095
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotions-security.e2e.spec.js#cannot activate
      a draft, 403 permission_missing
    category: permission
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions-security.e2e.spec.js#cannot
        activate a draft, 403 permission_missing
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions-security.e2e.spec.js#cannot resume
        a paused promotion, 403 permission_missing
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions-security.e2e.spec.js#activates a
        draft and resumes a paused promotion
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions-security.e2e.spec.js#cannot pause
        a promotion, 403 permission_missing
    step_evidence:
      - do: 'Attempt preparation, pause, activation and resumption'
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotions-security.e2e.spec.js#cannot
          activate a draft, 403 permission_missing
    assertions:
      - expect: 'Management cannot launch or resume, and activation authority alone cannot pause'
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotions-security.e2e.spec.js#cannot
          activate a draft, 403 permission_missing
  - id: ATS-COM-CAT-014-10
    title: Console actions reflect the separate launch grant
    level: unit
    proves:
      - BR-CAP-COM-CAT-014-11
    preconditions: 'Read-only, coordinator and administrator fixtures exist'
    steps: Render promotion actions for each
    expected: Coordinators receive preparation and pause actions without Activate or Resume
    negative: false
    data_needs: none
    bound_to:
      - TST-COM-CAT-004
    provenance: test
    source: >-
      react-console:components/commerce/__tests__/permission-gating.test.tsx#lets a coordinator
      prepare and pause but not launch or resume (commerce.promotions.activate)
    category: permission
    binding_evidence:
      - >-
        react-console:components/commerce/__tests__/permission-gating.test.tsx#lets a coordinator
        prepare and pause but not launch or resume (commerce.promotions.activate)
      - >-
        react-console:components/commerce/__tests__/permission-gating.test.tsx#offers a read-only
        person no pencil and no lifecycle
    step_evidence:
      - do: Render promotion actions for each
        provenance: test
        source: >-
          react-console:components/commerce/__tests__/permission-gating.test.tsx#lets a coordinator
          prepare and pause but not launch or resume (commerce.promotions.activate)
    assertions:
      - expect: Coordinators receive preparation and pause actions without Activate or Resume
        provenance: test
        source: >-
          react-console:components/commerce/__tests__/permission-gating.test.tsx#lets a coordinator
          prepare and pause but not launch or resume (commerce.promotions.activate)
  - id: ATS-COM-CAT-014-11
    title: Used promotions retain economic terms while allowing maintenance
    level: api
    proves:
      - BR-CAP-COM-CAT-014-12
      - INV-DOM-COM-CAT-005-05
    preconditions: A promotion already has redemptions
    steps: 'Change its benefit, extend dates, change limits and duplicate after ending'
    expected: Locked benefit changes fail; permitted maintenance saves and the duplicate is editable
    negative: true
    data_needs: Planted redemptions
    bound_to:
      - TST-COM-CAT-094
    provenance: test
    source: >-
      services-commerce:modules/promotions/tests/e2e/promotion-redemptions.e2e.spec.js#refuses the
      new benefit value, 409 promotion_locked
    category: invariant
    binding_evidence:
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-redemptions.e2e.spec.js#refuses the
        new benefit value, 409 promotion_locked
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-redemptions.e2e.spec.js#names the
        column and offers to end the promotion and duplicate it
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-redemptions.e2e.spec.js#extends the
        window
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-redemptions.e2e.spec.js#changes the
        benefit value of its duplicate, a draft never redeemed
    step_evidence:
      - do: 'Change its benefit, extend dates, change limits and duplicate after ending'
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotion-redemptions.e2e.spec.js#refuses
          the new benefit value, 409 promotion_locked
    assertions:
      - expect: Locked benefit changes fail; permitted maintenance saves and the duplicate is editable
        provenance: test
        source: >-
          services-commerce:modules/promotions/tests/e2e/promotion-redemptions.e2e.spec.js#refuses
          the new benefit value, 409 promotion_locked
  - id: ATS-COM-CAT-014-12
    title: Promotion view reflects a coordinator’s saved edit
    level: browser
    proves:
      - AS-JNY-COM-CAT-004-03
    preconditions: The browser uses mocked promotions
    steps: 'Create a category offer, view it and change its benefit to fifteen percent'
    expected: The view displays the revised offer
    negative: false
    data_needs: Console mock
    bound_to:
      - TST-COM-CAT-008
    provenance: test
    source: >-
      react-console:e2e/mock/promotions.mock.spec.ts#a new promotion is created for a category,
      shown on its view, and an edit to 15% comes back to the view
    category: rule
    binding_evidence:
      - >-
        react-console:e2e/mock/promotions.mock.spec.ts#a new promotion is created for a category,
        shown on its view, and an edit to 15% comes back to the view
    step_evidence:
      - do: 'Create a category offer, view it and change its benefit to fifteen percent'
        provenance: test
        source: >-
          react-console:e2e/mock/promotions.mock.spec.ts#a new promotion is created for a category,
          shown on its view, and an edit to 15% comes back to the view
    assertions:
      - expect: The view displays the revised offer
        provenance: test
        source: >-
          react-console:e2e/mock/promotions.mock.spec.ts#a new promotion is created for a category,
          shown on its view, and an edit to 15% comes back to the view
  - id: ATS-COM-CAT-014-13
    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-014-14
    title: Promotion lifecycle changes checkout candidates
    category: interaction
    level: api
    proves:
      - BR-CAP-COM-CAT-014-09
      - BR-CAP-COM-CAT-014-12
      - BR-CAP-COM-CAT-016-11
    preconditions: An active automatic promotion matches the cart.
    steps: 'Preview it, pause and preview again, resume and place; attempt to edit the economic benefit.'
    expected: 'Pause removes the benefit, resume restores it and first use prevents the benefit edit.'
    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 every
        operation from a state it does not move, 409 invalid_transition
      - >-
        services-commerce:modules/promotions/tests/e2e/promotion-redemptions.e2e.spec.js#names the
        column and offers to end the promotion and duplicate it
      - >-
        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
    exercises:
      - CAP-COM-CAT-014
      - CAP-COM-CAT-016
    workflows: []
    step_evidence:
      - do: >-
          Preview it, pause and preview again, resume and place; attempt to edit the economic
          benefit.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses every
            operation from a state it does not move, 409 invalid_transition
          - >-
            services-commerce:modules/promotions/tests/e2e/promotion-redemptions.e2e.spec.js#names
            the column and offers to end the promotion and duplicate it
          - >-
            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
    assertions:
      - expect: 'Pause removes the benefit, resume restores it and first use prevents the benefit edit.'
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses every
            operation from a state it does not move, 409 invalid_transition
          - >-
            services-commerce:modules/promotions/tests/e2e/promotion-redemptions.e2e.spec.js#names
            the column and offers to end the promotion and duplicate it
          - >-
            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
  - id: ATS-COM-CAT-014-15
    title: First redemption changes which promotion edits remain possible
    category: interaction
    level: api
    proves:
      - BR-CAP-COM-CAT-014-12
      - BR-CAP-COM-CAT-017-01
    preconditions: An unused active promotion is editable.
    steps: 'Place its first qualifying sale, change its benefit and then change its display name.'
    expected: 'Usage is retained, economic editing refuses and the display-name edit succeeds.'
    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-redemptions.e2e.spec.js#names the
        column and offers to end the promotion and duplicate it
      - >-
        services-commerce:modules/promotions/tests/integration/services/persistence/promotion-redemptions.integration.spec.js#locks
        the promotion at its first redemption
    exercises:
      - CAP-COM-CAT-014
      - CAP-COM-CAT-017
    workflows: []
    step_evidence:
      - do: 'Place its first qualifying sale, change its benefit and then change its display name.'
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/tests/e2e/promotion-redemptions.e2e.spec.js#names
            the column and offers to end the promotion and duplicate it
          - >-
            services-commerce:modules/promotions/tests/integration/services/persistence/promotion-redemptions.integration.spec.js#locks
            the promotion at its first redemption
    assertions:
      - expect: 'Usage is retained, economic editing refuses and the display-name edit succeeds.'
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/tests/e2e/promotion-redemptions.e2e.spec.js#names
            the column and offers to end the promotion and duplicate it
          - >-
            services-commerce:modules/promotions/tests/integration/services/persistence/promotion-redemptions.integration.spec.js#locks
            the promotion at its first redemption
  - id: ATS-COM-CAT-014-16
    title: Promotion lifecycle is exercised across every declared move
    category: transition
    level: api
    proves:
      - BR-CAP-COM-CAT-014-09
      - BR-CAP-COM-CAT-014-10
      - BR-CAP-COM-CAT-014-11
      - BR-CAP-COM-CAT-014-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-007-promotion-lifecycle.md#STATE-COM-CAT-007
      - >-
        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/promotions.e2e.spec.js#refuses every
        operation from a state it does not move, 409 invalid_transition
      - >-
        services-commerce:modules/promotions/tests/e2e/promotions-security.e2e.spec.js#cannot
        activate a draft, 403 permission_missing
      - >-
        services-commerce:modules/promotions/services/promotions/promotion-rules.ts:PROMOTION_OPERATIONS
      - >-
        services-commerce:modules/promotions/services/promotions/promotion-rules.ts:PROMOTION_OPERATIONS
      - >-
        services-commerce:modules/promotions/services/persistence/promotions.ts:PROMOTION_TRANSITIONS
      - >-
        services-commerce:modules/promotions/services/persistence/promotions.ts:PROMOTION_TRANSITIONS
      - >-
        services-commerce:modules/promotions/tests/integration/services/persistence/promotion-redemptions.integration.spec.js#locks
        the promotion at its first redemption
    state_model: STATE-COM-CAT-007
    transition_cases:
      - from: draft
        to: active
        trigger: Staff request campaign activation.
        guard: >-
          Launch authority requires a complete usable offer, benefit currency and selected
          eligibility rule, an active code for code claims, and capacity under the
          automatic-campaign ceiling.
        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
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-007-promotion-lifecycle.md#STATE-COM-CAT-007
      - from: active
        to: paused
        trigger: Staff request a temporary campaign pause.
        guard: The caller must hold campaign management authority.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses every
            operation from a state it does not move, 409 invalid_transition
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-007-promotion-lifecycle.md#STATE-COM-CAT-007
      - from: paused
        to: active
        trigger: Staff request resumption or activation of a paused campaign.
        guard: Launch authority and the campaign activation conditions must pass.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/tests/e2e/promotions-security.e2e.spec.js#cannot
            activate a draft, 403 permission_missing
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-007-promotion-lifecycle.md#STATE-COM-CAT-007
      - from: active
        to: ended
        trigger: Staff request permanent campaign ending.
        guard: The caller must hold campaign management authority.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/services/promotions/promotion-rules.ts:PROMOTION_OPERATIONS
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-007-promotion-lifecycle.md#STATE-COM-CAT-007
      - from: paused
        to: ended
        trigger: Staff request permanent campaign ending.
        guard: The caller must hold campaign management authority.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/services/promotions/promotion-rules.ts:PROMOTION_OPERATIONS
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-007-promotion-lifecycle.md#STATE-COM-CAT-007
      - from: draft
        to: archived
        trigger: Staff archive a prepared campaign.
        guard: The caller must hold campaign management authority.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/services/persistence/promotions.ts:PROMOTION_TRANSITIONS
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-007-promotion-lifecycle.md#STATE-COM-CAT-007
      - from: ended
        to: archived
        trigger: Staff archive a campaign that has ended.
        guard: The caller must hold campaign management authority.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/services/persistence/promotions.ts:PROMOTION_TRANSITIONS
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-007-promotion-lifecycle.md#STATE-COM-CAT-007
      - from: active
        to: locked
        trigger: Placement commits the first campaign redemption.
        guard: The placement must successfully acquire promotion capacity.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/tests/integration/services/persistence/promotion-redemptions.integration.spec.js#locks
            the promotion at its first redemption
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/state-models/STATE-COM-CAT-007-promotion-lifecycle.md#STATE-COM-CAT-007
    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-007-promotion-lifecycle.md#STATE-COM-CAT-007
          - >-
            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/promotions.e2e.spec.js#refuses every
            operation from a state it does not move, 409 invalid_transition
          - >-
            services-commerce:modules/promotions/tests/e2e/promotions-security.e2e.spec.js#cannot
            activate a draft, 403 permission_missing
          - >-
            services-commerce:modules/promotions/services/promotions/promotion-rules.ts:PROMOTION_OPERATIONS
          - >-
            services-commerce:modules/promotions/services/promotions/promotion-rules.ts:PROMOTION_OPERATIONS
          - >-
            services-commerce:modules/promotions/services/persistence/promotions.ts:PROMOTION_TRANSITIONS
          - >-
            services-commerce:modules/promotions/services/persistence/promotions.ts:PROMOTION_TRANSITIONS
          - >-
            services-commerce:modules/promotions/tests/integration/services/persistence/promotion-redemptions.integration.spec.js#locks
            the promotion at its first redemption
    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-007-promotion-lifecycle.md#STATE-COM-CAT-007
          - >-
            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/promotions.e2e.spec.js#refuses every
            operation from a state it does not move, 409 invalid_transition
          - >-
            services-commerce:modules/promotions/tests/e2e/promotions-security.e2e.spec.js#cannot
            activate a draft, 403 permission_missing
          - >-
            services-commerce:modules/promotions/services/promotions/promotion-rules.ts:PROMOTION_OPERATIONS
          - >-
            services-commerce:modules/promotions/services/promotions/promotion-rules.ts:PROMOTION_OPERATIONS
          - >-
            services-commerce:modules/promotions/services/persistence/promotions.ts:PROMOTION_TRANSITIONS
          - >-
            services-commerce:modules/promotions/services/persistence/promotions.ts:PROMOTION_TRANSITIONS
          - >-
            services-commerce:modules/promotions/tests/integration/services/persistence/promotion-redemptions.integration.spec.js#locks
            the promotion at its first redemption
  - id: ATS-COM-CAT-014-17
    title: Mixed promotion benefit shapes refuse without a draft
    category: invariant
    level: api
    proves:
      - INV-DOM-COM-CAT-005-02
      - BR-CAP-COM-CAT-014-02
    preconditions: An administrator can prepare an automatic promotion.
    steps: >-
      Create a promotion with a valid merchandise part and an order-total benefit together;
      separately try a twenty-first part.
    expected: Both requests refuse and no invalid draft is retained.
    negative: true
    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 parts together
        with an order-total offer, 400 promotion_invalid in plain words
      - >-
        rhythm-v2-docs:specs/commerce/catalog/design/objects/DOM-COM-CAT-005-promotion.md#DOM-COM-CAT-005
    verification_note: >-
      This design references the invariant; executable binding awaits an assertion-by-assertion
      comparison with this complete scenario.
    step_evidence:
      - do: >-
          Create a promotion with a valid merchandise part and an order-total benefit together;
          separately try a twenty-first part.
        provenance: proposed
        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
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/objects/DOM-COM-CAT-005-promotion.md#DOM-COM-CAT-005
    assertions:
      - expect: Both requests refuse and no invalid draft is retained.
        provenance: proposed
        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
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/objects/DOM-COM-CAT-005-promotion.md#DOM-COM-CAT-005
  - id: ATS-COM-CAT-014-18
    title: A case-only campaign name collision preserves the first draft
    category: invariant
    level: api
    proves:
      - INV-DOM-COM-CAT-005-03
      - BR-CAP-COM-CAT-014-08
    preconditions: A saved campaign has the name Spring Benefit.
    steps: Create a second campaign named SPRING BENEFIT and reread the first.
    expected: The name conflict refuses and the original draft stays unchanged.
    negative: true
    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 a name another
        promotion of the tenant has, whatever its case, 409 promotion_name_taken
      - >-
        rhythm-v2-docs:specs/commerce/catalog/design/objects/DOM-COM-CAT-005-promotion.md#DOM-COM-CAT-005
    verification_note: >-
      This design references the invariant; executable binding awaits an assertion-by-assertion
      comparison with this complete scenario.
    step_evidence:
      - do: Create a second campaign named SPRING BENEFIT and reread the first.
        provenance: proposed
        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
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/objects/DOM-COM-CAT-005-promotion.md#DOM-COM-CAT-005
    assertions:
      - expect: The name conflict refuses and the original draft stays unchanged.
        provenance: proposed
        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
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/objects/DOM-COM-CAT-005-promotion.md#DOM-COM-CAT-005
  - id: ATS-COM-CAT-014-19
    title: Invalid campaign moves preserve state and history
    category: invariant
    level: api
    proves:
      - INV-DOM-COM-CAT-005-04
      - BR-CAP-COM-CAT-014-09
    preconditions: 'Independent campaigns start in draft, active, paused, ended and archived states.'
    steps: >-
      For each origin, request operations the lifecycle forbids and compare the saved state and
      history before and after.
    expected: Each refusal identifies the states and no successful move is recorded.
    negative: true
    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 every
        operation from a state it does not move, 409 invalid_transition
      - >-
        rhythm-v2-docs:specs/commerce/catalog/design/objects/DOM-COM-CAT-005-promotion.md#DOM-COM-CAT-005
    verification_note: >-
      This design references the invariant; executable binding awaits an assertion-by-assertion
      comparison with this complete scenario.
    step_evidence:
      - do: >-
          For each origin, request operations the lifecycle forbids and compare the saved state and
          history before and after.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses every
            operation from a state it does not move, 409 invalid_transition
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/objects/DOM-COM-CAT-005-promotion.md#DOM-COM-CAT-005
    assertions:
      - expect: Each refusal identifies the states and no successful move is recorded.
        provenance: proposed
        source:
          - >-
            services-commerce:modules/promotions/tests/e2e/promotions.e2e.spec.js#refuses every
            operation from a state it does not move, 409 invalid_transition
          - >-
            rhythm-v2-docs:specs/commerce/catalog/design/objects/DOM-COM-CAT-005-promotion.md#DOM-COM-CAT-005
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-014-create-a-promotion-and-take-it-through-its-life.md#CAP-COM-CAT-014
  - >-
    rhythm-v2-docs:specs/commerce/catalog/design/workflows/WF-COM-CAT-020-create-or-edit-a-promotion-and-its-parts.md#WF-COM-CAT-020
  - >-
    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-068'
  failure: 'gap: FND-COM-CAT-068'
  boundary: covered
  concurrency: 'gap: FND-COM-CAT-068'
  invariant: covered
  interaction: covered
coverage_notes:
  edge: >-
    No complete edge scenario is established by this back-fill; the finding records the missing
    proof.
  failure: >-
    No complete failure 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-068'
    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-068'
  - key: commerce.promotions.manage
    coverage: 'gap: FND-COM-CAT-068'
    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-068'
  - key: commerce.promotions.activate
    coverage: 'gap: FND-COM-CAT-068'
    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-068'
---

# Prepare and control a promotion’s lifecycle: Capability Test Specification

## Coverage

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

## Interactions With Related Capabilities

Code state changes promotion launch readiness. Promotion lifecycle changes checkout candidates. First redemption changes which promotion edits remain possible.

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