---
id: STATE-ACC-PRD-002
title: Record lock
object: DOM-ACC-PRD-001
product_area: accounting
module: products
revision: 3
status: draft
design_intent: confirmed
origin: 'services-accounting:docs/milestones/ACCT-M1-books-and-invoices.md#D11 (Andrew, 2026-10-19: product
  lock rules approved as built)'
implementation_evidence: observed
source_sha:
  services-accounting: f47cb263875bbc0c5640fba7dae6785ec32695f8
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
states:
- name: unlocked
  meaning: Permissions and ordinary validation govern changes and deletion.
- name: delete-locked
  meaning: Deletion is prohibited, but otherwise permitted editing continues.
- name: update-and-delete-locked
  meaning: Updates and deletion are protected; supported product or setup replacement can explicitly reduce
    the lock.
transitions:
- from: unlocked
  to: delete-locked
  trigger: Specify deletion protection on creation or replacement.
  guard: The ordinary write checks must succeed.
  side_effects:
  - Create or advance the record version
  who: A caller authorized to save the record
  provenance: observed
  source: services-accounting:modules/products/aws/api/products-api-mapper.ts:toInput
- from: unlocked
  to: update-and-delete-locked
  trigger: Specify combined protection on creation or replacement.
  guard: The ordinary write checks must succeed.
  side_effects:
  - Create or advance the record version
  who: A caller authorized to save the record
  provenance: observed
  source: services-accounting:modules/products/aws/api/products-api-mapper.ts:toInput
- from: delete-locked
  to: unlocked
  trigger: Explicitly clear protection during replacement.
  guard: Replacement validation and the read-version check must succeed.
  side_effects:
  - Advance the record version
  who: A permitted record editor
  provenance: observed
  source: services-accounting:modules/products/services/rules/product-rules.ts:lockedReplaceProblem
- from: delete-locked
  to: update-and-delete-locked
  trigger: Replace a delete lock with combined protection.
  guard: Replacement validation and the read-version check must succeed.
  side_effects:
  - Advance the record version
  who: A permitted record editor
  provenance: observed
  source: services-accounting:modules/products/services/rules/product-rules.ts:lockedReplaceProblem
- from: update-and-delete-locked
  to: unlocked
  trigger: Replace a product or setup with an explicit empty lock.
  guard: The new lock must be stated; omission and a repeated combined lock fail, and kind defaults do not
    support this unlock path.
  side_effects:
  - Remove the protection
  - Commit the remaining replacement and version increase
  who: An editor holding product or setup update authority
  provenance: test
  source: services-accounting:modules/products/tests/integration/services/persistence/products.integration.spec.js#unlocks
    a locked product replaced with sys_locked null, then edits and deletes it
- from: update-and-delete-locked
  to: delete-locked
  trigger: Explicitly reduce a product or setup to deletion-only protection.
  guard: A changed lock must be supplied; a combined default lock refuses the write before reduction.
  side_effects:
  - Advance the record version
  who: An editor holding product or setup update authority
  provenance: observed
  source: services-accounting:modules/products/services/rules/product-rules.ts:lockedReplaceProblem
invalid:
- from: update-and-delete-locked
  to: update-and-delete-locked
  why: Replacement cannot silently omit or retain a combined lock while changing its protected record.
- from: delete-locked
  to: unlocked
  why: Omitting a lock does not unlock; explicitly clearing it may do so.
concurrency: Products and setups use the shared product lock with their separate versions. A kind default locks
  its own row, independently of product mutations.
invariants: []
evidence:
- 'services-accounting at f47cb263: modules/products/services/rules/product-rules.ts (lockedReplaceProblem,
  lockedChangeProblem, lockedDeleteProblem); aws/api/products-api-mapper.ts; services/persistence/products.ts,
  product-accounting-setups.ts, product-type-tax-classes.ts; docs/accounting/products.md (Locks); docs/milestones/ACCT-M1-books-and-invoices.md
  (D11).'
- 'services-accounting at f47cb263: tests/integration/services/persistence/products.integration.spec.js, product-accounting-setups.integration.spec.js,
  products.shared-base.integration.spec.js.'
---

# Record lock

## The Lifecycle

```mermaid
stateDiagram-v2
  [*] --> unlocked
  unlocked --> delete_locked: request deletion protection
  unlocked --> update_and_delete_locked: request combined protection
  delete_locked --> unlocked: explicitly clear protection
  delete_locked --> update_and_delete_locked: strengthen protection
  update_and_delete_locked --> unlocked: explicit release on product or setup
  update_and_delete_locked --> delete_locked: explicit reduction on product or setup
```

## Transitions Explained

Protection is stored independently on the product, setup and kind tax default. It supplements permissions: deletion-only protection leaves editing available, while combined protection also blocks updates. The cited milestone confirms product lock behavior. A product or setup editor can explicitly reduce combined protection during replacement; defaults use different release checks.

## What Cannot Happen

Omitting protection does not request an unlock. Combined product or setup protection must be explicitly reduced for replacement to proceed. A combined lock on a kind default prevents replacement even when the request tries to clear it. A setup's deletion lock also blocks deleting its product.

## Concurrency and Timing

Product and setup protection changes use their common product lock and their own record versions. Kind-default changes use the default row and version independently.
