Skip to content

feat(pharmacy-orders, facilities): clean recovery of Phase 5 partial fulfillment + facility distance sorting - #257

Open
Oluwaferanmi18 wants to merge 14 commits into
DocForum:mainfrom
Oluwaferanmi18:ci-fix
Open

Oluwaferanmi18 wants to merge 14 commits into
DocForum:mainfrom
Oluwaferanmi18:ci-fix

Conversation

@Oluwaferanmi18

Copy link
Copy Markdown

Closes #73
Closes #74

What this is

A clean recovery of the Phase 5 implementation for #73 (item-level partial fulfillment) and #74 (facility location + distance-sorted selection), rebuilt on top of current main (8712c70).

Disclosure: #154 is the original PR for these two issues and remains open and untouched — this work is submitted as a separate PR rather than force-pushing that contributor's branch. #154 is referenced here purely for context: its description, review notes and history are the source material this branch recovers. Nothing here closes, edits or replaces #154.

Why a new branch was needed

By its head 57ce53c, #154's five successive merges with main had resolved conflicts toward main's side and silently deleted the branch's own work, while the tests, migration SQL and validation schemas referencing that work survived:

Dropped In merge
model FulfillmentItem + FulfillmentRecord.items e613309
/pharmacy-orders/fulfillments* routes + controllers 57ce53c (the PR head)
distance query in facilities/repositories (near/radiusKm) bf30314

CI's latest run on #154 (run 544) therefore failed at npm run typecheck — TS2300 duplicate pharmacyOrdersRouter/facilitiesRouter in src/app.ts, prisma.fulfillmentItem missing, near not accepted — with unit tests, integration tests and build all skipped.

This branch redoes the merge once, cleanly, from ac86f2a (the last commit that still contained every piece, including the Prisma 7 fixes) against main, resolving each conflict as the union of both sides rather than either side wholesale.

Two collisions that only running the suite exposed

  1. Duplicate migration DDL. The Phase 5 migration and main's later migrations both added PrescriptionItem.prescriptionId and FulfillmentRecord.facilityId with their indexes/FKs, so prisma migrate deploy failed with 42701 in all 33 integration files. The Phase 5 migration now creates only what it owns: the two partially_fulfilled enum values, the facility location columns + index, and the FulfillmentItem table.
  2. ADR 0012's one-record-per-order UNIQUE vs. split routing. [Phase 5] Partial fulfillment: item-level dispensing for multi-item prescriptions #73 requires routing an order's remaining items to a second facility as a new record, which that UNIQUE forbids (the split scenario returned 409 where the issue requires a 201). Uniqueness moved down one level to (order FK, facilityId) — the guarantee the ADR was actually buying — and the order relation becomes one-to-many (fulfillments[]). Recorded as a dated amendment in docs/adr/0012, ARCHITECTURE.md §4.7 and ARCHITECTURE_ESSENTIALS.md, plus migration 20261026000000_fulfillment_record_per_order_facility.

Consequence reviewers should check: record.facilityId is now the facility the patient routed to, which for a split order is not the order's own facility_profile_id.

GET /facilities/eligible contract decision

The endpoint doubles as main's public price-comparison query, so it keeps main's contract: unauthenticated, { facilities: [...] } envelope. #74's capabilityType/detail/near/radiusKm/limit params and address/distanceKm fields are added inside it. A query with neither type nor capabilityType is a 400 (fail closed — no unqualified directory), radiusKm without near is 400, and the patient's point is never stored, logged or echoed back. #74's test was updated to assert this shared contract.

Test results (backend/, exact CI steps)

New coverage (11 tests): split routing across two facilities with status derivation at each step, concurrent double-dispense race (exactly one 200, one 409), one-shot item claims, hard-rule-5 cross-facility 403s, expired-order non-resurrection, lab-order single-item flow, distance ordering against a haversine reference, unlocated-facility placement, query-contract validation, and the perf measurement below.

Issue #74 DoD (perf): 3005 facilities, near + radiusKm=60, 25 runs → p50 6.8 ms, p95 7.9 ms, max 8.5 ms (test ceiling 500 ms).

Docs (per AGENTS.md)

ARCHITECTURE.md §4.5/§4.6/§4.7/§5.3/§5.4/§6.1, ARCHITECTURE_ESSENTIALS.md data map, ROADMAP.md Phase 5 status + two checked items, docs/adr/0012 amendment, docs/api/README.md endpoint tables, and changelog.d/73-74-partial-fulfillment-facility-distance.md (a new file, not an append to ROADMAP.md, per changelog.d/README.md).

Oluwaferanmi18 and others added 8 commits October 17, 2026 07:01
…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>
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>
…ranch

The five earlier merges with main resolved conflicts toward main's side and
silently dropped this branch's work: the `FulfillmentItem` model (e613309),
the `/pharmacy-orders/fulfillments*` routes (57ce53c) and the distance query
in facilities/repositories (bf30314) were all gone by PR head — which is why
CI failed at `npm run typecheck` (duplicate `pharmacyOrdersRouter`/
`facilitiesRouter` identifiers in app.ts, plus tests referencing
`prisma.fulfillmentItem` and `near`).

This redoes the merge as one clean three-way merge from ac86f2a (the last
commit that still contained every piece) and resolves each conflict as the
union of both sides, re-seated onto what main changed underneath:

- schema.prisma: main's typed-order `FulfillmentRecord` (ADR 0012) keeps the
  feature's `items FulfillmentItem[]` relation; `FacilityProfile` gains the
  issue DocForum#74 location columns *and* main's relations + `@@index([latitude,
  longitude])`.
- pharmacy-orders: feature's item-level fulfillment service/controllers/
  routes alongside main's Order creation + fulfillment-proof endpoints; the
  record's order reference is now derived from `prescriptionId`/`labOrderId`
  instead of the removed polymorphic pair, and `createFulfillmentRecord`
  writes the typed FKs.
- facilities: main's price-aware `GET /facilities/eligible` contract is kept
  (public, `{facilities: [...]}`) and extended with issue DocForum#74's
  `capabilityType`/`detail`/`near`/`radiusKm`/`limit` params and
  `address`/`distanceKm` fields; one merged repository query does
  qualification + bounding box + haversine.
- lab-orders: main's order services plus the feature's forward-only
  `applyFulfillmentStatus`.
- app.ts: duplicate router imports/mounts from both sides removed.
- docs: ARCHITECTURE §4.5/§4.6/§4.7/§6.1, ARCHITECTURE_ESSENTIALS data map,
  ROADMAP status/changelog and docs/api endpoint tables merged by hand.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
… merge

Two collisions the merge with main left behind; both broke the suite
(`prisma migrate deploy` failed in all 33 integration files, and the split
scenario 409'd where issue DocForum#73 requires a 201).

1. Duplicate DDL. Both this branch's Phase 5 migration and main's later
   migrations added `PrescriptionItem.prescriptionId` (and its index/FK) and
   `FulfillmentRecord.facilityId` (and its index/FK), so the second
   definition failed with 42701 "column already exists". The Phase 5
   migration now creates only what it actually owns — the two
   `partially_fulfilled` enum values, the facility location columns + index,
   and the `FulfillmentItem` table — with a comment naming the migrations
   that own the rest.

2. One record per order vs. split routing. ADR 0012 put a UNIQUE on each
   order FK; issue DocForum#73 requires routing an order's remaining items to a
   *second* facility as a new record, which that UNIQUE forbids. Uniqueness
   moves down one level to (order FK, facilityId) — the guarantee ADR 0012
   was buying (no duplicated fulfillment of one order at one facility) — and
   the order relation becomes one-to-many (`fulfillments[]`) as Prisma
   requires once the FK is no longer unique. Amendment recorded in
   docs/adr/0012, ARCHITECTURE.md §4.7 and ARCHITECTURE_ESSENTIALS.md; new
   migration 20261026000000 drops the two per-column indexes and creates the
   composite ones.

Also: facility-distance.test.mts now asserts main's documented
`GET /facilities/eligible` contract — the public `{ facilities: [...] }`
envelope carrying issue DocForum#74's `address`/`distanceKm` fields — since that
endpoint doubles as the price-comparison query and must stay unauthenticated
for main's tests; a bare `near` without `type`/`capabilityType` is rejected
400 (fail closed, never an unqualified directory), and the never-store-the-
point rule is unchanged. ROADMAP's changelog entry moved into
`changelog.d/73-74-partial-fulfillment-facility-distance.md` per AGENTS.md,
and a line the conflict resolution had joined back onto DocForum#14's entry is split.

Checks (backend/): audit:prod, i18n:check, typecheck, unit 270/270,
integration 435/435 (33 files), build — all green.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

@afeezorobsco-cyber afeezorobsco-cyber left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweep: malware scan clean, CI green.

@github-actions github-actions Bot added the needs-rebase Has merge conflicts with main; the author needs to rebase label Oct 27, 2026
@github-actions

Copy link
Copy Markdown

@Oluwaferanmi18 This PR now has merge conflicts with main, most likely because #251 (feat(platform): feature flags for staged rollouts (DB-backed, per environment and per user cohort)) was just merged, so it can't be merged until it's rebased:

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-lease

The usual culprits are shared docs like ROADMAP.md and ARCHITECTURE*.md. If the conflict is in the ROADMAP.md changelog, move your entry into a new file under changelog.d/ (see changelog.d/README.md) rather than resolving it in place, so it can't conflict again. CI re-runs when you push, and the needs-rebase label is removed automatically once the conflicts are resolved.

Automated message. It won't repeat unless the PR conflicts again after your next push.

@github-actions github-actions Bot removed the needs-rebase Has merge conflicts with main; the author needs to rebase label Oct 27, 2026
@afeezorobsco-cyber

Copy link
Copy Markdown
Contributor

@Oluwaferanmi18 After updating this branch with main, CI fails at npm run prisma:generate:

Error code: P1012
error: Type "FulfillmentItem" is neither a built-in type, nor refers to another model, composite type, or enum.

prisma/schema.prisma references FulfillmentItem but no longer defines the model. It was probably dropped while resolving the merge. Please restore the model FulfillmentItem { ... } block (and its migration), run npx prisma validate && npm run typecheck in backend/, and push.

@afeezorobsco-cyber

Copy link
Copy Markdown
Contributor

@Oluwaferanmi18 Still the same prisma:generate failure as in my earlier comment, and now I have the exact location:

error: Type "FulfillmentItem" is neither a built-in type, nor refers to another model, composite type, or enum.
  -->  prisma/schema.prisma:1001
1001 |   items        FulfillmentItem[]

The relation field on line 1001 survived the merge, but the model FulfillmentItem { ... } block it points to didn't. Please re-add that model (and its migration), run npx prisma validate && npm run typecheck in backend/, and push.

@github-actions github-actions Bot added the needs-rebase Has merge conflicts with main; the author needs to rebase label Oct 28, 2026
@github-actions

Copy link
Copy Markdown

@Oluwaferanmi18 This PR now has merge conflicts with main, most likely because #162 (feat(api): idempotency and optimistic concurrency (#48, #49)) was just merged, so it can't be merged until it's rebased:

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-lease

The usual culprits are shared docs like ROADMAP.md and ARCHITECTURE*.md. If the conflict is in the ROADMAP.md changelog, move your entry into a new file under changelog.d/ (see changelog.d/README.md) rather than resolving it in place, so it can't conflict again. CI re-runs when you push, and the needs-rebase label is removed automatically once the conflicts are resolved.

Automated message. It won't repeat unless the PR conflicts again after your next push.

@github-actions github-actions Bot removed the needs-rebase Has merge conflicts with main; the author needs to rebase label Oct 29, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Phase 5] Facility location and distance-sorted facility selection [Phase 5] Partial fulfillment: item-level dispensing for multi-item prescriptions

2 participants