Kulcare Docs
DeveloperInventory

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.

UnitScopeCurrent evidence
U1Schema, models, scoped identities, and database guardsCommitted locally as e108df529a; undeployed
U2Effective permissions and facility accessCommitted locally as 7c58a77ef8; scoped CodeRabbit review: zero findings; undeployed
U3Receipts, transfers, corrections, and expiry validationCommitted locally as b33ed340ce; scoped CodeRabbit final pass: zero findings; undeployed
U4Reservations, consumption, unused returns, and concurrencyCommitted locally as d8da3e12c9; scoped CodeRabbit final pass: zero findings; undeployed
U5Source HTTP APIs and authorizationCommitted locally as 6876c737d0; scoped CodeRabbit final pass: zero findings; undeployed
U6Reconciliation, opening stock, and delivery evidenceCommitted 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.

MethodPathPurpose
GET / POST/itemsRead or create stock items
PATCH/items/:idUpdate item configuration with its expected version
GET / POST/items/:id/conversionsRead or append conversion versions
GET / POST/locationsRead or create facility stock locations
PATCH/locations/:idUpdate a location with its expected version
GET/balancesRead physical, held, free, and allocatable quantities
GET / POST/operationsRead history or confirm a supported stock command
GET / POST/reservationsRead holds or reserve explicit allocations
GET/reservations/:id/allocationsRead a hold's paginated allocations
POST/reservations/:id/releaseRelease an explicit remaining hold
GET/custody_allocationsRead remaining stock by original issue lineage
POST/consumptionsConsume explicit stock allocations
POST/unused_returnsReturn 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.

GateResult
Inventory, RBAC, and Shell permission-save regressionPass: 464 tests, 2,304 assertions; no skips
Inventory on the fresh-structure databasePass: 123 tests, 1,204 assertions; no skips
Hospital service and request regressionPass: 110 tests, 655 assertions; no skips
Inventory Ruby lintPass: 91 files; no offenses
Structure version and migration parity checksPass
Migration and fresh-schema parityPass: 442 definitions, including all six Inventory function bodies
Auth-identity packagePass: 273 tests and typecheck
FumadocsPass: typecheck, static build, and search index
Minitest-only CI guardFail: 20 existing RSpec files outside Inventory
Changed-file CI lintFail: seven offenses in earlier Hospital demo-seeder code/tests, outside the Inventory commits
Identity RLS auditFail: three unclassified tables and four existing policy-expression drifts
Integration RLS auditFail: missing RLS enable, force, and policy on four ABDM monthly partitions
Full domain regressionBlocked during discovery by a missing legacy basic-auth test credential
Full frontend/MFA CINot run; affected auth-identity package only
Complete PR review, deployment, and operator acceptanceOpen

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 requirementPrimary evidence under backend/domains/inventory/test
R1-R2: identities, exact quantities, and source ownershipmodels/stock_identity_test.rb, db_constraints/stock_scope_test.rb
R3-R4: no overdraft and preserved corrections/custodyservices/physical_commands_test.rb, services/internal_consumption_test.rb, services/stock_concurrency_test.rb
R5: retries, rollback, and unknown outcomesservices/operation_replay_test.rb, services/pilot_controls_test.rb
R6: tenant, facility, and action authorizationmodels/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 APIsrequests/stock_api_test.rb
R8: source transaction boundaryCaller 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 controlsservices/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.

RecommendationRequired behaviorDelivery status
Separate Shell grant areasKeep 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 serialA 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 lineageTrack 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 outcomesReuse 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 accessGrant the non-bypass test role schema and table access before tenant-isolation checks.Implemented in U1
Keep quarantine and write-off availableExpired 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 validationValidate 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:

CorrectionImplementation rule
Conversion evidenceA 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 quantityA 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 rollbackLock 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.

DataTables
Configuration and identityitems, item_conversions, locations, lots, serials
Confirmed historyoperations, movement_entries, lot_expiry_validations, reservation_events
Mutable projectionsbalances, 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 areaView keyManage key
Inventory (inventory)view_inventorymanage_inventory
Inventory consumption (inventory_consumption)Nonemanage_inventory_consumption
Inventory adjustments (inventory_adjustments)Nonemanage_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.

On this page