[Story-1080] Client delete guard parity

Story 1080Epic None13 files · +707/−19

PR #273 · pramaan-io/pramaan-functions · merged 2026-06-08

Why this matters

Deleting a client shouldn't orphan a matter.

What breaks without a guard

Hard delete orphans every matter that referenced this client.

The fix — Check the loans before pulling the book; mark withdrawn, never shred.

How it flows

sequenceDiagram
  autonumber
  participant partner as Partner
  participant service as Client Service
  participant matters as Matters Library
  participant catalog as Client Catalog
  partner->>service: DELETE /clients/{id}
  service->>matters: any open loans?
  matters->>service: zero loans
  service->>catalog: stamp withdrawn
  catalog->>catalog: status=deleted, row retained

app-api.json · +142 — new methods

Methodin → outwhat it does
_list_item(client, usage_count) → ClientListItem
_detail_view(client, usage_count) → ClientDetailView
_in_use_references(client_id, firm_id) → tuple[InUseReference, ...]

What changed, file by file

Service layer orchestrates soft-delete: delete_client guards the write, _in_use_references and _usage_count compute the matter signal, list views add canDelete.

new methodsignature
_list_item(client, usage_count) → ClientListItem
_detail_view(client, usage_count) → ClientDetailView
_in_use_references(client_id, firm_id) → tuple[InUseReference, ...]
_usage_count(db, client_id, firm_id) → int
delete_client(db, client_id) → None

Alembic migration adding client delete guard schema for the deleted lifecycle state and in-use indexing.

new methodsignature
upgrade() → None
downgrade() → None
New fields: revision : strdown_revision : str | Sequence[str] | Nonebranch_labels : str | Sequence[str] | Nonedepends_on : str | Sequence[str] | None

REST router gains guarded DELETE /app/clients/{client_id} that surfaces pramaan.client.in_use when matters block the delete.

new methodsignature
delete_client(client_id, db, _) → Response

Repository adds include_deleted flag so list queries hide soft-deleted rows from status=all and archived views.

New fields: include_deleted : bool

Client DTOs add usage_count, is_in_use, and can_delete fields so the UI renders guard-aware delete affordances.

New fields: usage_count : intis_in_use : boolcan_delete : bool

Files changed (13)

File±
alembic/versions/8f1d2e3c4b5a_0081_client_delete_guard.py ★+84
docs/walkthroughs/story-1080-walkthrough.html+98
openapi/app-api.json ★+142 −1
src/app_api/router_clients.py+17 −1
src/domains/client/AGENTS.md+25
src/domains/client/dtos/client.py+11 −1
src/domains/client/entity.py+5 −1
src/domains/client/repository.py+14 −4
src/domains/client/service.py ★+114 −8
src/shared/constants/firm_status.py+1
tests/contract/test_app_clients_openapi.py+59
tests/integration/test_app_clients.py+134 −3
tests/unit/test_dto_wire_format.py+3