Skip to content

feat(orders): partial fulfilment dispense and route remaining items (… - #168

Merged
afeezorobsco-cyber merged 8 commits into
DocForum:mainfrom
Bolajiomo99:feat/partial-fulfilment-dispense-some-items-and-route
Oct 27, 2026
Merged

afeezorobsco-cyber merged 8 commits into
DocForum:mainfrom
Bolajiomo99:feat/partial-fulfilment-dispense-some-items-and-route

Conversation

@Bolajiomo99

Copy link
Copy Markdown
Contributor

feat(orders): partial fulfilment — dispense some items and route the rest elsewhere

Close#54

Closes #54

Summary

This PR implements Phase W4 — Partial fulfilment for DocForum Web.

Facility staff can now dispense an order item by item and quantity by quantity, instead of marking a whole order fulfilled or rejected. Anything a facility cannot supply stays outstanding and the patient can route only those remaining items to another eligible facility from the patient orders screen.

The UI is deliberately conservative about money: it renders the payment-release rule supplied by docforum-core and explains the implications, but it does not calculate escrow releases, pro-rate payments, or call Stellar/escrow directly. The exact backend release rule remains owned by docforum-core and is still pending docforum-core#73.

The PR also adds the minimum orders foundations needed by #54 because #5/#8 are not fully implemented yet: patient order list/detail, item-level order models, item-level dispensing, remaining-item routing, and MSW coverage so the flow is testable without a backend.

Problem

Before this PR:

  • A facility could only act on an order as a whole (fulfilled / rejected).
  • There was no representation of quantityPrescribed, quantityDispensed, or quantityOutstanding.
  • There was no patient-facing order surface under /patient/orders.
  • There was no way to send the undispensed part of an order to a different facility.
  • There was no protection/UX for “another facility already dispensed this item”.
  • Partial fulfilment payment semantics were not visible to patients or facilities.

This made partial fulfilment unsafe and impossible: a facility with only part of an order could not record what it actually supplied, and a patient could not recover the remaining items elsewhere.

What changed

1. Item-level fulfilment models

Added/mirrored in src/types/models.ts:

  • Extended FulfillmentItemStatus with:
    • partially_dispensed
    • dispensed_elsewhere
  • Extended FulfillmentQueueItem with:
    • quantityPrescribed
    • quantityDispensed
    • quantityOutstanding
    • unit
    • canDispense
    • blockedReason
    • dispensedByFacilityId
    • dispensedByFacilityName
    • orderItemId (links a facility queue row to the patient’s order item)
  • Added:
    • PartialFulfillmentStatus
    • OrderItemFulfillmentStatus
    • OrderItem
    • OrderPaymentSummary
    • PatientOrder
    • DispenseFulfillmentItemInput
    • RouteRemainingItemsInput
  • Extended Order with optional fulfillmentStatus and items so existing patient-chart order tables can display partial status.

2. Facility-side partial dispensing

New feature area: src/features/fulfillment/

  • FacilityFulfillmentQueue
    • Groups queue rows by order.
    • Shows prescribed / dispensed / outstanding quantities per item.
    • Shows derived order-level status badges.
    • Allows dispensing a specific quantity for a specific item.
    • Validates against the outstanding quantity client-side, while treating the backend as the source of truth.
    • Allows rejecting an individual item.
    • Shows the recorded staff attribution for previous actions.
    • Clearly marks dispensed_elsewhere items and disables any further dispensing.
    • Shows blockedReason for routed/blocked items.
    • Includes a payment callout: partial fulfilment does not release the full escrow.
  • src/features/fulfillment/status.ts
    • itemFulfillmentStatus
    • deriveOrderFulfillmentStatus
    • ITEM_STATUS_LABELS
    • Defensive derivation in case a backend row omits an explicit item status.
  • useDispenseFulfillmentItem
    • Calls the item-level dispense endpoint.
    • Invalidates the facility queue and patient orders queries on success.

Facility dashboard (/facility) now uses the new per-item queue instead of the whole-order action UI. Existing whole-order attribution behaviour and tests are preserved.

3. Patient order list and detail

New feature area additions: src/features/orders/

  • PatientOrdersList at /patient/orders
    • Lists prescriptions and lab orders.
    • Shows order-level fulfilment badges (Unfulfilled, Partially filled, Fulfilled, Rejected).
    • Shows item counts and how many items are still outstanding.
    • Links to order detail.
  • PatientOrderDetail at /patient/orders/:orderId
    • Shows every item with prescribed / dispensed / outstanding quantities.
    • Shows status and facility history per item.
    • Clearly warns when an item was already dispensed elsewhere.
    • Shows when remaining quantities were routed to another facility.
    • Shows the backend-supplied payment rule and payment status.
    • Provides “Get remaining items elsewhere” for outstanding items only.
    • Handles all-fulfilled, fully-routed, and non-routable states.
  • RemainingItemsFacilityPicker
    • Loads facilities eligible for only the selected outstanding items.
    • Excludes the order’s current facility.
    • Lets the patient choose one facility and send only the remaining items.
    • Shows the payment implications before confirming.
    • Handles 409 conflicts explicitly: if another facility already dispensed or another client already routed an item, it asks the patient to refresh and shows the latest state.
    • Closes on Escape and backdrop click; includes a labelled modal dialog.
  • Added patient navigation link Orders and routes under the patient dashboard.
  • Added fulfilment badges to the existing patient-chart orders table so clinicians can see partial status too.

4. API service layer

Added:

  • src/services/fulfillment-service.ts
    • dispenseFulfillmentItem(facilityId, itemId, { quantity }, token)
  • src/services/orders-service.ts
    • listMyOrders
    • getOrder
    • getEligibleFacilitiesForRemainingItems
    • routeRemainingItems

All requests go through apiFetch and therefore through docforum-core. There are no direct calls to docforum-escrow or Stellar in this repo, preserving hard rule 1.

5. Mock API coverage (MSW)

Added/updated fixtures and handlers so the entire flow can be exercised locally:

  • db.orders seeded with:
    • a multi-item partially fulfilled prescription
    • an unfulfilled lab order
    • a fully fulfilled prescription
    • items in pending, partially_dispensed, dispensed, and dispensed_elsewhere states
    • held, released, and per-item payment-rule examples
  • src/mocks/handlers/orders.ts
    • GET /orders/mine
    • GET /orders/:orderId
    • GET /orders/:orderId/remaining-items/facilities?itemIds=…
    • POST /orders/:orderId/route-remaining
  • Facility handlers now:
    • Return only queue rows belonging to the requested facility.
    • Support POST /facilities/:facilityId/fulfillment-queue/:itemId/dispense.
    • Enforce outstanding quantity.
    • Return 409 for dispensed-elsewhere, already-routed, or over-quantity requests.
    • Sync facility queue state with patient order items.
    • Mark other facilities’ rows as dispensed_elsewhere when the last outstanding quantity is dispensed elsewhere.
    • Create destination-facility queue rows when remaining items are routed.
    • Recompute order fulfilment status after dispensing/rejecting.
  • Aligned the mock FACILITY_ID with the app’s hardcoded fac-main so the facility dashboard and mock queue are consistent.

6. Docs

Acceptance criteria mapping

Requirement Implementation Evidence
Dispense some items, not necessarily a whole order Quantity-per-item dispense endpoint/UI FacilityFulfillmentQueue, useDispenseFulfillmentItem, handler tests
Keep undispensed items outstanding quantityOutstanding maintained on queue rows and order items MSW dispense tests, patient detail UI
Route only the remaining items routeRemainingItems + eligible-facility picker RemainingItemsFacilityPicker, handler tests
Show partial order state to patients and staff Derived partially_fulfilled badges FulfillmentStatusBadge, deriveOrderFulfillmentStatus
Prevent double dispensing Disable rows and return 409 for dispensed-elsewhere items Queue component test, MSW conflict tests
Explain payment implications without computing release Render backend payment.rule verbatim Patient detail payment callout, facility queue callout
No direct escrow/Stellar calls Service layer uses apiFetch only Code review / architecture rule
Test without backend MSW fixtures + handlers Handler tests and local dev flow
Responsive screens Desktop/mobile CSS and screenshots 8 screenshots listed below

Error handling and edge cases

  • Quantity validation: whole numbers only, at least 1, never above outstanding. The input blocks invalid values locally; the backend/mock also rejects them.
  • Already dispensed elsewhere: shown as a warning, dispense action removed/disabled, and backend returns 409 if forced.
  • Already routed: remaining item cannot be routed again; UI shows the destination facility.
  • Route conflict (409): explicit message asks the patient to refresh; does not silently fail.
  • Partially fulfilled order: order badge remains Partially filled, not Fulfilled.
  • All outstanding dispensed elsewhere: order becomes fulfilled once nothing remains outstanding.
  • Rejected item: item cannot be dispensed; queue remains auditable.
  • No eligible facility: picker shows an empty state and keeps the confirm action disabled.
  • No payment rule: UI falls back to a neutral explanation and never invents an amount.
  • Loading/error states: patient lists/detail/picker and facility queue show spinners, empty states, and readable errors.
  • Modal accessibility: labelled dialog, aria-modal, Escape close, backdrop close, disabled confirm until a facility is selected.

Payment semantics — important for reviewers

This PR intentionally does not implement the release calculation.

  • The UI displays OrderPaymentSummary.rule exactly as supplied by docforum-core.
  • The UI shows held/released/refunded status and stroop totals when provided.
  • The UI states that partial fulfilment does not release the full escrow.
  • The actual rule for releasing or refunding partially fulfilled orders is owned by docforum-core and is pending docforum-core#73.
  • No UI code reduces, splits, or computes a partial release amount.

When docforum-core#73 lands, only the response shape/rule text should need confirmation; the screens are designed to render it rather than reinterpret it.

Assumed API contracts

These are documented in ARCHITECTURE_ESSENTIALS.md and mirrored in models.ts:

  • GET /orders/mine → PatientOrder[]
  • GET /orders/:orderId → PatientOrder
  • GET /orders/:orderId/remaining-items/facilities?itemIds=<csv> → FacilityWithDistance[]
  • POST /orders/:orderId/route-remaining → PatientOrder
  • POST /facilities/:facilityId/fulfillment-queue/:itemId/dispense → FulfillmentQueueItem

Notes:

  • orderItemId is assumed as the link between a facility queue row and a patient order item.
  • Backend must enforce quantity limits and return 409 for dispensed-elsewhere/already-routed items.
  • Backend must derive order-level fulfillmentStatus from item rows. The frontend keeps a defensive derivation helper only for resilience.

Screenshots

Desktop and mobile captures are included under screenshots/:

  • patient-orders-desktop.png
  • patient-orders-mobile.png
  • patient-order-partial-desktop.png
  • patient-order-partial-mobile.png
  • patient-order-route-picker-desktop.png
  • patient-order-route-picker-mobile.png
  • facility-partial-fulfilment-desktop.png
  • facility-partial-fulfilment-mobile.png

Manual QA steps

  1. Run npm run dev:mock.
  2. Open /facility as the facility persona.
  3. Confirm the queue shows grouped items with prescribed/dispensed/outstanding quantities.
  4. Dispense a partial quantity for “Demo Vitamin D 1000 IU (fake)”.
  5. Confirm the item becomes partially dispensed and the quantity is reduced.
  6. Try to dispense more than outstanding; confirm it is blocked.
  7. Confirm the dispensed-elsewhere item shows a warning and has no dispense action.
  8. Open /patient/orders as the patient persona.
  9. Confirm the partially fulfilled order shows Partially filled and the correct outstanding count.
  10. Open the order detail.
  11. Confirm item quantities, dispensed-elsewhere warning, and payment rule are visible.
  12. Click Get remaining items elsewhere.
  13. Confirm only eligible facilities for the outstanding items are shown.
  14. Select a facility and confirm the modal explains payment implications.
  15. Confirm sending remaining items succeeds and the destination is shown on the items.
  16. Re-open the facility queue and confirm routed items can no longer be dispensed by the original facility.
  17. Force a 409 path (or use an already-dispensed item) and confirm the patient sees an explicit conflict message.

Verification

All checks were run locally:

npm run typecheck    # pass
npm test             # 42 test files, 385 tests pass
npm run build        # pass
npm run check:dist   # pass — no mock code in production bundle
Note: the existing Vite CSS minifier prints a non-fatal @media warning from the global stylesheet; the build still succeeds. It is unrelated to this PR.
Tests added/updated
- src/features/fulfillment/components/FacilityFulfillmentQueue.test.tsx
- Grouping by order
- Partial status and quantities
- Partial dispense action
- Over-outstanding validation
- Dispensed-elsewhere warning and disabled action
- Reject action
- src/features/orders/components/PatientOrders.test.tsx
- Order list status/counts/link
- Item quantities, dispensed-elsewhere warning, payment rule
- Successful routing flow
- 409 conflict messaging
- src/mocks/handlers.test.ts
- Patient order list/detail
- Partial dispensing and order sync
- Over-quantity 409
- Remaining-item eligibility
- Routing to a second facility
- Original facility locking once another facility dispenses
- Facility ID alignment update
- Queue count update
Checklist
Implementation
- Implements DocForum/docforum-web#54
- Keeps all data access in the docforum-core API layer
- No direct docforum-escrow/Stellar calls
- Mirrors new/changed models in src/types/models.ts
- Preserves pages/ components/ features/ hooks/ services/ store/ types/ structure
- Item-level dispensing with quantity validation
- Item-level rejection
- Remaining-item routing to a second facility
- Dispensed-elsewhere items shown and disabled
- 409 conflict handling
- Payment rule rendered, not computed
- Patient order list + detail routes/nav
- Existing facility attribution behaviour preserved
Mocks and tests
- MSW fixtures for orders and partial fulfilment
- MSW item-level dispense endpoint
- MSW remaining-item routing endpoint
- Facility-scoped queue responses
- Status syncing between facility queue and patient order items
- Handler tests
- Component tests
- Full test suite passes
Docs and evidence
- ROADMAP.md updated
- ARCHITECTURE_ESSENTIALS.md updated with assumed contracts
- Desktop screenshots captured
- Mobile screenshots captured
- PR notes the PRD §4 core flow implemented
Review / follow-up
- Typecheck / test / build / check:dist pass
- Confirm final docforum-core#73 order/payment response shape
- Confirm backend-enforced orderItemId linkage and 409 semantics
- Manual QA against a real docforum-core backend once #73 lands
- Merge after review
Known limitations / follow-ups
1. docforum-core#73 is still pending. Payment-release rules and final order/payment field names are assumed. The UI intentionally renders backend text rather than calculating releases.
2. #5/#8 are not fully implemented. This PR adds the minimum orders and item-level fulfilment foundations required by #54. A later queue PR may extend this surface.
3. orderItemId is an assumed linkage field. If docforum-core chooses a different identifier, the mapping layer will need a small update.
4. No real-backend QA yet. MSW coverage is comprehensive, but final verification needs a running docforum-core API.
5. Screenshots are from the mock API. Data is intentionally fake (example.test, demo personas).
Reviewer notes
- The highest-risk area is the assumed order/item/payment contract. Please review ARCHITECTURE_ESSENTIALS.md → Phase W4 partial fulfilment against docforum-core#73 when it is available.
- The second highest-risk area is the 409 race: two facilities (or a facility and a patient routing action) acting at the same time. The UI handles 409 defensively, but the backend must remain authoritative.
- The facility queue fallback DEFAULT_MOCK_QUEUE remains for demo/test resilience, but real data comes from GET /facilities/:facilityId/fulfillment-queue.

@drips-wave

drips-wave Bot commented Oct 20, 2026

Copy link
Copy Markdown

@Bolajiomo99 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! 🚀

Learn more about application limits

@afeezorobsco-cyber

Copy link
Copy Markdown
Contributor

@Bolajiomo99 Thanks for the PR. It can't be merged yet. To unblock it:

Merge conflicts: this branch conflicts with main. Please rebase onto the latest main (git fetch upstream && git rebase upstream/main), resolve the conflicts (often ROADMAP.md, ARCHITECTURE*.md or prisma/schema.prisma), and force-push.

Push to this same branch and CI will re-run. Once it's green and conflict-free, it'll be reviewed for merge.

@github-actions github-actions Bot added the needs-rebase Has merge conflicts with main; the author needs to rebase label Oct 20, 2026
@github-actions github-actions Bot added needs-rebase Has merge conflicts with main; the author needs to rebase and removed needs-rebase Has merge conflicts with main; the author needs to rebase labels Oct 21, 2026
@github-actions

Copy link
Copy Markdown

@Bolajiomo99 This PR now has merge conflicts with main, most likely because #131 (feat(patient): order detail with a verifiable QR code and print-friendly view) was just merged, so it can't be merged until it's rebased:

git fetch upstream            # upstream = https://github.com/DocForum/docforum-web.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.

@afeezorobsco-cyber

Copy link
Copy Markdown
Contributor

@Bolajiomo99 CI is failing on this branch against the latest main, so it can't be merged yet. The errors from the latest run:

Frontend (typecheck + test + build)

src/components/FulfillmentStatusBadge.tsx(1,15): error TS2305: Module '"../types/models"' has no exported member 'PartialFulfillmentStatus'.
src/features/facility-queue/components/FacilityQueuePanel.tsx(20,7): error TS2739: Type '{ pending: string; in_progress: string; fulfilled: string; rejected: string; }' is missing the following properties from type 'Record<FulfillmentItemStatus, string>': disp
src/features/fulfillment/components/FacilityFulfillmentQueue.test.tsx(79,9): error TS2353: Object literal may only specify known properties, and 'canDispense' does not exist in type 'Partial<FulfillmentQueueItem>'.
src/features/fulfillment/components/FacilityFulfillmentQueue.tsx(32,27): error TS2339: Property 'quantityPrescribed' does not exist on type 'FulfillmentQueueItem'.
src/features/fulfillment/components/FacilityFulfillmentQueue.tsx(34,10): error TS2339: Property 'quantityDispensed' does not exist on type 'FulfillmentQueueItem'.
src/features/fulfillment/components/FacilityFulfillmentQueue.tsx(36,10): error TS2339: Property 'quantityOutstanding' does not exist on type 'FulfillmentQueueItem'.
src/features/fulfillment/components/FacilityFulfillmentQueue.tsx(73,40): error TS2739: Type 'FulfillmentQueueItem' is missing the following properties from type 'FulfillmentQuantity': quantityDispensed, quantityOutstanding, quantityPrescribed
src/features/fulfillment/components/FacilityFulfillmentQueue.tsx(77,29): error TS2339: Property 'canDispense' does not exist on type 'FulfillmentQueueItem'.

src/types/models.ts doesn't export the types and fields this PR uses any more. The additions to that file were probably dropped while resolving a conflict, so re-add them.

This PR also conflicts with main again, so please rebase first, then fix the above.

@github-actions

Copy link
Copy Markdown

@Bolajiomo99 This PR now has merge conflicts with main, most likely because #199 (feat(admin): per-specialty verification review for admins (Closes #65)) was just merged, so it can't be merged until it's rebased:

git fetch upstream            # upstream = https://github.com/DocForum/docforum-web.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.

Resolve conflicts with main, keeping both sides:

- src/types/models.ts: drop the duplicate `Order` the branch carried (its
  services all speak `PatientOrder`), and rename the partial-fulfilment item
  row to `OrderItemFulfillment` so it no longer collides with main's
  `OrderItem`.
- src/services/orders-service.ts: drop the branch's duplicate `getOrder`
  (same endpoint as main's, different return type) and keep `getPatientOrder`.
- src/features/patient-chart/components/PatientChartPanel.tsx: keep main's
  order columns — `activeOrders` is main's `Order`, which has no
  `fulfillmentStatus`.
- ROADMAP.md: restore the DocForum#54 item line that the earlier merge of main
  silently dropped.
…fulfilment

# Conflicts:
#	src/mocks/handlers.test.ts
@github-actions github-actions Bot removed the needs-rebase Has merge conflicts with main; the author needs to rebase label Oct 25, 2026
@Bolajiomo99

Copy link
Copy Markdown
Contributor Author

kindly review and merge

@afeezorobsco-cyber

Copy link
Copy Markdown
Contributor

@Bolajiomo99 The Browser (journeys + accessibility + visual) job fails the visual regression check: the rendered page no longer matches the committed baseline:

Expected: e2e/__screenshots__/visual-desktop/facility-dashboard.png
Expected: e2e/__screenshots__/visual-mobile/facility-dashboard.png

If the visual change is intentional, regenerate those baselines in the same Linux/Playwright setup CI uses (see the visual-testing notes in e2e/) and commit them. If it isn't intentional, fix the layout. Then push.

@github-actions

Copy link
Copy Markdown

🔎 Preview build (downloadable artifact)

Download the preview artifact · build log

  • Built from f983750; GitHub sign-in is required to download.
  • This build is not published to GitHub Pages or any other shared origin.
  • Runs against the mock API only — fake seed data, no real backend, no real credentials.
  • To view it locally: unzip the artifact, then serve the folder (for example
    npx serve <folder>) and open the URL it prints.
  • The artifact expires after 14 days and is replaced on every push to this PR.

@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.

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 W4] Partial fulfilment: dispense some items and route the rest elsewhere

2 participants