document_types — firm-wide document type catalog (FN-DOCTYPES)
A litigation matter accumulates hundreds of files — contracts, affidavits,
orders, bank statements, photos. Counsel needs to organise the Documents
Register by what each file is. Without a controlled vocabulary,
"what is this document?" becomes free text: one lawyer types
Contract, another contract, a third
Agreement, and the register can never group, filter, or roll up
consistently. This Story gives the firm a single, governed
Document Type catalog — the same shape as Filing Types, but
with one deliberate divergence (below).
Document Type mirrors the structure of
business.filing_types (id, firm_id, name, description,
availability, created/archived) but is firm-wide, NOT scoped
per practice area. A document type — Contract, Bank statement,
Photo — is universal across practice areas, unlike a filing type (which is
procedural and lives inside a proceeding). Two further consequences of
"it is just a label, not a court-cited reference":
*_number allocated by
business.allocate_reference (rendered FT-001).
Document types do not — the PRD column list is the contract and it omits
any number.document_type_id is simply a nullable column.pramaan-functions)business + documents schemas)| Object | Change |
|---|---|
business.document_types | NEW firm-wide catalog: id, firm_id, name(128), description(512)?, availability('live'|'archived'|'deleted'), created_by_user_id?, archived_at?/archived_by_user_id?, deleted_at?/deleted_by_user_id?, created_at, updated_at |
| constraints / indexes | CHECK availability, UNIQUE(firm_id, id), partial-unique lower(name) WHERE availability='live', firm-leading lookup indexes |
| RLS | ENABLE + FORCE; firm-isolation SELECT, firm-write INSERT, firm-update UPDATE, staff-metadata SELECT; GRANT SELECT,INSERT,UPDATE TO pramaan_app; set_updated_at trigger |
documents.documents | + document_type_id uuid NULL FK → business.document_types.id (ondelete RESTRICT) + firm-leading index ix_documents_firm_document_type |
Downgrade drops the column/FK/index then the table, policies, grant, and
trigger. Roundtrip proven (upgrade head → downgrade base → upgrade
head) and autogenerate reports no drift against the entity.
src/domains/document_type/ entity.py DocumentType (SQLModel) — no number column dtos/ Create/Update/ListQuery + ListItem/DetailView/ListResponse repository.py RLS-scoped reads/writes via app.config_firm_id_or_null() service.py CRUD + visibility lifecycle + delete-guard + audit events AGENTS.md domain doctrine src/app_api/router_document_types.py /app/document-types CRUD (settings:write)
| Route | Behaviour |
|---|---|
GET /app/document-types | list live (or ?includeArchived=true) — never deleted; search + pagination |
POST /app/document-types | create; case-insensitive name conflict → 409 pramaan.document_type.name_conflict |
GET /app/document-types/{id} | detail with usageCount/isInUse/canDelete |
PATCH /app/document-types/{id} | rename/redescribe/availability transition |
POST …/archive · …/unarchive | visibility lifecycle (delegate to the update path) |
DELETE /app/document-types/{id} | soft-delete only when unused; in-use → 409 pramaan.document_type.in_use |
A document type is never hard-deleted. availability moves
live → archived → deleted; deleted rows are retained forever for
FK integrity and audit. DELETE is allowed only when no
documents.documents row references the type — otherwise the
backend rejects it and the user must archive instead. This is the
"archive is broken-by-construction if you cannot list archived rows" rule:
management lists honour includeArchived so an archived type can
always be restored.
No bespoke spans (pramaan-docs ADR-124): the CRUD path rides the FastAPI
auto-instrumentation floor (HTTP + DB spans). Every mutation emits a typed
settings-catalog audit event via emit_catalog_event(kind="document_type", …)
— created / edited / deleted, stamped
area=settings. The subject kind is registered in the audit
registry and the routes are listed in the route-coverage manifest, so the
coverage and flush-watchdog gates pass.
Integration tests run a fresh firm with an empty catalog through the
live router: create → duplicate-conflict → list → patch → archive → list
excludes archived → includeArchived shows it → unarchive →
pin a real documents.documents row to the type → in-use DELETE is
409 → unlink → DELETE 204 → detail 404 → audit-log shows the
document_type.* sequence. Plus cross-tenant RLS isolation,
settings:write permission enforcement, and unauthenticated 401.
Contract test asserts the OpenAPI surface (paths, schema titles, the
absence of any documentTypeNumber and of matter-type
scoping). make openapi snapshots committed (app-api only).
Document Type is Filing Type minus the practice-area scoping and minus the number — a firm-wide, archive-not-delete label catalog that the Documents Register groups by, with the document→type link landing now as a nullable FK and the AI fill deferred to EPIC-009.