Orders Unification — Surface Analysis for #342
Purpose
The orders-unification work (#342) folds two legacy partner work-order concepts — raw-material purchase orders (inventory_orders) and design work-orders (production_runs) — onto Medusa's core order entity. A new unified_order_status sidecar module carries the load-bearing partner_status that partner panels key on. Customer retail orders remain untouched (they are already core order rows). The discriminator is link existence (D5): an order linked to a production_run is kind=design, linked to an inventory_order is kind=inventory, neither = retail.
The three order surfaces
1. unified_order_status — typed sidecar
Module: apps/backend/src/modules/unified_order_status/index.ts
Service: apps/backend/src/modules/unified_order_status/service.ts — extends MedusaService({ UnifiedOrderStatus }), auto-generates CRUD.
Model: apps/backend/src/modules/unified_order_status/models/unified-order-status.ts
// line 20–31
model.define("unified_order_status", {
id: model.id({ prefix: "uos" }).primaryKey(),
partner_status: model.enum([
"assigned", "accepted", "in_progress", "finished",
"partial", "completed", "declined"
]),
})
Migration: apps/backend/src/modules/unified_order_status/migrations/Migration20260614184932.ts — creates table with partner_status check constraint.
What it represents: A 1:1 sidecar row per unified order that holds the partner-facing work-progress status. Row presence means "this order has reached a partner-tracked state." Promoted off order.metadata.partner_status (PR-H / Chunk 9b-contract) to avoid the read-modify-write race that metadata wholesale-replacement caused — a single-column upsert (setUnifiedOrderPartnerStatus) has no lost-update hazard.
2. inventory_orders — raw-material POs
Module: apps/backend/src/modules/inventory_orders/index.ts — exports constant ORDER_INVENTORY_MODULE = "inventory_orders".
Service: apps/backend/src/modules/inventory_orders/service.ts — extends MedusaService({ InventoryOrder, OrderLine }). Adds createInvWithLines (validates line input, creates order + lines, collects per-line errors) and aliases listInventoryOrderLines / listAndCountInventoryOrderLines.
Model — InventoryOrder: apps/backend/src/modules/inventory_orders/models/order.ts — table inventory_orders, id prefix inv_order, fields quantity (float), total_price (bigNumber), status (enum: Pending/Processing/Shipped/Delivered/Cancelled/Partial, default Pending), expected_delivery_date (nullable), order_date (nullable), metadata (json, nullable), shipping_address (json, nullable), is_sample (boolean, default false). hasMany -> OrderLine. Cascades delete to orderlines.
Model — OrderLine: apps/backend/src/modules/inventory_orders/models/orderline.ts — table inventory_order_line, fields quantity (float), price (bigNumber), metadata (json, nullable). belongsTo -> InventoryOrder.
Constants: apps/backend/src/modules/inventory_orders/constants.ts — exports 5 statuses (INVENTORY_ORDER_STATUS: Pending/Processing/Shipped/Delivered/Cancelled). Note: the model's enum has 6 values (including "Partial"); the constants file omits it.
Migration: apps/backend/src/modules/inventory_orders/migrations/Migration20250416111440.ts (initial), Migration20250416111849.ts, Migration20250509141001.ts, Migration20250611132802.ts, Migration20250810071843.ts, Migration20250821160909.ts, Migration20250821160920.ts, Migration20260305151334.ts — 8 migrations total.
What it represents: A raw-material purchase order sent to a partner supplier. The legacy surface before #342 was a standalone CRUD (/admin/inventory-orders, /partners/inventory-orders). Under #342, each inventory_order gets a unified core order row linked 1:1 via the order-inventory-order link (D5). The legacy entity still tracks execution detail (stock locations, tasks, payments, feedback, line fulfillments).
3. fullfilled_orders — line-level fulfillment events
Module: apps/backend/src/modules/fullfilled_orders/index.ts — exports constant FULLFILLED_ORDERS_MODULE = "fullfilled_orders".
Service: apps/backend/src/modules/fullfilled_orders/service.ts — extends MedusaService({ Line_fulfillment }). Wraps listLine_fulfillments as listLineFulfillments.
Model: apps/backend/src/modules/fullfilled_orders/models/line_fulfillment.ts — table line_fulfillment, fields id, quantity_delta (float — deliveries are in raw-material units (kg), must accept decimals — the integer column silently rounded 1.5 → 2, fixed per the model comment), event_type (enum: sent/shipped/received/adjust/correction), transaction_id (text), notes (text, nullable), metadata (json).
Migration: apps/backend/src/modules/fullfilled_orders/migrations/Migration20250810060405.ts, Migration20260304163723.ts, Migration20260612202252.ts — 3 migrations.
What it represents: Atomic fulfillment events at the line level — tracks individual shipment/receipt/adjustment/correction transactions for inventory order lines. Does NOT link to the core Medusa Order entity at all. Links only within the inventory-order sub-domain via two links: one to OrderLine and one to InventoryOrder.
How each links to the core Medusa Order entity
Links that connect directly to OrderModule.order
Each uses defineLink from @medusajs/framework/utils and is a managed (pivot-table) link so they are fully bidirectional in query.graph.
| Link file | From | To | Cardinality | Role |
|---|---|---|---|---|
apps/backend/src/links/order-unified-status.ts | OrderModule.linkable.order | UnifiedOrderStatusModule.linkable.unifiedOrderStatus | 1:1, filterable: ["id"], field: "unified_order_status" | Chunk 9b sidecar — promotes partner_status off metadata |
apps/backend/src/links/order-inventory-order.ts | OrderModule.linkable.order | InventoryOrderModule.linkable.inventoryOrders | 1:1, filterable: ["id"], field: "inventory_order" | D5 discriminator for kind=inventory + pointer replacing metadata.unified_order_id |
apps/backend/src/links/order-production-run.ts | OrderModule.linkable.order | ProductionRunsModule.linkable.productionRuns | 1:1, filterable: ["id"], field: "production_run" | D5 discriminator for kind=design + pointer |
apps/backend/src/links/partner-order.ts | PartnerModule.linkable.partner (isList: true) | OrderModule.linkable.order (isList: true) | M:N | D3 partner-scoping for work-orders |
apps/backend/src/links/design-order-link.ts | DesignModule.linkable.design (isList: true) | OrderModule.linkable.order (isList: true) | M:N | Design-to-order trace, shared with retail (NOT a kind discriminator) |
apps/backend/src/links/partner-inventory-order.ts | PartnerModule.linkable.partner | InventoryOrderModule.linkable.inventoryOrders (isList: true, field: 'inventory_orders') | M:N | Legacy partner→inventory-order scoping (does NOT involve core Order) |
apps/backend/src/links/conversion-order-link.ts | Modules.ORDER primaryKey id | AdPlanningModule.linkable.conversion.id primaryKey order_id | M:N, readOnly: true | Read-only attribution link |
Link directionality finding (empirically verified in code)
Per the comments in apps/backend/src/links/order-inventory-order.ts lines 18–31 and apps/backend/src/links/order-production-run.ts lines 23–40:
- Forward (legacy→core):
<entity>.order— e.g.inventory_orders.order,production_runs.order→ the unified order. Authoritative for transactional reads. - Reverse (core→legacy):
order.inventory_orders,order.production_runs— auto-derived PLURAL accessor names. The Index Module alias (field: "inventory_order"orfield: "production_run") does NOT rename thequery.graphreverse accessor. - Read-only links (
conversion-order-link.ts) are UNI-directional — need a separate inverse definition.
Inventory-order sub-domain links (NO direct OrderModule.order link)
These link InventoryOrdersModule and FullfilledOrdersModule to each other and to other modules, but do NOT touch the core Order entity:
| Link file | From | To |
|---|---|---|
apps/backend/src/links/fullfilled-orders-order-lines.ts | InventoryOrdersModule.linkable.inventoryOrderLine (isList) | FullfilledOrdersModule.linkable.lineFulfillment (isList) |
apps/backend/src/links/fullfilled-orders-orders.ts | InventoryOrdersModule.linkable.inventoryOrders (isList) | FullfilledOrdersModule.linkable.lineFulfillment (isList) |
apps/backend/src/links/inventory-orders-inventory-items.ts | InventoryOrdersModule.linkable.inventoryOrderLine (isList) | InventoryModule.linkable.inventoryItem (isList) |
apps/backend/src/links/inventory-orders-stock-locations.ts | InventoryOrdersModule.linkable.inventoryOrders (isList) | StockLocationModule.linkable.stockLocation (isList) — has extra columns from_location/to_location |
apps/backend/src/links/inventory-orders-tasks.ts | InventoryOrdersModule.linkable.inventoryOrders (isList) | TasksModule.linkable.task (isList, field: 'tasks') |
apps/backend/src/links/inventory-orders-internal-payments.ts | InventoryOrdersModule.linkable.inventoryOrders | InternalPaymentModule.linkable.internalPayments (isList) |
apps/backend/src/links/inventory-order-feedback.ts | InventoryOrderModule.linkable.inventoryOrders (isList, filterable: id/status/order_number) | FeedbackModule.linkable.feedback (isList, filterable: id/rating/status/submitted_at) |
apps/backend/src/links/inbound-email-inventory-order.ts | InboundEmailModule.linkable.inboundEmail (isList) | InventoryOrdersModule.linkable.inventoryOrders (isList) |
Partner-ui order read routes
All routes are under apps/backend/src/api/partners/. The partner-ui frontend (apps/partner-ui/) calls these via React Query hooks in apps/partner-ui/src/hooks/api/orders.tsx and apps/partner-ui/src/hooks/api/partner-inventory-orders.tsx.
Standard order routes (/partners/orders/...)
Each is guarded by validatePartnerOrderOwnership (apps/backend/src/api/partners/helpers.ts lines 212–254) which checks retail via sales channel and work via the D3 partner↔order link.
| Method | Route | File | What it does |
|---|---|---|---|
| GET | /partners/orders | apps/backend/src/api/partners/orders/route.ts | Lists orders for the authenticated partner. ?kind= discriminator (retail/design/inventory/all, default retail). Delegates to listPartnerOrdersWorkflow (apps/backend/src/workflows/orders/list-partner-orders.ts). DEFAULT_FIELDS includes unified_order_status.partner_status. Note: uses relation.* not *relation syntax per comment lines 7–20. |
| GET | /partners/orders/:id | apps/backend/src/api/partners/orders/[id]/route.ts | Order detail (lines 64–87: attaches production_runs, inventory_orders, unified_order_status via a second query.graph call after getOrderDetailWorkflow because the admin field set doesn't know about custom links). |
| POST | /partners/orders/:id | same file lines 89–135 | PATCH — whitelisted fields only (email, shipping_address, billing_address, locale, metadata). Metadata is read-then-merged with PROTECTED_UNIFICATION_METADATA_KEYS force-restored. |
| POST | /partners/orders/:id/cancel | .../cancel/route.ts | Cancels via cancelOrderWorkflow |
| POST | /partners/orders/:id/fulfillments | .../fulfillments/route.ts | Creates fulfillment via createOrderFulfillmentWorkflow |
| POST | /partners/orders/:id/fulfillments/:fulfillmentId/cancel | .../cancel/route.ts | Cancels fulfillment via cancelOrderFulfillmentWorkflow |
| POST | /partners/orders/:id/fulfillments/:fulfillmentId/shipment | .../shipment/route.ts | Creates shipment via createOrderShipmentWorkflow |
| POST | /partners/orders/:id/fulfillments/:fulfillmentId/mark-as-delivered | .../mark-as-delivered/route.ts | Marks delivered via markOrderFulfillmentAsDeliveredWorkflow |
| GET | /partners/orders/:id/fulfillments/:fulfillmentId/label | .../label/route.ts | Fetches shipping label (carrier provider getLabel()) |
| GET | /partners/orders/:id/fulfillments/:fulfillmentId/tracking | .../tracking/route.ts | Fetches tracking via carrier track() |
| POST | /partners/orders/:id/fulfillments/:fulfillmentId/pickup | .../pickup/route.ts | Schedules pickup via carrier schedulePickup() |
| GET | /partners/orders/:id/shipping-options | .../shipping-options/route.ts | Lists shipping options |
| GET | /partners/orders/:id/preview | .../preview/route.ts | Order edit preview |
| GET | /partners/orders/:id/line-items | .../line-items/route.ts | Line items with variant/product |
| GET | /partners/orders/:id/changes | .../changes/route.ts | Order changes (edits, claims, exchanges, returns, transfers) |
| POST | /partners/orders/:id/changes/:changeId | .../changes/[changeId]/route.ts | Update change |
| POST | /partners/orders/changes/:orderChangeId | apps/backend/src/api/partners/orders/changes/[orderChangeId]/route.ts | Update change by ID (alternative path) |
| POST | /partners/orders/:id/transfer | .../transfer/route.ts | Request transfer |
| POST | /partners/orders/:id/transfer/cancel | .../transfer/cancel/route.ts | Cancel transfer request |
| POST | /partners/orders/:id/credit-lines | .../credit-lines/route.ts | Create credit line |
Inventory order routes (/partners/inventory-orders/...)
These operate on the legacy InventoryOrder entity directly. They do NOT go through validatePartnerOrderOwnership — they use getPartnerFromAuthContext + check the partner-inventory-order link.
| Method | Route | File | What it does |
|---|---|---|---|
| GET | /partners/inventory-orders | apps/backend/src/api/partners/inventory-orders/route.ts | Lists inventory orders for partner via query.graph on partner-inventory-order link entry point (InventoryOrderPartnerLink.entryPoint). Derives partner_status from workflow tasks (sent/received/shipped). Status and q filtering applied in-app via applyInventoryOrderListFilters (apps/backend/src/api/partners/inventory-orders/list-filters.ts). NOTE: pagination MUST happen after in-app filters — paginating in query.graph slices before filters, giving wrong page/count (#484). |
| GET | /partners/inventory-orders/:orderId | apps/backend/src/api/partners/inventory-orders/[orderId]/route.ts | Detail with unified_order_id derived from inventory_orders.order.id forward link (lines 244–246). Includes order lines, stock locations, tasks, inventory items, raw materials, line fulfillments, internal payments. Derives partner status from workflow tasks. |
| POST | /partners/inventory-orders/:orderId/start | .../start/route.ts | Validates Pending → Processing, runs updateInventoryOrderWorkflow, completes partner-order-received task. |
| POST | /partners/inventory-orders/:orderId/complete | .../complete/route.ts | Delegates to partnerCompleteInventoryOrderWorkflow. |
| POST | /partners/inventory-orders/:orderId/submit-payment | .../submit-payment/route.ts | Runs createPaymentAndLinkWorkflow. |
Validators and helpers
apps/backend/src/api/partners/orders/validators.ts— Zod schema for?kind=(retail/design/inventory/all).apps/backend/src/api/partners/inventory-orders/list-filters.ts— Pure functionapplyInventoryOrderListFiltersfor in-app status + free-text filtering and pagination.apps/backend/src/api/partners/helpers.ts— Exports:refetchPartner,getPartnerFromAuthContext,validatePartnerStoreAccess,getPartnerStore,tryGetPartnerStore,tryGetPartnerSalesChannelId,validatePartnerEntityOwnership,validatePartnerOrderOwnership(dual-scope: retail via sales channel, work via D3 link),validatePartnerOrderEntityOwnership,scopeAndAggregateVariantInventory,ensureInventoryLevelsForVariants,getPartnerSalesChannelId.
AuthenticatedMedusaRequest pattern
All partner routes use req.auth_context.actor_id to identify the partner. The route ${orderId}/route.ts detail route (lines 64–87) uses a bare try/catch that swallows query.graph failures (lines 82–84: // leave the order as-is; the UI falls back to retail rendering).
State / status model
The unified_order_status vocabulary
Defined at apps/backend/src/modules/unified_order_status/models/unified-order-status.ts line 23–29:
assigned → accepted → in_progress → finished → partial → completed → declined
This 7-value enum is the shared vocabulary the T3 partner panels key on.
Inventory order → core order status mapping (kind=inventory)
Defined in apps/backend/src/workflows/inventory_orders/dual-write-unified-order.ts:
LEGACY_TO_CORE_STATUS (lines 41–48):
| Legacy status | core order.status |
|---|---|
| Pending | pending |
| Processing | pending |
| Shipped | pending |
| Partial | pending |
| Delivered | completed |
| Cancelled | canceled |
LEGACY_TO_PARTNER_STATUS (lines 56–61):
| Legacy status | unified_order_status.partner_status |
|---|---|
| Processing | in_progress |
| Shipped | finished |
| Partial | partial |
| Delivered | completed |
Pending and Cancelled are absent on purpose: "assigned" is stamped by send-to-partner, and the §5 table defines no partner_status for either.
Production run → core order status mapping (kind=design)
Defined in apps/backend/src/workflows/production-runs/dual-write-unified-run-order.ts:
RUN_TO_CORE_STATUS (lines 35–43):
| Run status | core order.status |
|---|---|
| draft | draft |
| pending_review | draft |
| approved | pending |
| sent_to_partner | pending |
| in_progress | pending |
| completed | completed |
| cancelled | canceled |
deriveRunPartnerStatus (lines 52–73) — derives partner_status from run lifecycle timestamps:
| Run status + lifecycle | partner_status |
|---|---|
| sent_to_partner | assigned |
| in_progress + finished_at | finished |
| in_progress + started_at | in_progress |
| in_progress + accepted_at | accepted |
| in_progress (self-serve, no timestamps) | in_progress |
| completed | completed |
| cancelled (if declined opt) | declined |
| cancelled (admin) | undefined (no-op) |
Write sites for partner_status
All 5 write sites use setUnifiedOrderPartnerStatus (apps/backend/src/workflows/inventory_orders/dual-write-unified-order.ts lines 154–183):
mirrorPartnerLinkOnUnifiedOrderStepat line 411 — writes"assigned"on send-to-partner for inventorymirrorUnifiedOrderStatusStepat line 472 — mirrors legacy status transitionsprojectRunToUnifiedOrderat line 341 — writes on create for design runs already in a partner-tracked statemirrorRunStatusToUnifiedOrderat line 423 — mirrors run lifecycle transitionsmirrorRunPartnerLinkOnUnifiedOrderStepat line 536 — writes"assigned"on send-to-partner for design
The setUnifiedOrderPartnerStatus helper (lines 154–183):
- Resolves the sidecar row via
query.graphonorder.unified_order_status.id - If exists, calls
service.updateUnifiedOrderStatuses([{ id, partner_status }]) - If not, creates via
service.createUnifiedOrderStatusesthenremoteLink.createto link to the order
Invariant: partial delivery tracking
The fullfilled_orders module tracks line-level fulfillment events via Line_fulfillment with event_type enum and quantity_delta (float). The inventory order status includes "Partial" which maps to partner_status = "partial". The quantity_delta is float because "deliveries are in raw-material units (kg)" per the model comment at apps/backend/src/modules/fullfilled_orders/models/line_fulfillment.ts line 6.
Gotchas / invariants
-
query.graphreverse accessors are PLURAL, not singular. Managed links are bidirectional, butquery.graphreverse accessors auto-derive as PLURAL (order.inventory_orders,order.production_runs). Thefield: "inventory_order"pin only affects the Index Module's singular alias, NOTquery.graph. Using the singular form inquery.graphgives "Entity 'Order' does not have property 'production_run'". Verified atapps/backend/src/links/order-production-run.tslines 23–30 andapps/backend/src/links/order-inventory-order.tslines 18–31. -
Forward link resolution is authoritative for transactional reads.
query.graphon<entity>.orderis synchronous/authoritative.query.indexis eventually consistent and must never be used for mirror-step correctness. Documented atapps/backend/src/links/order-production-run.tslines 20–22 andapps/backend/src/links/order-inventory-order.tslines 15–16. -
Metadata is REPLACED wholesale by
updateOrders, not merged. The partner PATCH route atapps/backend/src/api/partners/orders/[id]/route.tslines 110–129 performs an explicit read-then-merge to preserve keys, andPROTECTED_UNIFICATION_METADATA_KEYS(defined atapps/backend/src/workflows/inventory_orders/dual-write-unified-order.tslines 28–36) are force-restored so partner input can never overwrite them. This is whypartner_statuswas promoted off metadata onto the typed sidecar column (PR-H). -
DEFAULT_FIELDSin the partner orders list route usesrelation.*not*relationsyntax. TheGET /partners/ordersroute atapps/backend/src/api/partners/orders/route.tslines 21–38 explicitly documents (in comments lines 7–20) that*relationsyntax only works via the admin middleware'svalidateAndTransformQuerywhich the partner route does NOT run. Withoutrelation.*form,customer,sales_channel,shipping_addresscome back null. -
Pagination on inventory-orders list MUST happen after in-app filters.
query.graphcannot filter on linked-module columns, so status/qare matched in-app. Paginating inquery.graphslices before filters, returning wrong page count (the #484 bug). Documented atapps/backend/src/api/partners/inventory-orders/route.tslines 174–178 andapps/backend/src/api/partners/inventory-orders/list-filters.tslines 7–12. -
validatePartnerOrderOwnershipdual-scope. Both scopes must pass for access: retail via sales channel (order.sales_channel_id === store.default_sales_channel_id), work via D3 partner↔order link existence. If either matches, access is granted. Atapps/backend/src/api/partners/helpers.tslines 212–254. -
The
design ↔ orderlink is NOT a kind discriminator. It is shared with retail purchases (order-placed.tsrunslinkDesignsToOrderon every purchase). Only theorder ↔ production_runandorder ↔ inventory_orderlinks are discriminators. Documented atapps/backend/src/links/order-production-run.tslines 12–14. -
partner-inventory-orderlink binds the partner directly to the inventory order (NOT via the unified core order). Defined atapps/backend/src/links/partner-inventory-order.ts. This is the legacy scoping mechanism. The D3partner-order.tslink (atapps/backend/src/links/partner-order.ts) is the unification-era scoping to core Order. Both coexist during transition. -
Inventory order model enum has 6 values; constants define 5. The model at
apps/backend/src/modules/inventory_orders/models/order.tslines 8–14 includes "Partial" in the enum, butapps/backend/src/modules/inventory_orders/constants.tsdefines only["Pending", "Processing", "Shipped", "Delivered", "Cancelled"]— "Partial" is missing from the constants. -
quantity_deltais float, not integer. TheLine_fulfillmentmodel atapps/backend/src/modules/fullfilled_orders/models/line_fulfillment.tsline 7 ismodel.float()because raw-material quantities (kg) need decimals. The comment notes that an integer column silently rounded 1.5→2 (#342). -
Order detail route attaches execution links via a second
query.graphcall, NOT through thegetOrderDetailWorkflowfields. Atapps/backend/src/api/partners/orders/[id]/route.tslines 64–87: after the workflow resolves, the route makes a separatequery.graphonordersentity requestingproduction_runs.id,inventory_orders.id,unified_order_status.partner_status. ThegetOrderDetailWorkflow's field set is the admin order query config which doesn't expand custom links. The second call is best-effort — caught (line 82) so a graph hiccup doesn't break the route. -
setUnifiedOrderPartnerStatushas a first-write race that is a non-issue in practice. The create path projection and the single send-to-partner mirror establish the sidecar row before any concurrent transition can run. Documented atapps/backend/src/workflows/inventory_orders/dual-write-unified-order.tslines 149–153.
Open questions / (unverified)
-
Line fulfillment
quantity_deltasemantics: Theevent_typeenum includesadjustandcorrectionbut the model comment only mentions the integer-rounding fix. Howadjust/correctioninteract with the inventory order'sPartialstatus is not captured in source comments. (unverified) -
Inventory order constants file drift:
apps/backend/src/modules/inventory_orders/constants.tsomits"Partial"fromINVENTORY_ORDER_STATUSwhile the model schema allows it. Whether this is intentional (Partial as a transitional/derived state, not a first-class status) or an accident is undocumented. -
fullfilled_ordersmodule misspelling: The module namefullfilled_orders(double l, double f) appears consistently, including the constantFULLFILLED_ORDERS_MODULE. This is the canonical spelling but differs from standard English. No rename is tracked. -
Migration count proliferation: The
inventory_ordersmodule has 8 migration files andfullfilled_ordershas 3 — notably more than other modules, suggesting schema churn. Whether all history is still needed or if migrations could be squashed is unaddressed. -
query.indexusage in partner /adminGET /orders?kind=: The admin retail list filter at (unverified — I did not readapps/backend/src/api/admin/orders/route.tsin this session) currently usesquery.graphwith anid: $in/$ninapproach, not the Index Module anti-join. The eventual-consistency tradeoff and scaling limits of the injected id array are documented as accepted inORDERS_UNIFICATION_342.mdbut not verified here. -
Partner-order detail route's
unified_order_statusBEST-EFFORT second query could silently miss status on production. Atapps/backend/src/api/partners/orders/[id]/route.tsline 82, the catch block is empty — anyquery.graphfailure silently drops the status enrichment, and the UI falls back to retail rendering. This is intentional but could mask a production issue where partner_status fails to render. -
fullfilled_ordershas no direct link to Medusa's coreOrderentity. All its links go throughInventoryOrdersModule. After the inventory_orders → core Order unification, the fulfillment chain isorder → inventory_order ↔ line_fulfillment— two hops. Whether a directorder ↔ line_fulfillmentlink is needed is not addressed.