Inventory Homogenization
One movement spine for every consumable: streaming, device cache, AI rules, exploration, consume-to-charge billing; 10 invariants.
⚠️ Correction (2026-06-14, audit): Any statement here that
billable_ledger“has zero writers” or thatcreateFromLedger“doesn’t exist” is stale. BothwriteGhostLedgerLinesandsalesOrders/createFromLedgerare shipped (salesOrder.controller.mixin.ts:725-857, commits5ecb96fe7/a79b451a9, 2026-06-09). The accurate state: the writers exist but are inert —billable_ledgeris empty in prod because the order producer is mis-keyed (medicationCode ≠ sku) and the stock producer is unstamped (noencounterId). Seerevenue-loop-flow-audit.md§1 + broken-links #1/#2. Original content below is unchanged.
Status: Design + verified live-state audit; P0–P3 SHIPPED + live-verified 2026-06-10 (see §9 table) Date: 2026-06-10 Companion docs:
realtime-inventory-supply-chain.md(the streaming substrate master plan, shipped 2026-05-25),menu-price-ledger.md(pricing substrate),universal-facility-stay-billing.md(facility-stay rules engine),emr-cowork-substrate.md(AI coworker invariants),operational-facts-erp-hrm-activation.md(ERP connectors)Read this before any inventory / stock / supply-chain / consume-to-charge / supply-AI work. This doc is the homogenization layer: it does not redesign the streaming substrate — it ties the five currently-disconnected planes (stream → device cache → rules/AI → exploration → billing) into one contract.
1. Verified Live State (audited 2026-06-10 against the shared PH Supabase + EC2)
The question “are we streaming inventory to a cache on devices, medical supplies included?” has a precise answer: the pipeline is built and deployed, but it is dry. Every layer exists; two small operational faults stop the flow.
1.1 What is SHIPPED and LIVE
| Layer | Component | State (verified) |
|---|---|---|
| Write source | foundation service: stockItem, stockBatch, stockBalance, stockTransaction (13 movement types incl. GOOD_RECEIVE / STOCK_ISSUE / STOCK_TRANSFER / STOCK_CONSUMPTION / DISPOSAL / STOCK_RESERVE), stockRequest, stockReturn, pharmacyPR/pharmacyPO |
✅ live on EC2, full WAC costing per transaction |
| Event emit | stockEvent.emitter.ts → Moleculer stock.* events; StockEventBridgeMixin wired into foundationService.ts:398 → inserts hospital_events rows (STOCK_BALANCE_CHANGED, STOCK_TRANSACTION_CREATED, STOCK_ITEM_*, STOCK_BATCH_EXPIRING) |
✅ code deployed — ❌ silently no-ops (see 1.2) |
| Read model | stock_balance_cache, stock_alerts, stock_movement_log, stock_item_catalog (migration 20260525b) — all four in supabase_realtime publication |
✅ tables exist live — ❌ 0 rows each |
| Orchestration | inventory-orchestrator edge fn (ACTIVE, v2), sync-inventory-cache edge fn (ACTIVE, v2, reconcile/full/archive modes) |
✅ deployed — ❌ never fed / never scheduled |
| Frontend | web/packages/inventory-kit: /inventory-dashboard, /inventory/stock-monitor, /inventory/hub, /inventory (18 deep-link tabs), /medical-supplies-work-list, /inventory/ai-receive, /inventory/ai-dispense; hooks useStockBalanceRealtime, useStockAlerts, useStockMovementFeed, useStockAvailability, useStockItemSearch |
✅ shipped, realtime subscriptions correct — render empty because tables are empty |
| Pricing substrate | menu_items (34,205 rows, projected 2026-06-10), resolve_menu_price(p_sku, …) RPC (answers, returns unresolved for unknown SKU), billable_ledger table |
✅ live in DB — ❌ billable_ledger has ZERO writers (0 rows); ⚠️ no repo migration file creates these objects (schema drift — applied via SQL editor; 20260605b only references them in comments) |
| Billing close | close-encounter-billing edge fn (ACTIVE, v3, redeployed 2026-06-10) |
⚠️ skeleton — reads billable_ledger correctly, POST createFromLedger + billClosed stamping are explicit TODOs; NestJS endpoint doesn’t exist |
Medical supplies are covered by the same spine — not a separate system. Product.type distinguishes
DRUG | MEDICAL_SUPPLY | CONSU | DEVICE | BLOOD_PRODUCT | … (25+ types); stock_item_catalog.category projects
drug | medical_supply | consumable | equipment | reagent | other. One pipeline, one classification axis.
1.2 Why the stream is dry — two root causes (both trivial)
- EC2 env:
env-files/ever/.envon the PH box defines only ONE ofSUPABASE_URL/SUPABASE_KEY(verified:grep -c= 1).StockEventBridgeMixin.getSupabaseClient()requires both and returnsnullotherwise → every stock event is dropped at debug level (“Supabase not configured”). ZeroSTOCK_*rows have ever landed inhospital_events. - Cron never seeded: migration
20260525c_inventory_cron_seed.sqlwas never applied —inventory_cache_configdoesn’t exist live andcron_jobshas no inventory entry. So the dailysync-inventory-cachereconcile (which would have backfilled the cache from MongoDB even without events) never ran once.
The P0 fix is operational, not code: add the missing env var + restart foundation, apply 20260525c,
invoke sync-inventory-cache once with {"mode":"full"}. The four dashboards light up with no frontend change.
1.3 What is genuinely MISSING (design gaps, not faults)
| Gap | Detail |
|---|---|
| No persistent device cache | Frontend realtime state is in-memory only. No IndexedDB/localStorage persistence for catalog or balances; service-worker background-sync funcs are unimplemented TODOs. Cold boot = full refetch; offline = blank. |
| Consume ≠ charge | Stock decrement and charge capture are fully decoupled. Charges are created at order-acknowledgement from frontend-supplied OrderRequestItem.unitPrice (defaults to 0!). Dispense (rx.dispensed) only flips queue status. Nothing writes billable_ledger. Supplies issued to wards via STOCK_ISSUE produce no charge at all. |
| No rules engine | Reorder min/max fields exist on StockItem (Odoo-synced) but nothing evaluates them; STOCK_REORDER_TRIGGERED event type exists with no producer. No FEFO enforcement, no par levels, no ABC classification, no substitution rules. |
| Parallel silos off the spine | Blood bank (blood_bag_locations, blood_inventory_movements, blood_dispense_events — no realtime publication, no charge hook), nutrition (nutritionPurchaseOrder/nutritionReceiving), compounding bulk stock, OR implant/supply lines (or_case_costing.supply_line_items[], manual) each track movement in their own shape. |
| No exploration surface | stock_movement_log is the perfect audit substrate (actor, WAC before/after, patient HN, ref no) but has no explorer UI, no anomaly detection, no link from HDAP. |
| No demand intelligence | No forecast, no usage-rate projection, no stockout prediction. |
menu_items.financial_product_id is NULL on sampled rows — the projection carries SKU+price but not the Mongo product linkage, so joining ledger↔chargemaster↔stock item is not yet possible. |
2. The Homogenization Contract — one spine, five planes
┌─ PLANE 4: EXPLORATION ─────────────────────────┐
│ HDAP → Inventory Explorer (movement log, │
│ anomaly feed, shrinkage/diversion detection) │
└────────────────▲───────────────────────────────┘
│ reads
┌─ WRITE SOURCES ─────────┐ ┌─ PLANE 1: ONE MOVEMENT SPINE ─────────────────┐
│ pharmacy dispense │ │ hospital_events (STOCK_*) │
│ ward supply issue ├──▶│ → inventory-orchestrator │
│ blood dispense (adapter) │ │ → stock_balance_cache / stock_movement_log │
│ nutrition (adapter) │ │ stock_alerts / stock_item_catalog │
│ compounding (adapter) │ └──────┬──────────────────┬─────────────────────┘
│ OR implants (adapter) │ │ realtime │ change feed
└──────────────────────────┘ ▼ ▼
┌─ PLANE 2: DEVICE CACHE ─┐ ┌─ PLANE 3: RULES + AI ──────────┐
│ inventory-kit hooks │ │ inventory_rules (deterministic │
│ + IndexedDB persistence │ │ policy_gates-shape JSONB) │
│ + offline op queue │ │ + supply-chain AI coworker │
└──────────────────────────┘ │ (recommender-only, │
│ cowork_proposals) │
└──────────────┬─────────────────┘
│ approved actions
┌─ PLANE 5: BILLING ────────────────▼─────────────────┐
│ consume = charge: │
│ STOCK_ISSUE/dispense → resolve_menu_price() │
│ → billable_ledger (append-only) │
│ → encounter_journey_cache.financial_summary │
│ → close-encounter-billing → SalesOrder │
│ + leakage view: movements WITHOUT charges │
└──────────────────────────────────────────────────────┘
The homogenizing idea: every consumable domain — drugs, medical supplies, blood products, nutrition,
compounding ingredients, OR implants — speaks the SAME event vocabulary (STOCK_*), lands in the SAME four
read-model tables, is priced by the SAME resolve_menu_price(), charges into the SAME billable_ledger,
and is governed by the SAME rule-row pattern (scope_json / predicate_json / action_json / priority)
already proven by policy_gates, cds_rules, facility_billing_rule, and detection_rules.
Domain differences are data rows and thin adapters, never forks.
3. Plane 1 — Movement Spine (mostly shipped; adapters needed)
3.1 Canonical movement event (already defined, keep as-is)
STOCK_TRANSACTION_CREATED payload (from stockEvent.emitter.ts) is the canonical shape. Every adapter
must produce it — including the fields that make billing and audit possible downstream:
itemId/itemCode/itemName, locationId, toLocationId?, quantity, beforeQty, stockOnHand, unitCost, totalCost, wacBefore, wacAfter, batchId?, batchNumber?, userId, userDisplayName, departmentName?, patientHn?, refNo?.
Additive field (new, optional): encounterId + chargeIntent. Today stock events carry patientHn but no
encounter linkage, which is why consume→charge can’t be wired. Adapters that know the encounter MUST stamp it:
// addition to StockTransactionEventData (additive, optional)
encounterId?: string; // Mongo encounter _id when movement is encounter-scoped
chargeIntent?: 'billable' | 'non_billable' | 'estimate'; // default: derived from product binding
3.2 Domain adapters (thin, data-preserving — never rewrite the domain)
| Domain | Adapter | Effort |
|---|---|---|
| Pharmacy dispense | orderRequestProductDispense HANDED_OVER → emit STOCK_TRANSACTION_CREATED (movement_type: stock_issue, with encounterId). The cart flow already calls foundation.stockTransaction.createWithStockAdjustment (medicationCart.controller.mixin.ts:82) — the emitter fires there for free once the bridge env is fixed. The non-cart dispense path needs the same call added. |
S |
| Ward supply issue | Already STOCK_ISSUE through stockTransaction — nothing to do beyond P0. |
— |
| Blood bank | New thin projector: blood_dispense_events INSERT trigger → synthesize STOCK_TRANSACTION_CREATED row in hospital_events (category: blood_product, bag IDs in refNo). Blood keeps its own tables (chain-of-custody invariants stay); the spine gets a copy of the movement, same as specimen-transport keeps its own context. |
S |
| Nutrition | nutritionReceiving → good_receive; kitchen consumption → stock_consumption. |
S |
| Compounding | Ingredient consumption per worksheet → stock_consumption with refNo = worksheet id (the compounding doc already names inventory-orchestrator for bulk stock — this is that hook). |
M |
| OR implants/supplies | When or_case_costing.status → posted, emit one stock_consumption per supply_line_items[]/implant_line_items[] row (alongside the already-designed ledger trigger trg_or_case_costing_sync_ledger). |
M |
Invariant: one funnel. No adapter writes stock_balance_cache/stock_movement_log directly — only
inventory-orchestrator writes the read model (mirrors the “single dispatch funnel” rule of department_queues).
4. Plane 2 — Device Cache (the actual “streaming to devices” answer)
Today: realtime → in-memory React state. Good for connected desktops; useless for cold boot, tablets on ward Wi-Fi dead zones, and the (designed) edge-AI boxes.
4.1 Three-tier cache, additive to existing hooks
- Tier 1 — in-memory realtime (exists): keep
useStockBalanceRealtimemap-based upsert as-is. - Tier 2 — persistent snapshot (new): IndexedDB persistence inside
inventory-kit(NOT the dead service worker —service-worker.tsis generateSW dead code per the Vercel cache fix; don’t rewire it).stock_item_catalog(34k-row chargemaster-adjacent catalog) → full snapshot, refreshed byupdated_at > last_snapshotdelta query on boot, then realtime deltas. This makes item search/autocomplete instant and offline-capable.stock_balance_cachefor the user’suserStoreInventory-authorized locations only (scoped, small).- Implementation: a
createPersistedRealtimeStore(table, scopeFilter)helper inweb/packages/inventory-kit/src/cache/wrappingidb-keyval/localforage — boot: hydrate from IndexedDB → render → delta-fetch → subscribe. Stale data renders with the existingisLive-style badge (Patient 360 pattern).
- Tier 3 — offline op queue (new, later): stock receipts/issues/adjustments queued in IndexedDB when
offline, flushed through the normal REST endpoints on reconnect with client-generated
adjustNofor idempotency. Conflict policy: server WAC always wins; queued op replays as a new transaction (append-only), never a balance overwrite.
4.2 REPLICA IDENTITY
Set REPLICA IDENTITY FULL on the four cache tables (one-line migration) so realtime UPDATE payloads carry
full rows — required for the map-upsert pattern to stay correct on partial updates.
5. Plane 3 — Rules + AI (deterministic gates, recommender AI)
5.1 inventory_rules — same shape as every other rule engine in the platform
One table, the proven five-column pattern:
CREATE TABLE inventory_rules (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
rule_kind text NOT NULL, -- 'reorder' | 'par_level' | 'expiry_window' | 'fefo'
-- | 'substitution' | 'abc_class' | 'consumption_anomaly'
-- | 'controlled_substance_watch' | 'auto_charge'
scope_json jsonb NOT NULL DEFAULT '{}', -- {item_id?|category?|location_id?|store_type?|country?}
predicate_json jsonb NOT NULL DEFAULT '{}', -- templated tokens, cds_rules-safe grammar — NOT raw SQL
action_json jsonb NOT NULL DEFAULT '{}', -- {alert_severity, suggest_reorder_qty?, create_ack?,
-- draft_pr?, block_dispense?:false, charge_sku?}
priority int NOT NULL DEFAULT 100,
is_active boolean NOT NULL DEFAULT false, -- OFF by default, like everything else
country_code text DEFAULT '*',
created_by uuid, notes text,
created_at timestamptz DEFAULT now(), updated_at timestamptz DEFAULT now()
);
- Evaluator lives in
inventory-orchestrator(it already computes stock_status and writes dedupedstock_alerts— the rules table replaces its hardcoded thresholds, exactly howpolicy_gatesreplaced hardcoded payment gates). Effective-rule resolution: most-specific scope wins, then priority — same resolution semantics asward_admission_policies/facility_billing_rule. - Rule kinds are templated, not freeform (the
encounter_watch_ruleslesson): each kind has a typed predicate schema, so the admin UI is a form, not a JSONB editor. - Admin UI:
/admin/inventory-rulescloned fromCdsRulesPage; seed library shipsdraft/inactive (reorder-at-min for all categories, 90/30/7-day expiry ladder, FEFO suggestion on batch-tracked items, controlled-substance variance watch). - Country/market differences = seed rows in
infrastructure/market-packs/medos-{country}/seed-inventory-rules.sql(TH NHSO consignment patterns, JP lot-trace strictness, PH multi-branch min/max) — zero TypeScript per country. - Hard rule: rules never block clinical dispense by default.
block_dispenseexists for controlled substances only and requires an explicitpolicy_gatesrow (hard-stop + override-with-reason perhospital-movement-architecture.md) — the inventory engine suggests, the policy-gate engine gates.
5.2 AI layer — supply-chain coworker on the shipped cowork substrate
All AI is recommender-first per emr-cowork-substrate.md (off by default, kill switch, accept/edit/reject
= the training label, llm_audit_log provenance). Register one cowork_agents row: supply-chain-coworker,
plane = Operational (no PHI needed — item/location/quantity series only; strip patientHn from anything
sent to a model).
| Capability | Mechanism | Writes |
|---|---|---|
| Reorder proposals | Nightly run over stock_balance_cache + 90-day stock_movement_log usage rate → propose PR drafts (item, qty, vendor from StockItem.vendors) |
cowork_proposals → Inbox Accept creates pharmacyPR via existing REST. Never auto-submits a PO. |
| Demand forecast | Deterministic first: moving-average + day-of-week seasonality as a twin_metric_supply_* RPC in the existing METRIC_SOURCES registry (HORUS twin calibration, per operational-facts-erp-hrm-activation.md). LLM narrative on top, display-only. |
stock_alerts row alert_type='reorder_suggested' with suggested_reorder_qty (column already exists) |
| Consumption anomaly / shrinkage detection | Deterministic consumption_anomaly rules (z-score vs item-location baseline) catch diversion patterns — controlled substances especially (RUDS-adjacent: high-value items walking out, after-hours adjustments, repeated small negative adjustments by one user). LLM second-opinion ranks, never suppresses (the e-Kardex AI stance: re-rank only). |
stock_alerts severity≤warning + AcknowledgementRequest to pharmacy lead |
| AI receive / AI dispense (routes exist) | Document-intelligence path: photo/PDF of delivery note → SpreadsheetIngestPanel-style extraction → prefilled GOOD_RECEIVE draft; human confirms every line (draft-until-signed, scribe pattern). |
Nothing until human confirm |
| Charge-leakage recommender | See §7.3 — flags movements with no matching ledger row. | proposal rows |
Invariants (inherit, don’t reinvent): AI cannot create stock transactions, cannot auto-approve PRs/POs, cannot change prices, cannot suppress alerts; every inference audited; per-tenant feature flag.
6. Plane 4 — Logs & Data Exploration
stock_movement_log is already the right substrate (append-only, actor, costing, before/after, patient link).
Three additions:
- Inventory Explorer at HDAP (
/admin/hdap→ Monitor group, beside Connector Activity): movement stream with the Supabase-Logs-style panel pattern (ConnectorActivityPanelprecedent) — filter by item/location/type/user/date, drill to balance history, export CSV. Saved views per user. - Retention split: hot log stays 14 days in
stock_movement_log(existing archive logic); addstock_movement_archive(monthly-partitioned, or fold into the medallion Bronze layer perai-training-corpus.md) so exploration and forecasting have ≥13 months of history. The sync fn’sarchive_onlymode gets a destination instead of deleting. - Ask-the-data: wire the existing
DataAnalysisPanel(local Ollama RAG, cited sources) to an inventory corpus — read-only RPCs over movement archive + balances (top movers,expiry exposure by location,usage vs forecast). Same safety contract as the unified-clinical-assistant READ tool class: no gate, no writes, no raw SQL from the model — only whitelisted RPCs.
7. Plane 5 — Billing Linkage (consume = charge)
7.1 The chain to wire (most pieces verified live)
movement (STOCK_ISSUE / dispense HANDED_OVER / blood dispense / OR posted / compounding consume)
→ inventory-orchestrator handler [exists, extend]
→ if chargeIntent='billable' AND encounterId present:
resolve_menu_price(sku, payor, scheme, contract) [LIVE — verified responding]
→ INSERT billable_ledger (append-only) [table LIVE — zero writers today]
→ encounter_journey_cache.financial_summary [trg_sync_billing_queue exists]
→ close-encounter-billing → POST /v2/financial/salesOrders/createFromLedger [skeleton + missing endpoint]
This completes menu-price-ledger.md M4 for the consumable categories — it does not replace order-time
charging for services/procedures. Drug/supply charges move from “charge at acknowledgement with
frontend-supplied unitPrice (often 0)” to “charge at consumption with backend-resolved price.” During
transition, both write; the ledger row carries metadata.source='consume' and the reconciliation view
(7.3) shows divergence before cutover (the M2 shadow-mode discipline).
7.2 Catalog homogenization — the one mapping table that makes it work
Three product identities exist today: foundation.StockItem (logistics), financial.Product (chargemaster,
stockItemRef link exists), menu_items (Supabase pricing projection, financial_product_id currently
NULL). The keystone fix:
- Backfill
menu_items.financial_product_idfrom the projection source, and addstock_item_catalog.menu_item_sku(projected viaProduct.stockItemRefjoin) so the orchestrator can gomongo_item_id → sku → resolve_menu_price()in one lookup. - Items with no product binding (pure logistics stock, e.g. cleaning supplies) resolve to
non_billable— no-silent-zero invariant: a billable-intent movement that resolvesunresolvedwrites a ledger row withqty×0+needs_pricing=trueflag and raises astock_alertsrow, rather than silently dropping the charge. - ⚠️
menu_items/billable_ledger/resolve_menu_pricehave no repo migration (live-only objects, created 2026-06-10, likely by a concurrent session). Before building on them: snapshot the live DDL into a checked-in migration (pg_dump --schema-onlyof the three objects) so the repo regains source-of-truth. Do not drop or recreate — they may be another session’s in-flight work.
7.3 Leakage reconciliation — the immediate revenue win
One view, huge demo value:
CREATE VIEW inventory_charge_leakage_v AS
SELECT m.* FROM stock_movement_log m
LEFT JOIN billable_ledger b ON b.metadata->>'movement_id' = m.mongo_transaction_id
WHERE m.movement_type IN ('stock_issue','stock_consumption')
AND m.patient_hn IS NOT NULL
AND b.id IS NULL;
Surfaced as a cashier/finance panel (“consumed but never charged”) + the AI coworker proposes the missing
charges as cowork_proposals. This is the homogenized answer to “supplies charged at 0฿” — detection first,
auto-capture after trust is established.
8. Invariants
- One funnel — only
inventory-orchestratorwrites the stock read model; adapters emit events. - Append-only —
stock_movement_logandbillable_ledgernever UPDATE quantities; corrections are new rows (void+repost). - Consume = charge, but never block care — pricing failures alert, they never stop a dispense.
- No-silent-zero — unresolvable price ⇒ flagged ledger row + alert, never a dropped charge.
- Rules are data — country/site differences are rule rows + market-pack seeds; zero per-country code.
- AI is recommender-only — no autonomous stock writes, PO submissions, price changes, or alert suppression; accept/edit/reject is the training label.
- PHI-safe AI — item/location/quantity series only;
patientHnstripped from any model payload. - Fail-soft frontend — every surface renders from device cache with staleness badge when the stream is down.
- Off by default — rules ship
is_active=false; AI behind per-tenant flag; default config = today’s behavior. - Schema drift is debt — every live-DB object must have a checked-in migration before code depends on it.
9. Rollout
| Phase | Scope | Effort | Outcome |
|---|---|---|---|
| P0 — turn the stream on ✅ SHIPPED 2026-06-10 | StockEventBridgeMixin accepts SUPABASE_SERVICE_ROLE_KEY (the var actually on EC2); 20260525c fixed (nested $$ bug — why it never applied) + applied, cron reads Vault secrets (project_url/service_role_key — hosted Supabase denies ALTER DATABASE SET); 20260610b REPLICA IDENTITY applied; fixed stockBalance.repository empty-$and Mongo BadValue (unfiltered lists 500’d); sync rewritten page-streamed + batch upserts + background mode (was per-row → WORKER_RESOURCE_LIMIT; sync HTTP cap 150s → 502); 20260610g plain unique index on mongo_stock_balance_id + fn_inventory_upsert_alerts RPC (PostgREST can’t target the COALESCE expression index nor the partial dedup index — every balance/alert upsert had been failing). Live-verified: 3,892 balances / 20,237 catalog items / 3,309 alerts; synthetic STOCK_BALANCE_CHANGED → orchestrator → cache+alert E2E pass. |
done | All four inventory dashboards live with real data, drugs AND supplies |
| P1 — catalog homogenization ✅ SHIPPED 2026-06-10 | 20260610c snapshots live menu_items/billable_ledger/resolve_menu_price DDL into the repo (drift closed); financial_product_id was already backfilled by the concurrent menu-items-sync session (34,194/34,205); item→SKU join = menu_items.metadata->>'stockItemRef' stamped by toMenuItemRow (re-ran POST /v2/financial/products/syncMenuItems: 45,000 projected, 9,712 stock-backed items carry the join key); encounterId/chargeIntent added to stock event payload + orchestrator passthrough |
done | Ledger writers possible; repo = source of truth again |
| P2 — consume→charge ✅ SHIPPED 2026-06-10 | 20260610e trg_stock_movement_post_ledger (fail-soft, idempotent on origin_ref, no-silent-zero, HN-fallback encounter lookup with 14-day recency bound + metadata.encounter_source audit flag) + inventory_charge_leakage_v. Scheme/payor enrichment from insurance_context + the finance panel UI = follow-up |
done (panel UI open) | Supplies stop leaking revenue; auditable charge trail |
| P3 — rules engine ✅ SHIPPED 2026-06-10 | 20260610f inventory_rules (+resolve_inventory_rule() SQL, realtime, open-write RLS like cds_rules) + 7-rule draft seed library; shared TS evaluator functions/_shared/inventory-rules.ts wired into BOTH orchestrator (60s-TTL cache) and sync (fail-open: no active rules = today’s behavior); admin UI /admin/inventory-rules (InventoryRulesPage + inventoryRules.service). Market-pack seeds + typed per-kind predicate forms = follow-up |
done (seeds/forms open) | Reorder/expiry/FEFO/par-level as live-editable data |
| P4 — device cache | createPersistedRealtimeStore (IndexedDB) for catalog + scoped balances; staleness badge; offline op queue last |
M | True cache-on-device, instant search, offline-tolerant |
| P5 — AI coworker + exploration | supply-chain-coworker agent (reorder proposals, anomaly re-rank, AI-receive extraction); HDAP Inventory Explorer + movement archive + ask-the-data RPCs |
L | AI-integrated supply chain, fully audited |
| P6 — silo adapters | Blood / nutrition / compounding / OR-implant event adapters onto the spine | M | Everything consumable on one movement + charge spine |
P0 is independent and should happen immediately. P1–P2 complete the menu-price-ledger M4 path for consumables. P3–P6 are independently shippable after P1.
10. Open questions
- Who owns the live
menu_itemsprojection? Created 2026-06-10 03:54 UTC with no repo migration — confirm whether a concurrent session/branch carries the migration before snapshotting DDL (consolidate-all-pockets rule). - Cutover policy for drug charges — at HANDED_OVER (consumption) vs current acknowledgement-time SalesOrder: needs finance sign-off per market (NHSO claims reference order-time in some flows).
- Controlled-substance hard gates — which jurisdictions require blocking dispense on count variance
(vs alert-only)? Determines which
policy_gatesrows ship per market pack. - Movement archive home — dedicated partitioned table vs the medallion Bronze layer (
ai-training-corpus.md). Leaning Bronze if the forecast work lands in the twin; dedicated table if finance needs 7-year retention first.