---
id: DOM-ACC-PRD-001
title: Product
realises: CON-ACC-PRD-001
product_area: accounting
module: products
revision: 3
status: draft
design_intent: proposed
implementation_evidence: partially-observed
source_sha:
  services-accounting: f47cb263875bbc0c5640fba7dae6785ec32695f8
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
owned_by: SVC-ACC-PRD-products
readable_by: []
lifecycle: STATE-ACC-PRD-001
custom_fields: none
identity: Each product has a workspace-unique readable key shared across kinds. Creation accepts a client key
  or derives one from the name. The key, kind and business unit are permanent. Older generated keys can still
  be read, referenced and deleted, but cannot pass the constraint on a product update.
attributes:
- name: id
  kind: text
  required: true
  computed: false
  notes: A readable key has at most 63 characters, starts with a lowercase letter and uses lowercase letters,
    digits and single underscores. Reserved keys fail. Name-derived collisions require another client choice.
- name: kind (product type)
  kind: reference
  required: true
  computed: false
  notes: The registered kind fixes ownership, pricing, extension metadata and supported features. New products
    require an active kind; replacement cannot change it.
- name: pricing model
  kind: enum
  required: true
  computed: true
  notes: Pricing is copied from the kind as fixed, giver-priced or account-only. Submitted pricing-model values
    are discarded.
- name: name and description
  kind: text
  required: true
  computed: false
  notes: Name must remain nonblank after trimming; description is optional.
- name: business unit
  kind: reference
  required: true
  computed: false
  notes: The workspace business unit is selected once. Books owns its currencies and reference records used
    by this product.
- name: status
  kind: enum
  required: true
  computed: false
  notes: Active and inactive are valid creation and replacement values. Retirement and reactivation use lifecycle
    operations.
- name: sale window
  kind: date
  required: false
  computed: false
  notes: Availability can have optional opening and closing instants; when both exist, opening must precede
    closing.
- name: hidden from portal
  kind: enum
  required: true
  computed: false
  notes: The flag defaults to allowing portal visibility. The current form does not expose this setting.
- name: cancellation and restocking fees
  kind: money
  required: false
  computed: false
  notes: These optional charges use nonnegative integer minor units of the base currency and are refused for
    unsellable kinds.
- name: notes
  kind: text
  required: false
  computed: false
  notes: Optional internal remarks remain part of the product record.
- name: prices
  kind: collection
  required: false
  computed: false
  notes: The product owns a grid of up to 500 cells and submits that entire grid when saved.
- name: bundle components
  kind: collection
  required: false
  computed: false
  notes: A bundle owns up to 50 distinct component entries with bundled or inline roles, positive integer quantities
    and standard and optional member allocations. Replacement rewrites the entries.
- name: quantity discounts
  kind: collection
  required: false
  computed: false
  notes: At most 50 inclusive bands carry a percentage or monetary reduction without overlapping quantities.
    Null disables the feature; the HTTP schema rejects an empty list.
- name: purchase limit
  kind: collection
  required: false
  computed: false
  notes: The stored per-customer ceiling has a positive maximum for all time or a rolling interval, with optional
    refusal wording.
- name: tasks to generate
  kind: collection
  required: false
  computed: false
  notes: Up to 20 instructions specify CRM task types, optional queues and assignees, and optional nonnegative
    due delays. Saving rewrites the list.
- name: confirmation message
  kind: enum
  required: false
  computed: false
  notes: Confirmation selects a shared template or Platform-owned content; null disables it and omission preserves
    the existing selection during HTTP replacement.
- name: accounting summary
  kind: computed
  required: false
  computed: true
  notes: Reads return the setup's revenue account and recognition strategy, or no summary when a setup is absent.
- name: resolved tax class
  kind: computed
  required: false
  computed: true
  notes: Reads derive the class from the setup, then the kind default, then no class.
- name: lock
  kind: enum
  required: false
  computed: false
  notes: Protection can be absent, deletion-only, or combined update and deletion protection.
- name: version and stamps
  kind: computed
  required: true
  computed: true
  notes: The service supplies versions and creation and modification stamps. A client may retain an external
    import reference.
relationships:
- to: DOM-ACC-PRD-002
  cardinality: one-to-many (between zero and 500 owned price cells)
  ownership: Price entries belong to the product and are replaced and cascade-deleted with it.
  cross_service: false
- to: DOM-ACC-PRD-004
  cardinality: one-to-zero-or-one
  ownership: The optional setup belongs to the product but uses separate write operations; product removal
    also removes it.
  cross_service: false
- to: DOM-ACC-PRD-001
  cardinality: many-to-many (no more than 50 components per bundle, with components reusable across bundles)
  ownership: The containing bundle owns the entry. Its component reference prevents deletion of the included
    product.
  cross_service: false
invariants:
- id: INV-DOM-ACC-PRD-001-01
  statement: Within a workspace, the product key is unique across every kind.
  enforced_at:
  - database
  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'
- id: INV-DOM-ACC-PRD-001-02
  statement: Product inserts and updates require readable-key syntax; the migration preserves older rows without
    validating their keys.
  enforced_at:
  - api
  - service
  - database
  provenance: test
  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
- id: INV-DOM-ACC-PRD-001-03
  statement: A saved product cannot change its key, kind or business unit.
  enforced_at:
  - service
  - database
  provenance: test
  source: services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
    a change of business_unit_id (RHF01)
- id: INV-DOM-ACC-PRD-001-04
  statement: The product's pricing model must equal the model registered for its kind.
  enforced_at:
  - service
  - database
  provenance: test
  source: services-accounting:modules/products/tests/integration/db/products.table.integration.spec.js#refuses
    a pricing model other than its type’s
- id: INV-DOM-ACC-PRD-001-05
  statement: Creation validates an active kind and a business unit in the current workspace.
  enforced_at:
  - service
  provenance: observed
  source: services-accounting:modules/products/services/persistence/products.ts:createProduct
- id: INV-DOM-ACC-PRD-001-06
  statement: Storage permits active, inactive and retired; supported writers enter and leave retirement through
    lifecycle actions.
  enforced_at:
  - api
  - service
  - database
  provenance: test
  source: services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
    replacing a retired product
- id: INV-DOM-ACC-PRD-001-07
  statement: Failure of the local product transaction preserves the previously committed product and owned
    collections.
  enforced_at:
  - service
  provenance: test
  source: services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#leaves
    the old prices and components when a new price is refused by the database after the old ones were removed
- id: INV-DOM-ACC-PRD-001-08
  statement: Service bundle validation checks distinct sellable components in the same unit and rejects cycles
    in its read graph without locking all participating products.
  enforced_at:
  - service
  provenance: observed
  source: services-accounting:modules/products/services/persistence/products.ts:assertProductRules, readBundleGraph
- id: INV-DOM-ACC-PRD-001-09
  statement: HTTP bundle validation multiplies allocations by quantity, while domain saving sums allocations
    without that multiplication.
  enforced_at:
  - service
  provenance: observed
  source: services-accounting:modules/products/services/rules/product-rules.ts:bundleTotalsProblem; services-accounting:db/migrations/20261019201047_product_lock_and_held_bundle_components.sql:accounting.save_product
- id: INV-DOM-ACC-PRD-001-10
  statement: An included component cannot be removed while a bundle references it.
  enforced_at:
  - service
  - database
  provenance: test
  source: services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#refuses
    deleting a product another product bundles, 409, and keeps it
- id: INV-DOM-ACC-PRD-001-11
  statement: Product validation refuses quantity bands that cover the same quantity.
  enforced_at:
  - service
  provenance: test
  source: services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#refuses
    overlapping bulk pricing ranges, naming the rows
- id: INV-DOM-ACC-PRD-001-12
  statement: Kinds that cannot be sold reject fees and selling settings; giver-priced kinds additionally reject
    price grids, discount bands and bundles.
  enforced_at:
  - service
  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
- id: INV-DOM-ACC-PRD-001-13
  statement: Service callers can write only their kind owner's products; console and system logic are exempt
    from owner matching, subject to applicable kind checks.
  enforced_at:
  - service
  - database
  provenance: test
  source: services-accounting:modules/products/tests/integration/services/persistence/products.shared-base.integration.spec.js#refuses
    another service replacing, retiring, reactivating or deleting the package, and lets its owner
- id: INV-DOM-ACC-PRD-001-14
  statement: The products HTTP boundary refuses custom-field input.
  enforced_at:
  - api
  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
- id: INV-DOM-ACC-PRD-001-15
  statement: Task types and queues must resolve in the workspace, with newly assigned references active.
  enforced_at:
  - service
  provenance: test
  source: services-accounting:modules/products/tests/integration/services/persistence/products.features.integration.spec.js#keeps
    a task type and a queue that became inactive while the product held them
- id: INV-DOM-ACC-PRD-001-16
  statement: Workspace-scoped reads and writes do not expose another workspace's product.
  enforced_at:
  - service
  - database
  provenance: test
  source: services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#answers
    another tenant’s product as absent and leaves it
physical:
- product_types
- products
- product_bundle_components
- product_bulk_pricing_rules
- product_task_templates
evidence:
- 'services-accounting at f47cb263: modules/products/db/products.table.ts, product-types.table.ts, product-bundle-components.table.ts,
  product-bulk-pricing-rules.table.ts, product-task-templates.table.ts; services/persistence/products.ts (createProduct,
  updateProduct, changeStatus, deleteProduct, lockProduct); services/rules/product-rules.ts (productOwnerProblem,
  bundle and feature rules); services/persistence/task-configurations.ts; metadata/products.object.ts; docs/accounting/products.md
  (Ids, Types, Products, Features, Custom fields).'
- 'services-accounting at f47cb263: tests/integration/db/products.table.integration.spec.js; tests/integration/services/persistence/products.integration.spec.js,
  products.features.integration.spec.js, products.shared-base.integration.spec.js; tests/e2e/products-shared-base.e2e.spec.js.'
- 'react-console at baf6dae3: lib/accounting/configuration/setup-types.ts (Product); components/accounting-config/products/use-products.ts.'
---

# Product

## What It Models

A product is the shared catalogue record used to identify an offer or an accounting reference. Accounting keeps the common fields for every registered kind; the application that owns a kind keeps its specialized extension. Prices and selling instructions belong to the common record, while finance maintains its accounting setup separately.

## Identity and Attributes

Creation fixes the readable identity, kind and business unit. Product reads combine stored settings with price and component lists, a financial summary and the resolved tax class. Ordinary HTTP saving replaces the supplied configuration and advances its version; confirmation is preserved when omitted.

## Relationships

A product owns its prices, selling collections and optional accounting setup. Bundle entries point to other products in the same business unit. Financial documents and domain extensions can also reference the product, so removing it depends on those references.

## Invariants

Enforcement varies by entry point. Table constraints keep identities and references consistent, while service validation checks collection totals and the bundle graph it reads. The product lock does not protect the entire graph from concurrent edits to different bundles.

## How It Is Stored and Shared

Products maintains the common tables and registers their standard fields as read-only metadata. No custom-field storage is declared. Other applications read the tables under workspace restrictions; domains save their common records through Accounting's database functions. Platform commits owned confirmation content independently. Products declares neither revision history nor change events.

## Open Design Questions

Product must settle omission and audience admission differences between the write paths, recovery after a remote template change, and the required audit trail. Quick status actions also need an implementation correction because their incomplete replacement drops hidden availability values.
