---
id: STATE-ACC-PRD-001
title: Product lifecycle
object: DOM-ACC-PRD-001
product_area: accounting
module: products
revision: 3
status: draft
design_intent: proposed
implementation_evidence: observed
source_sha:
  services-accounting: f47cb263875bbc0c5640fba7dae6785ec32695f8
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
states:
- name: active
  meaning: The record is available for new use and can be corrected.
- name: inactive
  meaning: The offer is withdrawn while edits and existing financial references remain available.
- name: retired
  meaning: The retained record cannot be ordinarily replaced until reactivated.
- name: deleted
  meaning: The local aggregate has been removed; reusing its key requires a new creation.
transitions:
- from: active
  to: inactive
  trigger: Save inactive status by replacing at the product's read version.
  guard: Product and owner validation must pass, the key must satisfy current syntax, and combined protection
    must be explicitly reduced.
  side_effects:
  - Advance product version and modification stamps
  - Rewrite the replacement's owned lists
  - Call Platform if confirmation changes
  who: A product editor or the owning application's supported writer
  provenance: test
  source: services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#answers
    the replaced record at version 2, the cleared fields left out and both collections replaced
- from: inactive
  to: active
  trigger: Save active status by replacement or versioned reactivation.
  guard: Replacement passes normal validation; reactivation requires inactive status, a matching version and
    no combined lock.
  side_effects:
  - Advance version and modification stamps
  - Replacement can rewrite settings and lists; reactivation retains them
  who: A permitted editor or owning service at a supported entry point
  provenance: test
  source: services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#reactivates
    an inactive product
- from: active
  to: retired
  trigger: Retire at the current read version through HTTP or the domain function.
  guard: The kind owner must admit the caller, the version must match, and combined protection must be absent.
  side_effects:
  - Set retired status
  - Advance version and stamps without replacing configuration
  who: An authorized editor or owning domain caller
  provenance: test
  source: services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#retires
    the product at version 2, changing nothing else
- from: inactive
  to: retired
  trigger: Retire an inactive product using the version read.
  guard: Owner and version checks pass and updates are not locked.
  side_effects:
  - Advance version and modification stamps
  who: An authorized editor or owning domain caller
  provenance: observed
  source: services-accounting:modules/products/services/rules/product-rules.ts:retireProblem
- from: retired
  to: active
  trigger: Reactivate a retired product at the version read.
  guard: The caller passes owner checks and the version matches, without combined protection.
  side_effects:
  - Set active status
  - Advance version and modification stamps
  who: A caller with product update authority
  provenance: test
  source: services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#reactivates
    the retired product at version 3
- from: active
  to: deleted
  trigger: Delete the current product without a version precondition.
  guard: Owner checking and reference constraints permit removal, and neither product nor setup prevents deletion.
  side_effects:
  - Cascade through local prices, components, selling lists, setup children and handovers
  - After commit, attempt remote cleanup and log a failure
  who: A caller with product delete authority
  provenance: test
  source: services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#takes
    its prices, components, accounting setup and the setup’s collections with it
- from: inactive
  to: deleted
  trigger: Delete an inactive product.
  guard: Apply the same owner, protection and dependency checks as active deletion.
  side_effects:
  - Cascade-delete the owned local records
  - Attempt owned-template clearing after commit
  who: A permitted product deleter
  provenance: observed
  source: services-accounting:modules/products/services/persistence/products.ts:deleteProduct
- from: retired
  to: deleted
  trigger: Delete a retired product.
  guard: Retirement adds no delete refusal; owner, reference and protection checks still apply.
  side_effects:
  - Cascade-delete the owned local records
  - Attempt owned-template clearing after commit
  who: A permitted product deleter
  provenance: observed
  source: services-accounting:modules/products/services/persistence/products.ts:deleteProduct
invalid:
- from: retired
  to: inactive
  why: Ordinary replacement cannot change a retired product; reactivation must precede an inactive replacement.
- from: retired
  to: retired
  why: The retire operation refuses an already retired product.
- from: active
  to: active
  why: Reactivation alone is refused for an active product; ordinary active replacement is still allowed.
- from: deleted
  to: active
  why: Deletion has no restore command; creating under the old key produces a new record.
concurrency: The product row lock serializes same-product local mutations. Product replacement and lifecycle
  changes require its version; deletion checks the locked current state without a version. Setup changes maintain
  an independent setup version.
invariants:
- INV-DOM-ACC-PRD-001-06
- INV-DOM-ACC-PRD-001-10
- INV-DOM-ACC-PRD-001-13
evidence:
- 'services-accounting at f47cb263: modules/products/services/persistence/products.ts (updateProduct, retireProduct,
  reactivateProduct, changeStatus, deleteProduct); services/rules/product-rules.ts (retireProblem, reactivateProblem,
  productUpdateProblem); docs/accounting/products.md (Lifecycle); tests/integration/services/persistence/products.integration.spec.js.'
- 'react-console at baf6dae3: components/accounting-config/products/product-row-actions.tsx; use-products.ts
  (productStatusWrite).'
---

# Product lifecycle

## The Lifecycle

```mermaid
stateDiagram-v2
  [*] --> active: create normally
  [*] --> inactive: create withdrawn
  active --> inactive: replace
  inactive --> active: replace or reactivate
  active --> retired: retire
  inactive --> retired: retire
  retired --> active: reactivate
  active --> deleted: delete when permitted
  inactive --> deleted: delete when permitted
  retired --> deleted: delete when permitted
  deleted --> [*]
```

## Transitions Explained

A product starts active unless creation requests inactive status. Replacement can withdraw or restore it; retirement uses a dedicated versioned action and blocks ordinary corrections until reactivation. Deletion removes the local aggregate and then attempts to clear any owned Platform template. Older generated identities remain readable and deletable, but product updates fail the identity constraint.

## What Cannot Happen

Creation and ordinary replacement cannot request retirement. Repeating retirement on a retired product or reactivation on an active product is refused. Locks and dependencies can prevent deletion regardless of status. Reusing a deleted identity creates a new record rather than restoring the removed one.

## Concurrency and Timing

Replacement and lifecycle actions compare the version read by the caller while holding the product lock. Deletion locks and checks the current record without a version precondition. List activation and deactivation use replacement and can clear availability values they fail to resend.
