Inventory Developer Guide
Accepted ledger decisions, foundation safeguards, permission rules, and source delivery limits.
Scope And Current Status
Inventory is a reusable backend domain. It is not a separate MFA app. Hospital is the first UI consumer. Hospital owns clinical links; Inventory owns stock identities and stock history. Shell owns permission management. Admin owns feature flag management.
Use these source documents:
The unit numbers below belong to the Inventory plan. They do not renumber the Hospital plan.
| Unit | Scope | Current evidence |
|---|---|---|
| U1 | Schema, models, scoped identities, and database guards | Committed locally as e108df529a; undeployed |
| U2 | Effective permissions and facility access | Committed locally as 7c58a77ef8; scoped CodeRabbit review: zero findings; undeployed |
| U3 | Receipts, transfers, corrections, and expiry validation | Committed locally as b33ed340ce; scoped CodeRabbit final pass: zero findings; undeployed |
| U4 | Reservations, consumption, unused returns, and concurrency | Committed locally as d8da3e12c9; scoped CodeRabbit final pass: zero findings; undeployed |
| U5 | Source HTTP APIs and authorization | Committed locally as 6876c737d0; scoped CodeRabbit final pass: zero findings; undeployed |
| U6 | Reconciliation, opening stock, and delivery evidence | Committed locally as b9d714482b; scoped CodeRabbit final pass: zero findings; undeployed |
Hospital U10 now connects the case and inventory UI locally after source delivery. Deployment and clinical, Inventory, and QA acceptance remain open. Use the release acceptance record for current release gates. A passing schema test does not prove a working stock command, API, or Hospital workflow.
Stock Commands And Expiry Approval
U3 adds Inventory::CommandContext.call. The source service joins a caller transaction through a savepoint.
It does not commit independently of Clinical. No HTTP stock routes are provided by U3.
The physical stock QA guide lists manual checks.
Every command takes a facility-scoped operation key and explicit input. Quantity values must be decimal strings. An identical authorized retry returns the original operation. A changed payload cannot reuse its key. Do not change the key after an unavailable lookup or an unknown commit outcome. Receipt and opening source rows also have independent uniqueness. A new key cannot duplicate a source row. Opening rows require an explicit count date. Adjustments require a count reference and reason code.
Commands serialize through a transaction-scoped advisory lock per tenant/facility. This is a conservative correctness choice, not a high-throughput claim. They lock the current facility and referenced resources before quantity checks and writes. U5 configuration HTTP writes use the same lock. Do not add direct stock projection writes. Required audit evidence must persist in the same transaction. A failed audit write rolls back the command. Responses distinguish a confirmed operation, verified rollback, and an unknown outcome. The review found that deterministic database rejections were reported as unknown outcomes. Regression tests now distinguish those rollbacks from connection loss and PostgreSQL uncertain transaction errors. The retry test also restores inherited transaction dispatch after its failure simulation.
U1-U3 focused verification passed 408 tests and 1,940 assertions without failures, errors, or skips. Hospital service/request regression passed 110 tests and 655 assertions. The migration passed forward, unused rollback, reapply, and empty-database structure loading. CodeRabbit reviewed 22 scoped files. Its final pass had no findings after three retained findings were fixed. Full project CI, operator QA, deployment, and Hospital integration remain separate gates.
Migration 20261011000003_add_inventory_expiry_approvals.rb adds scoped approval evidence.
The approval record is immutable except for revocation. It uses ENABLE/FORCE tenant RLS and a scoped validation foreign key.
No rule or approval is seeded. A designated facility inventory or pharmacy lead must approve label interpretation.
An authorized backend operator records the approved decision with Inventory::ExpiryApproval.approve!.
That actor needs View, Manage, and adjustments. There is no public approval route in this delivery.
The supported rule names are period_start, next_period_start, and explicit_instant.
They describe calculation methods, not an assumed clinical shelf-life policy.
The approved precision, version, and facility timezone must match the validation request.
The server derives the cutoff. A browser cannot supply approval authority, timezone, or cutoff.
A missing or invalid facility timezone fails validation. A timezone change requires new approval evidence.
Expiry validation appends a revision and changes eligibility, not physical quantity or existing holds.
Initial retry permissions use the original action evidence, even after a later correction.
Internal Holds And Custody
U4 adds reserve, release, issue, consume, unused return, and service-only expiry commands.
Each hold allocation can name a custody allocation. The database checks item, bucket, and facility identity together.
custody_events is immutable history. Custody projections keep the original issue link through transfers.
An unnamed command cannot take issued stock. A named command cannot take another hold's units.
Issue moves stock and its matching hold. It does not consume stock or create a billing charge.
Unused return releases only its matching remaining hold. Damaged, expired, and unresolved-expiry returns enter quarantine.
Consumption reversal restores physical stock and custody, not an old reservation.
Issue reversal compensates its remaining stock and active hold together. Later use can block reversal.
Multi-line consumption reversal must name one original movement entry.
Reservation versions protect held commands. Multiple lines in one command use the same starting version.
Retries require current grants and return one original operation. A caller rollback removes all inner effects.
The scheduled sweep uses explicit tenant and facility job arguments. It records the service actor inventory_reservation_expiry.
No HTTP caller can select that service actor. The job is a cleanup path, not the expiry eligibility authority.
Deployment must install the updated Whenever schedule and run the default Sidekiq queue.
Use the U4 manual QA guide.
U4 schema parity passed for 420 column, constraint, index, trigger, policy, and RLS definitions. U1-U4, RBAC, Shell-save, and queue verification passed 434 tests and 2,217 assertions without failures, errors, or skips. Forward migration, unused rollback/reapply, populated rollback refusal, and fresh structure loading passed. Nine independent-connection race tests passed under the non-bypass RLS role. The final CodeRabbit pass reviewed 31 files with no findings. The first pass found that an elapsed-time exception could also admit a terminal reservation. A failing regression proved the defect. The fix preserves terminal states and only permits elapsed time for an open hold. After a hold expires, return free unused custody without naming that old hold allocation.
Source HTTP Boundary
U5 exposes facility-scoped routes at /api/inventory/facilities/:facility_id.
Every request needs tenant membership, active facility access, and View. Each write also checks its command grant.
The route facility is authoritative; a different facility in the login session cannot replace it.
Query filters use the actual query string, not legacy controller-injected user or audit parameters.
Each read route has an explicit filter list. Unsupported filters fail instead of returning a wider result.
All list reads have stable pagination and a maximum page size of 100.
All paths below are relative to /api/inventory/facilities/:facility_id.
| Method | Path | Purpose |
|---|---|---|
| GET / POST | /items | Read or create stock items |
| PATCH | /items/:id | Update item configuration with its expected version |
| GET / POST | /items/:id/conversions | Read or append conversion versions |
| GET / POST | /locations | Read or create facility stock locations |
| PATCH | /locations/:id | Update a location with its expected version |
| GET | /balances | Read physical, held, free, and allocatable quantities |
| GET / POST | /operations | Read history or confirm a supported stock command |
| GET / POST | /reservations | Read holds or reserve explicit allocations |
| GET | /reservations/:id/allocations | Read a hold's paginated allocations |
| POST | /reservations/:id/release | Release an explicit remaining hold |
| GET | /custody_allocations | Read remaining stock by original issue lineage |
| POST | /consumptions | Consume explicit stock allocations |
| POST | /unused_returns | Return only unused issued stock |
Mutations take operation_key and an input object. POST operations also takes kind.
Do not repeat a member-route ID inside input. The server supplies authenticated actor and scope.
Configuration success returns its resource in data.effects.resource. Stock success returns the operation and its recorded effects.
Balance version and lot_version are strings. Untracked balances expose lot_version: null.
Read a fresh lot_version for an unresolved lot before initial expiry validation and again before correction.
The original operation's evidence.lot_version is historical. A retry cannot replace a current read.
The API regression first failed because the balance response omitted this required lot token.
It now uses only the read token and proves current versions after correction, original retry evidence, and null for untracked stock.
Configuration writes use the same stock lock and idempotency boundary as stock commands.
Item and location codes allow at most 120 characters. Their names allow 240 characters.
An item's base unit allows 40 characters. Create and update requests enforce these limits before storage.
Oversized fields and database check rejections return VALIDATION_FAILED with HTTP 422 and a rolled-back outcome.
Connection loss still returns an unavailable command with an unknown outcome. Preserve its operation key.
The whole-PR review found the field-limit gap. Request tests cover create, update, exact limits, and database rejections.
Versions protect updates. Stock history prevents ordinary tracking-rule changes.
Physical or held stock prevents deactivation. Conversions append versions instead of rewriting them.
inventory.operation_results stores immutable post-command snapshots with tenant RLS and scoped operation identity.
Retries return those original snapshots. Refresh balances to obtain a current update version.
Older operations without a stored snapshot report effect_snapshot_status: unavailable.
Do not construct a historical result from current projections.
The U5 QA guide lists source API checks.
No source response adds patient, case, medication-order, or billing identities.
U5 verification passed 448 tests and 2,180 assertions, plus 110 Hospital tests and 655 assertions. There were no failures, errors, or skips. Schema parity passed for 436 definitions. Forward, unused rollback/reapply, fresh schema loading, and populated rollback refusal passed. CodeRabbit reviewed 37 scoped files with zero findings. Code review: harness-native fallback. The CE full review requires independent leaf agents, but repository rules require main-thread execution. The approved CodeRabbit review provides external coverage; no completed CE review is claimed.
Reconciliation And Pilot Controls
U6 adds read-only reconciliation, immutable opening-count approvals, an atomic retry-safe opening importer, and write-stop controls. The pilot operations guide contains operator commands, evidence requirements, and rollback limits. The U6 QA guide contains manual acceptance steps. Approval needs View, Manage, and adjustments. Import needs View and Manage. Every action also needs facility access. Opening commands must exactly match an approved manifest row. Direct API calls cannot bypass the approval. Reconciliation reports drift and missing audit evidence. It does not repair projections or create stock. The write stop preserves reads and authorized retries, but prevents all new Inventory effects, including service expiry. Full CI is not green. The gate table below records the broader run's failures. Operator acceptance and deployment remain open. Hospital U10 is implemented locally.
U1-U6 regression with the lot-version API correction passed 464 tests and 2,304 assertions without failures, errors, or skips. Ten independent-connection races ran under the non-bypass RLS role. Hospital regression passed 110 tests and 655 assertions. All 91 Inventory Ruby files passed scoped lint. U6 migration forward, unused rollback/reapply, fresh schema load, populated rollback refusal, and 436-definition parity passed. CodeRabbit reviewed 19 scoped U6 files with zero findings. The separate two-file lot-version API follow-up also completed with zero findings after the service rate limit reset. Code review: harness-native fallback. The CE full review requires independent leaf agents that repository rules prohibit. Source implementation is complete locally. Release and consumer acceptance remain open.
Broader Verification Status
This section records the earlier U6 run. It is not the current whole-PR result. The release acceptance record separates current automated checks from staging and human approvals.
The backend CI command used a disposable database and PARALLEL_WORKERS=4.
It exited with failure. Its full regression step failed during test discovery, before a valid aggregate result.
Do not report that the parallel domain suite passed.
| Gate | Result |
|---|---|
| Inventory, RBAC, and Shell permission-save regression | Pass: 464 tests, 2,304 assertions; no skips |
| Inventory on the fresh-structure database | Pass: 123 tests, 1,204 assertions; no skips |
| Hospital service and request regression | Pass: 110 tests, 655 assertions; no skips |
| Inventory Ruby lint | Pass: 91 files; no offenses |
| Structure version and migration parity checks | Pass |
| Migration and fresh-schema parity | Pass: 442 definitions, including all six Inventory function bodies |
| Auth-identity package | Pass: 273 tests and typecheck |
| Fumadocs | Pass: typecheck, static build, and search index |
| Minitest-only CI guard | Fail: 20 existing RSpec files outside Inventory |
| Changed-file CI lint | Fail: seven offenses in earlier Hospital demo-seeder code/tests, outside the Inventory commits |
| Identity RLS audit | Fail: three unclassified tables and four existing policy-expression drifts |
| Integration RLS audit | Fail: missing RLS enable, force, and policy on four ABDM monthly partitions |
| Full domain regression | Blocked during discovery by a missing legacy basic-auth test credential |
| Full frontend/MFA CI | Not run; affected auth-identity package only |
| Complete PR review, deployment, and operator acceptance | Open |
CI database preparation removed the disposable database's RLS test-role grants. The first later regression run failed or skipped those checks. It is not passing security evidence. Reprovisioning the non-bypass role and rerunning produced the passing result above. Do not change assertions or grant bypass privileges to conceal this setup failure.
Requirement Evidence
| Source requirement | Primary evidence under backend/domains/inventory/test |
|---|---|
| R1-R2: identities, exact quantities, and source ownership | models/stock_identity_test.rb, db_constraints/stock_scope_test.rb |
| R3-R4: no overdraft and preserved corrections/custody | services/physical_commands_test.rb, services/internal_consumption_test.rb, services/stock_concurrency_test.rb |
| R5: retries, rollback, and unknown outcomes | services/operation_replay_test.rb, services/pilot_controls_test.rb |
| R6: tenant, facility, and action authorization | models/ledger_rls_test.rb, policies/stock_policy_test.rb, policies/inventory_policy_matrix_test.rb, services/facility_access_test.rb; Identity RBAC tables |
| R7: usable source APIs | requests/stock_api_test.rb |
| R8: source transaction boundary | Caller rollback tests in services/internal_consumption_test.rb; Clinical service/request hospital/case_consumption_test.rb and frontend live-case-stock.test.tsx cover the local U10 consumer |
| R9: reconciliation and pilot controls | services/pilot_controls_test.rb; manual approval and release evidence remain open |
Each of the six units has a QA guide. An unchecked manual step is not an automated test result. Contracts and plans remain repository documents; this Fumadocs guide and the QA pages summarize their source delivery evidence.
Accepted Recommendations
The user accepted all retained review recommendations on 2026-10-11. Apply them within the accepted ledger scope. Do not request the same product decisions again. Acceptance is not implementation evidence or permission to skip tests, review, or QA.
| Recommendation | Required behavior | Delivery status |
|---|---|---|
| Separate Shell grant areas | Keep stock management, consumption, and adjustments separate. Every write also needs View. | U2 policy, catalog, and Shell-save tests pass; scoped review has zero findings |
| One physical unit per serial | A serial has at most one positive-stock bucket across locations and conditions. Lock the serial during stock changes. | Database guards and independent-connection command races pass |
| Preserve issue lineage | Track unused custody by original issue line. Later stock must not make an earlier consumed issue returnable. | U4 custody commands, compensation, return, and transfer races pass |
| Preserve unknown commit outcomes | Reuse the original operation key after an uncertain commit. Report no effect only after a verified rollback. | U3-U6 command, API, and import recovery tests implemented |
| Provision Inventory test access | Grant the non-bypass test role schema and table access before tenant-isolation checks. | Implemented in U1 |
| Keep quarantine and write-off available | Expired stock cannot be allocated, issued for use, or consumed. Audited release, quarantine return, and write-off remain available. | U3-U4 command, race, and API tests pass |
| Add lot expiry validation | Validate unresolved expiry or correct a prior validation without changing stock quantity. Verify approval on the server. | U3-U5 approval, command, API, and reconciliation tests implemented |
The following Unit 1 corrections are also retained:
| Correction | Implementation rule |
|---|---|
| Conversion evidence | A movement has either no conversion ID and no version, or both. Its scoped foreign key must match the conversion ID and version together. |
| Reservation-event quantity | A header event has no allocation or quantity. An allocation event has both and obeys the item's base-unit increment, countable, and serial rules. |
| Safe unused-schema rollback | Lock operations and balances before the history check. Check across tenants under the migration transaction. Refuse rollback when operation history or positive/held stock exists. A refused rollback must restore FORCE RLS. |
The conversion and rollback defects were found through local regression tests. CodeRabbit found the reservation-event defect. Its second scoped pass reported no findings after the fix. These are Unit 1 results, not a review of the unimplemented units.
Foundation Rules
Migration 20261011000001_create_inventory_ledger_foundation.rb creates 13 tables under inventory.
Models live under backend/domains/inventory/models and inherit from Inventory::BaseRecord.
| Data | Tables |
|---|---|
| Configuration and identity | items, item_conversions, locations, lots, serials |
| Confirmed history | operations, movement_entries, lot_expiry_validations, reservation_events |
| Mutable projections | balances, reservations, reservation_allocations, custody_allocations |
Scope And Quantity
Every table has tenant/facility identity, scoped relationships, and ENABLE/FORCE tenant RLS. Composite foreign keys reject mixed facilities within one tenant. RLS does not grant facility access. U2 checks membership and active facility assignment separately. Do not add a platform-admin stock bypass.
Store quantities as exact numeric values. Reject excess precision instead of rounding it on storage.
The foundation allows at most six decimal places and absolute values below 100000000000000.
Countable quantities must be whole units. Other quantities must match the configured base-unit increment.
Serial stock represents one physical unit, not one unit in each location.
Balances must satisfy on_hand >= reserved >= 0. There is no negative-stock override.
Inventory::Balance#free_quantity is arithmetic only. It is not an allocation-eligibility check.
Expiry, condition, custody, and other command rules still apply before stock can be used.
History And Expiry
Confirmed operations, movement entries, conversion versions, expiry revisions, and reservation events reject updates, deletes, and truncation. Corrections must append evidence rather than rewrite confirmed history. Base units and tracking rules cannot change after stock or operation history exists.
An expiry revision preserves the source label and precision, interpretation rule/version, approval reference, timezone, cutoff, actor, reason, and prior revision. Current and prior revisions must belong to the same tenant, facility, and lot. An unresolved lot has no inferred cutoff. Do not treat unresolved expiry as available stock.
U1 stores an approval reference. It does not verify facility approval or derive a medically approved cutoff. Test factories use test-only approval evidence. Do not copy that evidence into a pilot seed or production approval workflow. U3 verifies approval on the server before confirming a validation.
The rollback check protects recorded history. It is not a production recovery procedure. Use a forward migration or disable writes when a delivered ledger needs correction.
Unit 2 Permission Contract
Use Shared::BasePolicy, current effective permissions, and active facility-access rules.
Do not authorize Inventory through Hospital controller inheritance or role-name checks.
Default grants come from the permission catalog, not policy shortcuts for owners or admins.
| Shell area | View key | Manage key |
|---|---|---|
Inventory (inventory) | view_inventory | manage_inventory |
Inventory consumption (inventory_consumption) | None | manage_inventory_consumption |
Inventory adjustments (inventory_adjustments) | None | manage_inventory_adjustments |
All reads require view_inventory. All writes require it plus the command's mutation key.
Owner/admin defaults grant all four keys. Staff and practitioners need explicit Shell grants.
Defaults do not bypass tenant, facility, stock, or command checks.
Reversal needs the original command permission and manage_inventory_adjustments.
Initial expiry validation needs View and stock management. A correction also needs adjustments.
Replay checks current grants against the original action, not the lot's later state.
For consumption-only access, save Inventory as View, Inventory consumption as Manage, and Inventory adjustments as None. This must allow consumption but deny receipt, count correction, and write-off. Test both role-level and member-level saves through existing Shell access-level services. An inactive assignment or revoked membership must deny access, including operation replay.
Add the four keys with real policy actions in the same change. Update the area registry, access table, defaults table, key-behavior map, defaults snapshot, and frontend permission constants together. Each key belongs to exactly one area and level. Name the inserting migration in the defaults snapshot. Do not seed an unwired key or add an Inventory RBAC editor.
Inventory::FacilityAccess checks current active membership from the database, not an ambient membership alone.
It applies the existing owner/admin tenant-facility rule and current staff/practitioner assignments.
Inventory::StockPolicy requires effective action keys after facility access passes.
Replay checks stored expiry_correction evidence for expiry operations. Missing evidence denies access.
Literal permission checks remain visible to the RBAC catalog scanner. The Inventory policy matrix covers all source actions.
The four keys are also exported as PERMISSIONS.INVENTORY by @kulcare/auth-identity.
Hospital U10 now connects inventory buttons through case-scoped commands. The source policy still checks each stock effect.
Use the U2 manual QA guide before accepting this unit.
Verification And Release Gates
Use a disposable test database, not development, staging, or production data. Follow the foundation QA setup. After a schema snapshot change, prepare the test database and provision the RLS role again. A schema reload can remove grants. Security checks must run without skips. Missing table grants do not prove tenant isolation.
Keep migrations authoritative and structure.sql additive.
Verify both migration application and a fresh schema load. Do not commit a regenerated dump that changes unrelated objects.
Equivalent PostgreSQL deparser output is not a schema mismatch, but compare the actual constraints and trigger definitions.
Recorded U1 evidence: 27 tests, 335 assertions, no failures, errors, or skips on both disposable databases. Scoped Ruby lint passed. The second CodeRabbit review reported no findings. The full CE review did not finish under the main-thread-only rule; CodeRabbit was the approved fallback. The Hospital/RBAC regression run had four role-check catalog failures for unchanged Connect and staff paths. The broad backend run produced no valid aggregate. Backend CI is not confirmed green.
At the end of each unit, add manual QA checkboxes and automated evidence to the issue or PR. Keep issue #2517 open for QA. Do not equate a completed implementation unit with release acceptance. Stock commands, source APIs, Hospital inventory, pharmacy readiness, MAR stock integration, and package billing remain outside U1 delivery.