---
id: ATS-ACC-PRD-002
scope: CAP-ACC-PRD-002
product_area: accounting
module: products
revision: 3
status: proposed
approved_by: null
approved_at: null
source_sha:
  services-accounting: f47cb263875bbc0c5640fba7dae6785ec32695f8
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
scenarios:
  - id: ATS-ACC-PRD-002-01
    title: Creation validates readable identities
    level: unit
    proves:
      - BR-CAP-ACC-PRD-002-01
    preconditions: An existing unit can own a new product.
    steps: 'Try invalid punctuation, a reserved identity, a leading digit, excess length and a valid identity.'
    expected: Invalid identities fail without writes and the valid identity succeeds.
    negative: true
    data_needs: An existing unit can own a new product.
    bound_to:
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
        an id that is not a configuration id, with the configuration-id sentence, and writes nothing
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
        a reserved id
      - >-
        services-accounting:modules/products/tests/e2e/products.e2e.spec.js#answers 400 naming id for an id
        that is not a configuration id
      - >-
        services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
        an id longer than 63 characters
      - TST-ACC-PRD-040
      - TST-ACC-PRD-030
      - TST-ACC-PRD-031
    provenance: test
    source: >-
      services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
      an id that is not a configuration id, with the configuration-id sentence, and writes nothing
    category: validation
    binding_scope:
      - suite: TST-ACC-PRD-040
        assertions:
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
            an id that is not a configuration id, with the configuration-id sentence, and writes nothing
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
            a reserved id
        provenance: test
        source: >-
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          an id that is not a configuration id, with the configuration-id sentence, and writes nothing;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          a reserved id; services-accounting:modules/products/tests/e2e/products.e2e.spec.js#answers 400
          naming id for an id that is not a configuration id;
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
          an id longer than 63 characters
      - suite: TST-ACC-PRD-030
        assertions:
          - >-
            services-accounting:modules/products/tests/e2e/products.e2e.spec.js#answers 400 naming id for an
            id that is not a configuration id
        provenance: test
        source: >-
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          an id that is not a configuration id, with the configuration-id sentence, and writes nothing;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          a reserved id; services-accounting:modules/products/tests/e2e/products.e2e.spec.js#answers 400
          naming id for an id that is not a configuration id;
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
          an id longer than 63 characters
      - suite: TST-ACC-PRD-031
        assertions:
          - >-
            services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
            an id longer than 63 characters
        provenance: test
        source: >-
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          an id that is not a configuration id, with the configuration-id sentence, and writes nothing;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          a reserved id; services-accounting:modules/products/tests/e2e/products.e2e.spec.js#answers 400
          naming id for an id that is not a configuration id;
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
          an id longer than 63 characters
  - id: ATS-ACC-PRD-002-02
    title: Name-derived collisions need an explicit choice
    level: unit
    proves:
      - BR-CAP-ACC-PRD-002-02
    preconditions: A product already owns the identity derived from Gala Ticket.
    steps: 'Create another without an identity, then with a distinct identity, and try a numeric-only name.'
    expected: The collision and unconvertible name fail; an explicit distinct identity succeeds.
    negative: true
    data_needs: A product already owns the identity derived from Gala Ticket.
    bound_to:
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
        a second product whose name suggests a taken id, as other configuration does, naming the id
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#creates
        the second product of that name once the client names an id of its own
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#asks
        for an id when the name suggests none
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#generates
        the id from the name when the client gives none, as every configuration id is
      - TST-ACC-PRD-040
    provenance: test
    source: >-
      services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
      a second product whose name suggests a taken id, as other configuration does, naming the id
    category: edge
    binding_scope:
      - suite: TST-ACC-PRD-040
        assertions:
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
            a second product whose name suggests a taken id, as other configuration does, naming the id
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#creates
            the second product of that name once the client names an id of its own
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#asks
            for an id when the name suggests none
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#generates
            the id from the name when the client gives none, as every configuration id is
        provenance: test
        source: >-
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          a second product whose name suggests a taken id, as other configuration does, naming the id;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#creates
          the second product of that name once the client names an id of its own;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#asks
          for an id when the name suggests none;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#generates
          the id from the name when the client gives none, as every configuration id is
  - id: ATS-ACC-PRD-002-03
    title: Identity uniqueness spans kinds
    level: api
    proves:
      - BR-CAP-ACC-PRD-002-03
      - INV-DOM-ACC-PRD-001-01
    preconditions: A donation has identity gala.
    steps: Create a different kind using gala.
    expected: The second create fails and only the original remains.
    negative: true
    data_needs: A donation has identity gala.
    bound_to:
      - >-
        services-accounting:modules/products/tests/e2e/products.e2e.spec.js#answers 400 to the same id under
        another type: a product id is unique across every type
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
        the same id under another type: a product id is unique across every type
      - >-
        services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses a
        second product under the same id, of another type: the id is unique across every type
      - TST-ACC-PRD-030
      - TST-ACC-PRD-040
      - TST-ACC-PRD-031
    provenance: test
    source: >-
      services-accounting:modules/products/tests/e2e/products.e2e.spec.js#answers 400 to the same id under
      another type: a product id is unique across every type
    category: invariant
    binding_scope:
      - suite: TST-ACC-PRD-030
        assertions:
          - >-
            services-accounting:modules/products/tests/e2e/products.e2e.spec.js#answers 400 to the same id
            under another type: a product id is unique across every type
        provenance: test
        source: >-
          services-accounting:modules/products/tests/e2e/products.e2e.spec.js#answers 400 to the same id under
          another type: a product id is unique across every type;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          the same id under another type: a product id is unique across every type;
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
          a second product under the same id, of another type: the id is unique across every type
      - suite: TST-ACC-PRD-040
        assertions:
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
            the same id under another type: a product id is unique across every type
        provenance: test
        source: >-
          services-accounting:modules/products/tests/e2e/products.e2e.spec.js#answers 400 to the same id under
          another type: a product id is unique across every type;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          the same id under another type: a product id is unique across every type;
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
          a second product under the same id, of another type: the id is unique across every type
      - suite: TST-ACC-PRD-031
        assertions:
          - >-
            services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
            a second product under the same id, of another type: the id is unique across every type
        provenance: test
        source: >-
          services-accounting:modules/products/tests/e2e/products.e2e.spec.js#answers 400 to the same id under
          another type: a product id is unique across every type;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          the same id under another type: a product id is unique across every type;
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
          a second product under the same id, of another type: the id is unique across every type
  - id: ATS-ACC-PRD-002-04
    title: 'Creation checks kind, unit and name'
    level: api
    proves:
      - BR-CAP-ACC-PRD-002-04
    preconditions: The register and one unit are available.
    steps: 'Try an unknown kind, inactive kind, foreign workspace unit and blank name.'
    expected: Each invalid creation fails and persists nothing.
    negative: true
    data_needs: The register and one unit are available.
    bound_to:
      - >-
        services-accounting:modules/products/tests/e2e/products-shared-base.e2e.spec.js#refuses a type
        registered inactive (an Events type), 400 with its sentence
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
        a product type the registry does not hold
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
        a business unit the tenant does not have
      - >-
        services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#turns a
        blank name into null and refuses it
      - TST-ACC-PRD-029
      - TST-ACC-PRD-040
      - TST-ACC-PRD-031
    provenance: test
    source: >-
      services-accounting:modules/products/tests/e2e/products-shared-base.e2e.spec.js#refuses a type
      registered inactive (an Events type), 400 with its sentence
    category: validation
    binding_scope:
      - suite: TST-ACC-PRD-029
        assertions:
          - >-
            services-accounting:modules/products/tests/e2e/products-shared-base.e2e.spec.js#refuses a type
            registered inactive (an Events type), 400 with its sentence
        provenance: test
        source: >-
          services-accounting:modules/products/tests/e2e/products-shared-base.e2e.spec.js#refuses a type
          registered inactive (an Events type), 400 with its sentence;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          a product type the registry does not hold;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          a business unit the tenant does not have;
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#turns a
          blank name into null and refuses it
      - suite: TST-ACC-PRD-040
        assertions:
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
            a product type the registry does not hold
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
            a business unit the tenant does not have
        provenance: test
        source: >-
          services-accounting:modules/products/tests/e2e/products-shared-base.e2e.spec.js#refuses a type
          registered inactive (an Events type), 400 with its sentence;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          a product type the registry does not hold;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          a business unit the tenant does not have;
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#turns a
          blank name into null and refuses it
      - suite: TST-ACC-PRD-031
        assertions:
          - >-
            services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#turns
            a blank name into null and refuses it
        provenance: test
        source: >-
          services-accounting:modules/products/tests/e2e/products-shared-base.e2e.spec.js#refuses a type
          registered inactive (an Events type), 400 with its sentence;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          a product type the registry does not hold;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          a business unit the tenant does not have;
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#turns a
          blank name into null and refuses it
  - id: ATS-ACC-PRD-002-05
    title: Stored pricing follows the kind
    level: unit
    proves:
      - BR-CAP-ACC-PRD-002-05
      - INV-DOM-ACC-PRD-001-04
    preconditions: A unit is available for product creation.
    steps: Create a donation and attempt a direct row write with a mismatched model.
    expected: The donation has variable pricing and the inconsistent database row fails.
    negative: true
    data_needs: A unit is available for product creation.
    bound_to:
      - >-
        services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses a
        pricing model other than its type’s
      - >-
        services-accounting:modules/products/tests/e2e/products.e2e.spec.js#creates a Donation Product with no
        prices and the variable pricing model
      - TST-ACC-PRD-031
      - TST-ACC-PRD-030
    provenance: test
    source: >-
      services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses a
      pricing model other than its type’s
    category: invariant
    binding_scope:
      - suite: TST-ACC-PRD-031
        assertions:
          - >-
            services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
            a pricing model other than its type’s
        provenance: test
        source: >-
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
          a pricing model other than its type’s;
          services-accounting:modules/products/tests/e2e/products.e2e.spec.js#creates a Donation Product with
          no prices and the variable pricing model
      - suite: TST-ACC-PRD-030
        assertions:
          - >-
            services-accounting:modules/products/tests/e2e/products.e2e.spec.js#creates a Donation Product
            with no prices and the variable pricing model
        provenance: test
        source: >-
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
          a pricing model other than its type’s;
          services-accounting:modules/products/tests/e2e/products.e2e.spec.js#creates a Donation Product with
          no prices and the variable pricing model
  - id: ATS-ACC-PRD-002-06
    title: Direct writes cannot move a product
    level: unit
    proves:
      - BR-CAP-ACC-PRD-002-06
      - INV-DOM-ACC-PRD-001-03
    preconditions: 'An existing product has fixed identity, kind and unit.'
    steps: Attempt direct database changes to those three properties.
    expected: The write-once protection rejects each change.
    negative: true
    data_needs: 'An existing product has fixed identity, kind and unit.'
    bound_to:
      - >-
        services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses a
        change of business_unit_id (RHF01)
      - >-
        services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses a
        change of product_type_id (RHF01)
      - >-
        services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses a
        change of id (RHF01)
      - 'react-console:acceptance/accounting/playwright/journeys/products/ACT-ACC-0143.spec.ts#ACT-ACC-0143'
      - TST-ACC-PRD-031
      - TST-ACC-PRD-015
    provenance: test
    source: >-
      services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses a
      change of business_unit_id (RHF01)
    category: invariant
    binding_scope:
      - suite: TST-ACC-PRD-031
        assertions:
          - >-
            services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
            a change of business_unit_id (RHF01)
          - >-
            services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
            a change of product_type_id (RHF01)
          - >-
            services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
            a change of id (RHF01)
        provenance: test
        source: >-
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
          a change of business_unit_id (RHF01);
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
          a change of product_type_id (RHF01);
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
          a change of id (RHF01);
          react-console:acceptance/accounting/playwright/journeys/products/ACT-ACC-0143.spec.ts#ACT-ACC-0143
      - suite: TST-ACC-PRD-015
        assertions:
          - 'react-console:acceptance/accounting/playwright/journeys/products/ACT-ACC-0143.spec.ts#ACT-ACC-0143'
        provenance: test
        source: >-
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
          a change of business_unit_id (RHF01);
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
          a change of product_type_id (RHF01);
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
          a change of id (RHF01);
          react-console:acceptance/accounting/playwright/journeys/products/ACT-ACC-0143.spec.ts#ACT-ACC-0143
  - id: ATS-ACC-PRD-002-07
    title: Availability boundaries must increase
    level: unit
    proves:
      - BR-CAP-ACC-PRD-002-08
    preconditions: An existing unit can own the product.
    steps: Try windows ending before or at their opening instant.
    expected: Both requests fail validation.
    negative: true
    data_needs: An existing unit can own the product.
    bound_to:
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
        a sale window that ends before it starts
      - >-
        services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses a
        sale window that ends the instant it starts
      - TST-ACC-PRD-040
      - TST-ACC-PRD-031
    provenance: test
    source: >-
      services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
      a sale window that ends before it starts
    category: boundary
    binding_scope:
      - suite: TST-ACC-PRD-040
        assertions:
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
            a sale window that ends before it starts
        provenance: test
        source: >-
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          a sale window that ends before it starts;
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
          a sale window that ends the instant it starts
      - suite: TST-ACC-PRD-031
        assertions:
          - >-
            services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
            a sale window that ends the instant it starts
        provenance: test
        source: >-
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          a sale window that ends before it starts;
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
          a sale window that ends the instant it starts
  - id: ATS-ACC-PRD-002-08
    title: Fees reject negative money
    level: unit
    proves:
      - BR-CAP-ACC-PRD-002-09
    preconditions: An existing unit can own the product.
    steps: Write negative cancellation and restocking amounts separately.
    expected: Both writes fail.
    negative: true
    data_needs: An existing unit can own the product.
    bound_to:
      - >-
        services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses a
        negative cancellation fee
      - >-
        services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses a
        negative restocking fee
      - TST-ACC-PRD-031
    provenance: test
    source: >-
      services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses a
      negative cancellation fee
    category: validation
    binding_scope:
      - suite: TST-ACC-PRD-031
        assertions:
          - >-
            services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
            a negative cancellation fee
          - >-
            services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
            a negative restocking fee
        provenance: test
        source: >-
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
          a negative cancellation fee;
          services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
          a negative restocking fee
  - id: ATS-ACC-PRD-002-09
    title: Kinds constrain selling configuration
    level: unit
    proves:
      - BR-CAP-ACC-PRD-002-10
      - INV-DOM-ACC-PRD-001-12
    preconditions: A unit and CRM task type support feature fixtures.
    steps: 'Try each fee and feature on Legacy Products, then discounts, bundles and other features on donations.'
    expected: >-
      Legacy feature writes fail; donations reject discounts and components while permitting their other
      supported settings.
    negative: true
    data_needs: A unit and CRM task type support feature fixtures.
    bound_to:
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
        a cancellation fee on a Legacy Product
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
        bulk pricing rules on a Legacy Product
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
        a purchase limit on a Legacy Product
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
        tasks to generate on a Legacy Product
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
        a confirmation e-mail on a Legacy Product
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
        a restocking fee on a Legacy Product
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
        bulk pricing on a Donation Product
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#accepts
        the other features on a Donation Product
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
        components on a Donation Product
      - TST-ACC-PRD-039
      - TST-ACC-PRD-040
    provenance: test
    source: >-
      services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
      a cancellation fee on a Legacy Product
    category: validation
    binding_scope:
      - suite: TST-ACC-PRD-039
        assertions:
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
            a cancellation fee on a Legacy Product
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
            bulk pricing rules on a Legacy Product
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
            a purchase limit on a Legacy Product
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
            tasks to generate on a Legacy Product
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
            a confirmation e-mail on a Legacy Product
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
            a restocking fee on a Legacy Product
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
            bulk pricing on a Donation Product
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#accepts
            the other features on a Donation Product
        provenance: test
        source: >-
          services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
          a cancellation fee on a Legacy Product;
          services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
          bulk pricing rules on a Legacy Product;
          services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
          a purchase limit on a Legacy Product;
          services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
          tasks to generate on a Legacy Product;
          services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
          a confirmation e-mail on a Legacy Product;
          services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
          a restocking fee on a Legacy Product;
          services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
          bulk pricing on a Donation Product;
          services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#accepts
          the other features on a Donation Product;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          components on a Donation Product
      - suite: TST-ACC-PRD-040
        assertions:
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
            components on a Donation Product
        provenance: test
        source: >-
          services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
          a cancellation fee on a Legacy Product;
          services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
          bulk pricing rules on a Legacy Product;
          services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
          a purchase limit on a Legacy Product;
          services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
          tasks to generate on a Legacy Product;
          services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
          a confirmation e-mail on a Legacy Product;
          services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
          a restocking fee on a Legacy Product;
          services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
          bulk pricing on a Donation Product;
          services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#accepts
          the other features on a Donation Product;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          components on a Donation Product
  - id: ATS-ACC-PRD-002-10
    title: Creation refusal leaves no owned rows
    level: unit
    proves:
      - BR-CAP-ACC-PRD-002-11
    preconditions: An existing unit can own the product.
    steps: Submit a create with an invalid second price.
    expected: 'No product, price or component survives.'
    negative: true
    data_needs: An existing unit can own the product.
    bound_to:
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#leaves
        nothing behind after a refusal
      - TST-ACC-PRD-040
    provenance: test
    source: >-
      services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#leaves
      nothing behind after a refusal
    category: failure
    binding_scope:
      - suite: TST-ACC-PRD-040
        assertions:
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#leaves
            nothing behind after a refusal
        provenance: test
        source: >-
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#leaves
          nothing behind after a refusal
  - id: ATS-ACC-PRD-002-11
    title: Older identities permit reads and references
    level: unit
    proves:
      - BR-CAP-ACC-PRD-002-12
    preconditions: An older product retains its generated identity.
    steps: 'Read it, reference it from a new bundle and attempt replacement.'
    expected: 'Reading and bundling succeed, but replacement fails without changes.'
    negative: true
    data_needs: An older product retains its generated identity.
    bound_to:
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
        a replacement, 409, saying why, and leaves the product unchanged
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#reads
        as any other product, under its uuid
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#may
        be bundled by a new product (the reference is the text form)
      - TST-ACC-PRD-040
    provenance: test
    source: >-
      services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
      a replacement, 409, saying why, and leaves the product unchanged
    category: edge
    binding_scope:
      - suite: TST-ACC-PRD-040
        assertions:
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
            a replacement, 409, saying why, and leaves the product unchanged
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#reads
            as any other product, under its uuid
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#may
            be bundled by a new product (the reference is the text form)
        provenance: test
        source: >-
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
          a replacement, 409, saying why, and leaves the product unchanged;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#reads
          as any other product, under its uuid;
          services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#may
          be bundled by a new product (the reference is the text form)
  - id: ATS-ACC-PRD-002-12
    title: Custom field values are absent
    level: api
    proves:
      - BR-CAP-ACC-PRD-002-13
      - INV-DOM-ACC-PRD-001-14
    preconditions: A unit supports a product fixture.
    steps: Create with custom field values and read an ordinary product.
    expected: Creation rejects the unsupported property and reads contain no such values.
    negative: true
    data_needs: A unit supports a product fixture.
    bound_to:
      - >-
        services-accounting:modules/products/tests/e2e/products-shared-base.e2e.spec.js#refuses a body that
        names custom fields, 400 naming the property
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.shared-base.integration.spec.js#answers
        no custom_fields property on a read
      - >-
        services-accounting:modules/products/tests/integration/services/persistence/products.shared-base.integration.spec.js#has
        no table of product custom field values
      - TST-ACC-PRD-029
      - TST-ACC-PRD-042
    provenance: test
    source: >-
      services-accounting:modules/products/tests/e2e/products-shared-base.e2e.spec.js#refuses a body that
      names custom fields, 400 naming the property
    category: validation
    binding_scope:
      - suite: TST-ACC-PRD-029
        assertions:
          - >-
            services-accounting:modules/products/tests/e2e/products-shared-base.e2e.spec.js#refuses a body
            that names custom fields, 400 naming the property
        provenance: test
        source: >-
          services-accounting:modules/products/tests/e2e/products-shared-base.e2e.spec.js#refuses a body that
          names custom fields, 400 naming the property;
          services-accounting:modules/products/tests/integration/services/persistence/products.shared-base.integration.spec.js#answers
          no custom_fields property on a read;
          services-accounting:modules/products/tests/integration/services/persistence/products.shared-base.integration.spec.js#has
          no table of product custom field values
      - suite: TST-ACC-PRD-042
        assertions:
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.shared-base.integration.spec.js#answers
            no custom_fields property on a read
          - >-
            services-accounting:modules/products/tests/integration/services/persistence/products.shared-base.integration.spec.js#has
            no table of product custom field values
        provenance: test
        source: >-
          services-accounting:modules/products/tests/e2e/products-shared-base.e2e.spec.js#refuses a body that
          names custom fields, 400 naming the property;
          services-accounting:modules/products/tests/integration/services/persistence/products.shared-base.integration.spec.js#answers
          no custom_fields property on a read;
          services-accounting:modules/products/tests/integration/services/persistence/products.shared-base.integration.spec.js#has
          no table of product custom field values
  - id: ATS-ACC-PRD-002-13
    title: Product creation needs create authority
    level: api
    proves:
      - BR-CAP-ACC-PRD-002-14
    preconditions: 'Read-only, product-manager and accounting-only identities are available.'
    steps: Attempt product creation under each identity.
    expected: Only the product manager succeeds.
    negative: true
    data_needs: 'Read-only, product-manager and accounting-only identities are available.'
    bound_to:
      - >-
        services-accounting:modules/products/tests/e2e/products-security.e2e.spec.js#is refused creating a
        product
      - 'services-accounting:modules/products/tests/e2e/products-security.e2e.spec.js#creates a product'
      - TST-ACC-PRD-028
    provenance: test
    source: >-
      services-accounting:modules/products/tests/e2e/products-security.e2e.spec.js#is refused creating a
      product
    category: permission
    binding_scope:
      - suite: TST-ACC-PRD-028
        assertions:
          - >-
            services-accounting:modules/products/tests/e2e/products-security.e2e.spec.js#is refused creating a
            product
          - 'services-accounting:modules/products/tests/e2e/products-security.e2e.spec.js#creates a product'
        provenance: test
        source: >-
          services-accounting:modules/products/tests/e2e/products-security.e2e.spec.js#is refused creating a
          product; services-accounting:modules/products/tests/e2e/products-security.e2e.spec.js#creates a
          product
  - id: ATS-ACC-PRD-002-14
    title: Creation chooses the initial availability state
    category: transition
    level: api
    proves:
      - BR-CAP-ACC-PRD-002-07
    preconditions: Valid fixed-priced products use distinct identities.
    steps: 'Create one without status, one inactive, and one requesting retirement.'
    expected: The first two reads show their requested initial states; retirement input is refused.
    negative: true
    data_needs: Valid fixed-priced products use distinct identities.
    bound_to: []
    provenance: proposed
    source: 'services-accounting:modules/products/aws/api/products.schemas.ts:productShape'
    binding_scope: []
  - id: ATS-ACC-PRD-002-15
    title: Invalid pricing or bundling prevents a complete product replacement
    category: interaction
    level: api
    proves:
      - BR-CAP-ACC-PRD-002-11
      - BR-CAP-ACC-PRD-004-06
      - BR-CAP-ACC-PRD-008-01
      - BR-CAP-ACC-PRD-011-02
    preconditions: A valid product has a standard price and same-unit sellable component.
    steps: >-
      Create it with both collections, then replace it with an invalid price and with a foreign-unit
      component.
    expected: The valid creation reads back as one aggregate; neither refused replacement changes it.
    negative: true
    data_needs: A valid product has a standard price and same-unit sellable component.
    bound_to: []
    provenance: proposed
    source: >-
      services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#leaves
      nothing behind after a refusal;
      services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#leaves
      the old prices and components when a rule refuses the replacement;
      services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
      an Accounting Product without a standard price in the base currency;
      services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
      a component of another business unit, naming both units
    exercises:
      - CAP-ACC-PRD-002
      - CAP-ACC-PRD-004
      - CAP-ACC-PRD-008
      - CAP-ACC-PRD-011
    binding_scope: []
evidence:
  - 'react-console:acceptance/accounting/playwright/journeys/products/ACT-ACC-0143.spec.ts'
  - 'services-accounting:modules/products/aws/api/products-api-mapper.ts'
  - 'services-accounting:modules/products/aws/api/products.routes.ts'
  - 'services-accounting:modules/products/aws/api/products.schemas.ts'
  - 'services-accounting:modules/products/db/products.table.ts'
  - 'services-accounting:modules/products/services/persistence/product-refusals.ts'
  - 'services-accounting:modules/products/services/persistence/products.ts'
  - 'services-accounting:modules/products/tests/e2e/products-security.e2e.spec.js'
  - 'services-accounting:modules/products/tests/e2e/products-shared-base.e2e.spec.js'
  - 'services-accounting:modules/products/tests/e2e/products.e2e.spec.js'
  - 'services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js'
  - >-
    services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js
  - 'services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js'
  - >-
    services-accounting:modules/products/tests/integration/services/persistence/products.shared-base.integration.spec.js
  - >-
    rhythm-v2-docs:specs/accounting/products/design/workflows/WF-ACC-PRD-001-create-or-replace-a-product-with-its-owned-settings.md
  - >-
    rhythm-v2-docs:specs/accounting/products/design/workflows/WF-ACC-PRD-002-save-a-product-and-its-accounting-setup-from.md
coverage:
  rule: 'gap: FND-ACC-PRD-051'
  transition: covered
  validation: covered
  permission: covered
  edge: covered
  failure: covered
  boundary: covered
  concurrency: 'gap: FND-ACC-PRD-051'
  invariant: covered
  interaction: covered
coverage_notes:
  rule:
    reason: No dedicated scenario isolates this category for the capability.
    provenance: unknown
    source: FND-ACC-PRD-051
  concurrency:
    reason: No dedicated scenario isolates this category for the capability.
    provenance: unknown
    source: FND-ACC-PRD-051
  remaining_obligations:
    reason: >-
      Concurrent identity creation, initial inactive status, all identity syntax boundaries and recovery after
      a lost creation response need executable proof. Workflow edges not named by a scenario remain in this
      finding rather than gaining a binding from shared workflow membership.
    provenance: unknown
    source: FND-ACC-PRD-051
  meaning:
    reason: >-
      Covered means a specification scenario exists in this category. Executable coverage and run results
      remain independent.
    provenance: documented
    source: 'rhythm-v2-docs:specs/CONVENTIONS.md#9.10 Capability Test Specification (`ATS`, under `design/tests/`)'
coverage_findings:
  - FND-ACC-PRD-051
related_capabilities:
  - CAP-ACC-PRD-004
  - CAP-ACC-PRD-008
  - CAP-ACC-PRD-011
workflows:
  - WF-ACC-PRD-001
  - WF-ACC-PRD-002
rule_coverage:
  - rule: BR-CAP-ACC-PRD-002-01
    scenarios:
      - ATS-ACC-PRD-002-01
  - rule: BR-CAP-ACC-PRD-002-02
    scenarios:
      - ATS-ACC-PRD-002-02
  - rule: BR-CAP-ACC-PRD-002-03
    scenarios:
      - ATS-ACC-PRD-002-03
  - rule: BR-CAP-ACC-PRD-002-04
    scenarios:
      - ATS-ACC-PRD-002-04
  - rule: BR-CAP-ACC-PRD-002-05
    scenarios:
      - ATS-ACC-PRD-002-05
  - rule: BR-CAP-ACC-PRD-002-06
    scenarios:
      - ATS-ACC-PRD-002-06
  - rule: BR-CAP-ACC-PRD-002-08
    scenarios:
      - ATS-ACC-PRD-002-07
  - rule: BR-CAP-ACC-PRD-002-09
    scenarios:
      - ATS-ACC-PRD-002-08
  - rule: BR-CAP-ACC-PRD-002-10
    scenarios:
      - ATS-ACC-PRD-002-09
  - rule: BR-CAP-ACC-PRD-002-11
    scenarios:
      - ATS-ACC-PRD-002-10
      - ATS-ACC-PRD-002-15
  - rule: BR-CAP-ACC-PRD-002-12
    scenarios:
      - ATS-ACC-PRD-002-11
  - rule: BR-CAP-ACC-PRD-002-13
    scenarios:
      - ATS-ACC-PRD-002-12
  - rule: BR-CAP-ACC-PRD-002-14
    scenarios:
      - ATS-ACC-PRD-002-13
interaction_coverage:
  - scenario: ATS-ACC-PRD-002-15
    exercises:
      - CAP-ACC-PRD-002
      - CAP-ACC-PRD-004
      - CAP-ACC-PRD-008
      - CAP-ACC-PRD-011
    provenance: proposed
    source: >-
      services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#leaves
      nothing behind after a refusal;
      services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#leaves
      the old prices and components when a rule refuses the replacement;
      services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
      an Accounting Product without a standard price in the base currency;
      services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
      a component of another business unit, naming both units
permission_coverage:
  - key: accounting.products.create
    where_checked: api
    scope: tenant
    scenarios:
      - ATS-ACC-PRD-002-13
    finding: null
    provenance: proposed
    source: >-
      services-accounting:modules/products/tests/e2e/products-security.e2e.spec.js#is refused creating a
      product
invariant_coverage:
  - invariant: INV-DOM-ACC-PRD-001-01
    scenarios:
      - ATS-ACC-PRD-002-03
    finding: null
    provenance: test
    source: >-
      services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses a
      second product under the same id, of another type: the id is unique across every type
  - invariant: INV-DOM-ACC-PRD-001-02
    scenarios:
      - ATS-ACC-PRD-002-01
    finding: FND-ACC-PRD-051
    provenance: proposed
    source: >-
      services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#holds the
      id check NOT VALID, as migration 0056 created it, so the rows written before it are not judged
    note: >-
      The mapped scenario exercises the related behavior; the complete invariant statement still needs
      dedicated proof.
  - invariant: INV-DOM-ACC-PRD-001-03
    scenarios:
      - ATS-ACC-PRD-002-06
    finding: null
    provenance: test
    source: >-
      services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses a
      change of business_unit_id (RHF01)
  - invariant: INV-DOM-ACC-PRD-001-04
    scenarios:
      - ATS-ACC-PRD-002-05
    finding: null
    provenance: test
    source: >-
      services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses a
      pricing model other than its type’s
  - invariant: INV-DOM-ACC-PRD-001-05
    scenarios:
      - ATS-ACC-PRD-002-04
    finding: FND-ACC-PRD-051
    provenance: proposed
    source: 'services-accounting:modules/products/services/persistence/products.ts:createProduct'
    note: >-
      The mapped scenario exercises the related behavior; the complete invariant statement still needs
      dedicated proof.
  - invariant: INV-DOM-ACC-PRD-001-12
    scenarios:
      - ATS-ACC-PRD-002-09
    finding: null
    provenance: test
    source: >-
      services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
      a confirmation e-mail on a Legacy Product
  - invariant: INV-DOM-ACC-PRD-001-14
    scenarios:
      - ATS-ACC-PRD-002-12
    finding: null
    provenance: test
    source: >-
      services-accounting:modules/products/tests/e2e/products-shared-base.e2e.spec.js#refuses a body that
      names custom fields, 400 naming the property
---

# Create a product with a permanent identity: Capability Test Specification

## Coverage

The frontmatter lists a proof scenario or finding for every required rule and records each applicable category. Scenario coverage describes what to check; bindings identify the assertions that already exist, and execution results remain unverified. Concurrent identity creation, initial inactive status, all identity syntax boundaries and recovery after a lost creation response need executable proof. Workflow edges not named by a scenario remain in this finding rather than gaining a binding from shared workflow membership.

## Interactions With Related Capabilities

Invalid pricing or bundling prevents a complete product replacement. A valid product has a standard price and same-unit sellable component. Create it with both collections, then replace it with an invalid price and with a foreign-unit component. The valid creation reads back as one aggregate; neither refused replacement changes it.
