Repository navigation
feat(pharmacy-orders, facilities): item-level partial fulfillment + distance-sorted facility selection - #154
Conversation
…istance-sorted facility selection Closes DocForum#73: FulfillmentItem rows under a FulfillmentRecord carry dispensed quantity/status per routed order item; order status derives from items (partially_fulfilled/fulfilled) via forward-only conditional updates through the prescriptions/lab-orders services; patients can route remaining items to a second facility; a unique index on (orderType, dispensedKey) makes two-facility double-dispense a constraint-level 409 under concurrency. Payment-release rule documented in ARCHITECTURE.md §5.3: no release until the whole order fulfills. Closes DocForum#74: FacilityProfile gains address/latitude/longitude + composite index; GET /facilities/eligible?near=lat,lng sorts by bounding box + haversine (PostGIS justification in ARCHITECTURE.md §5.4); the patient's point is never stored; unlocated facilities stay listed, sorted last; fictional seeded coordinates; perf measured at ~5ms p50 over ~3000 facilities. Also fills in PrescriptionItem.prescriptionId and FulfillmentRecord.facilityId (both already documented in ARCHITECTURE §4, schema was behind) and updates ROADMAP/ARCHITECTURE_ESSENTIALS/docs/api. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
|
@Oluwaferanmi18 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
…ng-and-facility-location
|
CI is failing on this PR. Integration tests fail (10 of 53), including your new Please push a fix and CI will re-run. Before pushing, run |
|
CI fails in |
The merge in 536504e resolved app.ts, ROADMAP.md and docs/api/README.md toward main's side, silently dropping content from b888485: - /facilities and /pharmacy-orders router imports + mounts (this is what failed CI — every fulfillment route 404'd in the integration tests) - the 2026-10-17 Phase 5 changelog entry in ROADMAP.md - the Facilities and Pharmacy orders endpoint tables in docs/api/README.md 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
…ng-and-facility-location Brings in upstream DocForum#151 (Prisma 7.10.1 + driver adapter), DocForum#155 (patient data export), DocForum#150 (field-level encryption), DocForum#147 (access-control tests). Conflicts resolved in backend/src/app.ts (kept /facilities and /pharmacy-orders mounts alongside /patients) and docs/api/README.md (kept the new Facilities/Pharmacy-orders tables; facilities and pharmacy-orders come out of the "no routes yet" list since this branch gives them routes). 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
…ver adapters
Prisma 7's driver adapter sends bound params with an unspecified type
OID, so Postgres does the inference itself. The bounding-box predicate
`fp."latitude" BETWEEN ${lat} - ${boxLat} AND ...` was therefore
`unknown - unknown`, which Postgres rejects as ambiguous (SQLSTATE
42725) — every ?near+radiusKm query 500'd after the Prisma 7 upgrade.
The box bounds are now precomputed in JS (they never depended on row
data), and every numeric param that reaches the SQL carries an explicit
`::float8`. Result shape, ordering and index usage are unchanged;
facility-distance integration tests (incl. the perf measurement, p50
4.4ms over 3005 facilities) pass again.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Prisma 7 stopped reading `prisma.seed` from package.json (the seed command now lives in prisma.config.ts's migrations section) and `new PrismaClient()` without a driver adapter throws — the issue DocForum#74 dev seed hit both. Seed command moved to prisma.config.ts, seed.ts now builds the same PrismaPg adapter as src/db/prisma-client.ts. Verified end-to-end against a throwaway embedded Postgres: `npx prisma db seed` runs and is idempotent (8 fictional facilities, 11 capabilities, 6 with coordinates after two consecutive runs). 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
|
@Oluwaferanmi18 Thanks for the PR. It can't be merged yet. To unblock it: Merge conflicts: this branch conflicts with Push to this same branch and CI will re-run. Once it's green and conflict-free, it'll be reviewed for merge. |
…ng-and-facility-location
…ng-and-facility-location
|
@Oluwaferanmi18 CI is failing on this branch against the latest Backend (typecheck + test + build)
|
…ng-and-facility-location
…ng-and-facility-location
|
@Oluwaferanmi18 This PR now has merge conflicts with git fetch upstream # upstream = https://github.com/DocForum/docforum-core.git
git rebase upstream/main
# resolve the conflicts, re-run the tests, then:
git push --force-with-leaseThe usual culprits are shared docs like Automated message. It won't repeat unless the PR conflicts again after your next push. |
…ng-and-facility-location
|
@Oluwaferanmi18 CI is failing on this branch against the latest Backend (typecheck + test + build)
|
|
@Oluwaferanmi18 This PR now has merge conflicts with git fetch upstream # upstream = https://github.com/DocForum/docforum-core.git
git rebase upstream/main
# resolve the conflicts, re-run the tests, then:
git push --force-with-leaseThe usual culprits are shared docs like Automated message. It won't repeat unless the PR conflicts again after your next push. |
…ng-and-facility-location
|
@Oluwaferanmi18 CI is failing at
|
|
@Oluwaferanmi18 This PR now has merge conflicts with git fetch upstream # upstream = https://github.com/DocForum/docforum-core.git
git rebase upstream/main
# resolve the conflicts, re-run the tests, then:
git push --force-with-leaseThe usual culprits are shared docs like Automated message. It won't repeat unless the PR conflicts again after your next push. |
Closes #73
Closes #74
Two independently-scoped Phase 5 issues, landed together because they share the same schema migration and doc updates.
#73 — Partial fulfillment: item-level dispensing
What
FulfillmentItemtable: one row per routed order item under aFulfillmentRecord, withquantityDispensedandstatus(pending | dispensed | unfulfilled).fulfilled, some →partially_fulfilled(new enum values onOrderStatusandFulfillmentStatus). Applied post-commit bypharmacy-orders/servicesthrough the prescriptions/lab-orders module services (hard rules 4 and 5 — facilities never touchPrescription/LabOrderrows,pharmacy-ordersnever writes those tables directly), conditional and forward-only, so anexpired/supersededorder is never resurrected (integration-tested).POST /pharmacy-orders/fulfillmentswith an explicitorderItemIdscreates a second record for just the remaining items; routing an already-dispensed item →409.dispensedKey(=orderItemIdwhendispensed, NULL otherwise) under a plain unique index(orderType, dispensedKey)— NULLs don't collide in Postgres, so only two dispenses of the same item ever conflict. A literalWHERE status='dispensed'partial index would be equivalent SQL but Prisma can't represent it inschema.prismaandmigrate diffwould try to drop one it finds in the DB. Genuinely concurrent dispense attempts by two facilities are tested: exactly one200, one409.fulfilled. Proration is logged as an open question in §6.2 rather than silently decided.Interpretation flag for reviewers (issue asked to flag design questions): this builds on the current polymorphic
FulfillmentRecord— issue #13's keep-vs-split ADR is untouched and now needs to account forFulfillmentItem. Also, hard rule 1 ("never mutate aPrescriptionafter issuance") is read together with §5.3 step 5 and the existingOrderStatus.fulfilledvalue: clinical content stays append-only; the lifecyclestatusfield is the one sanctioned derived mutation. Both flagged in the schema and ARCHITECTURE §4.7.Known gaps (recorded in ARCHITECTURE §6.2.7, not hidden)
PatientProfile ↔ orderlinkage is Phase 4 (issue [Phase 4] Order models: Prescription + LabOrder (append-only) #9). The coarseGET /pharmacy-orders/orders/:id/fulfillment-statusexists so patients get an honest status now.quantityOrderedstaysnulluntil Phase 4's item models grow a quantity column — not faked as 0.#74 — Facility location + distance-sorted selection
What
FacilityProfilegains nullableaddress,latitude,longitude+ composite(latitude, longitude)index (migration).GET /facilities/eligible?capabilityType=…&near=lat,lng&radiusKm=…— indexed bounding box + haversine in SQL,ORDER BY distance ASC NULLS LAST. PostGIS rejected: it would require an extension on every deployment (CI throwaway DBs, Render free-tier preview, bare local PG) to order one column — justification + math in ARCHITECTURE.md §5.4. Box vs circle correctness: the box is a provable superset (equatorial km/degree constants), so the result is exact.nearis only ever a bind parameter — never stored, never logged, never echoed back (response carries per-facilitydistanceKmonly; test asserts the point doesn't appear in the payload).distanceKm: null, sorted last (tested, including inside a radius search, per the issue's edge case literally).backend/prisma/seed.ts(npx prisma db seed): 8 fictional facilities with Lagos-area coordinates,.testemails, 2 deliberately without coordinates — no real facilities' private data.Performance measurement (issue #74 DoD)
backend/tests/integration/facility-distance.test.mts, 3005 facilities, near+radiusKm=60, 25 runs: p50 5.2 ms, p95 7.1 ms, max 7.8 ms.EXPLAIN (ANALYZE, BUFFERS)against 5008 facilities:Bitmap Index Scan on "FacilityProfile_latitude_longitude_idx"(Index Cond on the latitude/longitude range), Execution Time: 5.101 ms, 180 shared buffer hits, no seq scan onFacilityProfile.Test output
New integration coverage (11 tests): split routing across two facilities with derivation at each step, concurrent double-dispense race (exactly one winner), one-shot item claims, hard-rule-5 cross-facility 403s, expired-order non-resurrection, lab-order single-item flow, distance ordering vs. a haversine reference, unlocated-facility placement, query-contract validation, and the perf measurement above.
Docs (per AGENTS.md)
ARCHITECTURE.md§4.1/§4.5/§4.7 (model changes incl. newFulfillmentItem), §5.3 (derivation + payment release rule), new §5.4 (distance query, PostGIS justification, privacy rule), §6.1.3 note, §6.2 open questions 6–7.ARCHITECTURE_ESSENTIALS.mddata map.ROADMAP.mdPhase 5 status (in progress — [Phase 5] ADR: resolve polymorphic FulfillmentRecord design smell #13–[Phase 5] Lab result attachment (object storage) #16 still owed) + two checked items + changelog entry.docs/api/README.md— new/facilitiesand/pharmacy-ordersendpoint tables.Also fills in two schema TODOs that ARCHITECTURE §4.5/§4.7 had already specified:
PrescriptionItem.prescriptionIdFK andFulfillmentRecord.facilityIdFK.