Summary
A fulfillment provider that needs per-line variant data — any print-on-demand or dropship integration, which is most non-manual providers — cannot get it through the admin fulfillment flow.
createOrderFulfillmentWorkflow hydrates the order from a fixed field list that omits items.variant.metadata, and the items argument handed to the provider is a list of persisted fulfillment_item rows — a model with no variant relation and no variant_id. The result: provider.createFulfillment receives no way to map an order line onto a vendor SKU.
Version: @medusajs/core-flows / @medusajs/fulfillment 2.19.0
What we hit
First real order through the admin flow, POST /admin/orders/:id/fulfillments, against a custom print-on-demand provider that reads item.variant?.metadata (the natural reading of the createFulfillment(data, items, order, fulfillment) signature):
400 invalid_data
Variant undefined is not mappable to a vendor SKU
item.variant_id was undefined — not merely item.variant.metadata. Every unit test passed, because fixtures construct hydrated items; the real caller never produces them.
Root cause, traced through 2.19.0
create-fulfillment.ts — the order query omits variant metadata. useQueryGraphStep requests items.variant.manage_inventory, items.variant.sku, items.variant.weight, items.variant.hs_code, items.variant.origin_country … but never items.variant.metadata.
prepareFulfillmentData flattens the lines. Each order item becomes { line_item_id, inventory_item_id, quantity, title, sku, barcode }. Anything else on the variant is dropped here regardless of what step 1 fetched.
fulfillment_item cannot store it. The model (@medusajs/fulfillment/models/fulfillment-item) is { id, title, sku, barcode, quantity, line_item_id, inventory_item_id, fulfillment }. No variant, no variant_id.
- The provider receives those rows verbatim from
FulfillmentModuleService.createFulfillment.
A provider cannot resolve the data for itself either: its container is the fulfillment module's, not the app's. Enumerated at runtime on 2.19.0 it holds the module's own repositories/services plus logger, manager, configModule, event_bus, caching, __pg_connection__ — no query, no remoteQuery, no product.
Proposed fix — one line, plus one docs paragraph
The order argument is passed to the provider untouched (prepareFulfillmentData returns order: order; the module service forwards it). So adding the field to the query list makes variant metadata reachable:
--- a/packages/core/core-flows/src/order/workflows/create-fulfillment.ts
+++ b/packages/core/core-flows/src/order/workflows/create-fulfillment.ts
@@ fields: [
"items.variant.material",
+ "items.variant.metadata",
"items.variant_title",
Verified against a live 2.19.0 instance — same order, same query engine:
| field list |
items[0].variant keys |
variant.metadata |
| today |
manage_inventory, sku, weight, hs_code, id |
absent |
| with the patch |
manage_inventory, sku, weight, hs_code, metadata, id |
present |
What the one-liner does not fix: it puts metadata on order.items[].variant.metadata, not on the items argument — fulfillment_item still cannot carry a variant. A provider must correlate items[].line_item_id → order.items[].id and read the variant from the order. That is not obvious from the signature, and the provider guide should say it outright. Suggested addition:
The items argument is a list of fulfillment_item records. It carries line_item_id, quantity, title, sku and barcode only — it has no variant relation. To read variant data (including metadata), match item.line_item_id against order.items[].id and read the variant from the order.
Happy to open the PR for the one-line change plus the docs paragraph — it's ready to go.
Two smaller observations from the same debugging session (unserialized entities handed to providers, and additional_data being the only documented-nowhere channel that survives the path) in a comment below, kept out of the main report so it stays single-purpose.
Summary
A fulfillment provider that needs per-line variant data — any print-on-demand or dropship integration, which is most non-manual providers — cannot get it through the admin fulfillment flow.
createOrderFulfillmentWorkflowhydrates the order from a fixed field list that omitsitems.variant.metadata, and theitemsargument handed to the provider is a list of persistedfulfillment_itemrows — a model with novariantrelation and novariant_id. The result:provider.createFulfillmentreceives no way to map an order line onto a vendor SKU.Version:
@medusajs/core-flows/@medusajs/fulfillment2.19.0What we hit
First real order through the admin flow,
POST /admin/orders/:id/fulfillments, against a custom print-on-demand provider that readsitem.variant?.metadata(the natural reading of thecreateFulfillment(data, items, order, fulfillment)signature):item.variant_idwasundefined— not merelyitem.variant.metadata. Every unit test passed, because fixtures construct hydrated items; the real caller never produces them.Root cause, traced through 2.19.0
create-fulfillment.ts— the order query omits variant metadata.useQueryGraphSteprequestsitems.variant.manage_inventory,items.variant.sku,items.variant.weight,items.variant.hs_code,items.variant.origin_country… but neveritems.variant.metadata.prepareFulfillmentDataflattens the lines. Each order item becomes{ line_item_id, inventory_item_id, quantity, title, sku, barcode }. Anything else on the variant is dropped here regardless of what step 1 fetched.fulfillment_itemcannot store it. The model (@medusajs/fulfillment/models/fulfillment-item) is{ id, title, sku, barcode, quantity, line_item_id, inventory_item_id, fulfillment }. Novariant, novariant_id.FulfillmentModuleService.createFulfillment.A provider cannot resolve the data for itself either: its container is the fulfillment module's, not the app's. Enumerated at runtime on 2.19.0 it holds the module's own repositories/services plus
logger,manager,configModule,event_bus,caching,__pg_connection__— noquery, noremoteQuery, noproduct.Proposed fix — one line, plus one docs paragraph
The
orderargument is passed to the provider untouched (prepareFulfillmentDatareturnsorder: order; the module service forwards it). So adding the field to the query list makes variant metadata reachable:Verified against a live 2.19.0 instance — same order, same query engine:
items[0].variantkeysvariant.metadatamanage_inventory, sku, weight, hs_code, idmanage_inventory, sku, weight, hs_code, metadata, idWhat the one-liner does not fix: it puts metadata on
order.items[].variant.metadata, not on theitemsargument —fulfillment_itemstill cannot carry a variant. A provider must correlateitems[].line_item_id → order.items[].idand read the variant from the order. That is not obvious from the signature, and the provider guide should say it outright. Suggested addition:Happy to open the PR for the one-line change plus the docs paragraph — it's ready to go.
Two smaller observations from the same debugging session (unserialized entities handed to providers, and
additional_databeing the only documented-nowhere channel that survives the path) in a comment below, kept out of the main report so it stays single-purpose.