diff --git a/project-ideas/shopify-integration/shopify-event-based-inventory-sync-implementation.md b/project-ideas/shopify-integration/shopify-event-based-inventory-sync-implementation.md new file mode 100644 index 00000000..b7149d7d --- /dev/null +++ b/project-ideas/shopify-integration/shopify-event-based-inventory-sync-implementation.md @@ -0,0 +1,282 @@ +# Shopify Event-Based Inventory Sync Implementation + +## Purpose + +This document describes the current phase 1 implementation for Shopify inventory sync with direct `SECA` action and no dedicated sync history entities. + +The goal is to keep the implementation small, understandable, and close to the real OMS business events. + +The implementation manages Shopify inventory only for POS/store locations that exist in Shopify. Non-Shopify facilities are out of scope, except the `_NA_` facility reset path used for accumulated inventory. + +## Scope + +This implementation covers these lanes: + +1. Transfer shipment +2. Transfer receipt +3. Store fulfillment shipment +4. Inventory adjustment for cycle count, manual variance, external POS sale, and `_NA_` accumulated inventory reset delta from `ExternalInventoryReset` + +Reservation sync is intentionally not included in phase 1. For sales orders, Shopify inventory should change when the POS/store shipment is issued, not when OMS reservation happens. + +## Online Store Inventory Boundary + +This implementation is for store and POS inventory events in Shopify. The goal is to keep the store-side inventory movement and event trace aligned with OMS as events occur. + +This implementation is not the source for Shopify Online Store PDP inventory in phase 1. + +The intended operating model is: + +- store inventory events are posted to Shopify as they occur in OMS +- store inventory and transfer movement become visible in Shopify at the relevant store locations +- online available inventory continues to be synchronized by the existing hard sync and upload recent inventory changes jobs + +This boundary is important because Shopify calculates online quantity from locations that fulfill online orders. Shopify also allows a location to be prevented from fulfilling online orders, and when that is done the inventory at that location is removed from the product's online quantity shown to customers. + +Implementation implication: + +- this event-based sync should be used for store locations that are not intended to contribute to online sellable quantity +- under that Shopify location configuration, store event sync keeps Shopify store inventory accurate without changing PDP online available inventory +- if a store location is configured in Shopify to fulfill online orders, then its available inventory can contribute to online quantity and this assumption no longer holds + +Expected transition over time: + +- as more store-impacting OMS flows move to `SECA`-driven event sync, the hard sync jobs should stop producing store-side inventory changes in normal operation +- those jobs still remain the current source for online available inventory and for any inventory lanes not yet covered by event sync + +## What Phase 1 Does Not Add + +- no sync history entities +- no outbox entity +- no `SystemMessage` +- no scheduled retry table scan +- no generic `InventoryItemDetail` data feed + +Phase 1 is immediate-action integration from `SECA` with logging on failure. + +## High-Level Design + +```mermaid +classDiagram + class SecaTrigger { + +check product store setting + +call lane sync service + +log failures + } + class TransferSyncService { + +post#ShopifyTransferShipment + +receive#ShopifyTransferShipment + } + class FulfillmentSyncService { + +post#ShopifyFulfillment + } + class AdjustmentSyncService { + +post#ShopifyPhysicalInventoryVariance + +post#ShopifyManualPhysicalInventoryVariance + +post#ShopifyExternalInventoryReset + +post#ShopifyInventoryAdjustments + } + + SecaTrigger --> TransferSyncService + SecaTrigger --> FulfillmentSyncService + SecaTrigger --> AdjustmentSyncService +``` + +## Service Roles + +### 1. Transfer Sync Services + +Implemented roles: + +- `co.hotwax.sob.transfer.ShopifyTransferOrderServices.post#ShopifyTransferShipment` +- `co.hotwax.sob.transfer.ShopifyTransferOrderServices.receive#ShopifyTransferShipment` + +Responsibilities: + +`post#ShopifyTransferShipment` +- find the shipped OMS transfer shipment +- resolve the Shopify shop from `productStoreId` and mapped route facilities +- aggregate shipment lines by Shopify inventory item +- create Shopify `InventoryTransfer` +- create Shopify `InventoryShipment` +- store created Shopify shipment ids in `ShipmentAttribute` `SHPFY_INV_SHIPMENTS` + +`receive#ShopifyTransferShipment` +- process each `ShipmentReceipt` row after commit +- reuse `SHPFY_INV_SHIPMENTS` when already created for the OMS shipment +- if `SHPFY_INV_SHIPMENTS` is missing, initialize Shopify transfer and shipment from the OMS shipment first +- for `TO_Receive_Only`, initialize the Shopify transfer with destination location only and leave origin blank +- call `inventoryShipmentReceive` against the existing Shopify shipment line + +### 2. Store Fulfillment Sync Service + +Implemented role: + +- `co.hotwax.sob.fulfillment.FulfillmentFeedServices.post#ShopifyFulfillment` + +Responsibility: + +- resolve Shopify order and fulfillment orders +- compare assigned location with actual OMS shipping store +- move the fulfillment order when required +- create the Shopify fulfillment + +This is the correct store-shipment equivalent for Shopify. + +### 3. Inventory Adjustment Sync Services + +Implemented roles: + +- `co.hotwax.sob.product.InventoryServices.post#ShopifyPhysicalInventoryVariance` +- `co.hotwax.sob.product.InventoryServices.post#ShopifyManualPhysicalInventoryVariance` +- `co.hotwax.sob.product.InventoryServices.post#ShopifyExternalInventoryReset` +- `co.hotwax.sob.product.InventoryServices.post#ShopifyInventoryAdjustments` + +Responsibility: + +- handle adjustment-style deltas only +- call `inventoryAdjustQuantities` + +This service should be reused for: + +- cycle count +- manual variance +- external POS sale where Shopify did not create the sale +- `_NA_` accumulated inventory reset delta from the created `ExternalInventoryReset` record + +Manual variance is intentionally filtered: + +- `post#ShopifyManualPhysicalInventoryVariance` only syncs when the persisted `InventoryItemDetail` rows for the `physicalInventoryId` do not carry `orderId`, `returnId`, or `shipmentId` +- this prevents order-specific, shipment-specific, or return-specific physical inventory records from being pushed as manual adjustment deltas + +## SECA Responsibilities + +The `SECA` should do only three things: + +1. identify the source business key +2. call the lane sync service +3. log failure without disturbing committed OMS work + +The `SECA` should not: + +- contain business mapping logic +- build GraphQL payloads +- query Shopify directly + +## Suggested SECA Layout + +| OMS service | SECA timing | Sync service | +| --- | --- | --- | +| `co.hotwax.poorti.TransferOrderFulfillmentServices.ship#TransferOrderShipment` | `post-commit` | `co.hotwax.sob.transfer.ShopifyTransferOrderServices.post#ShopifyTransferShipment` | +| `create#org.apache.ofbiz.shipment.receipt.ShipmentReceipt` | `post-commit` | `co.hotwax.sob.transfer.ShopifyTransferOrderServices.receive#ShopifyTransferShipment` | +| `co.hotwax.poorti.FulfillmentServices.ship#Shipment` | `post-commit` | `co.hotwax.sob.fulfillment.FulfillmentFeedServices.post#ShopifyFulfillment` | +| `co.hotwax.cycleCount.InventoryCountServices.create#PhysicalInventory` | `post-commit` | `co.hotwax.sob.product.InventoryServices.post#ShopifyPhysicalInventoryVariance` | +| `co.hotwax.poorti.FulfillmentServices.create#PhysicalInventory` | `post-commit` | `co.hotwax.sob.product.InventoryServices.post#ShopifyManualPhysicalInventoryVariance` | +| `create#ExternalInventoryReset` | `post-commit` | `co.hotwax.sob.product.InventoryServices.post#ShopifyExternalInventoryReset` | + +## Failure Handling + +Phase 1 failure handling is intentionally simple: + +- OMS business work is already committed +- Shopify sync is attempted immediately +- failure is logged +- no sync history row is created +- replay is manual in phase 1 + +Current async behavior is lane-specific: + +- transfer shipment sync is async +- fulfillment sync is async +- cycle count variance sync is async +- receipt sync is intentionally not async, to avoid parallel `ShipmentReceipt` rows creating duplicate Shopify transfer/shipment records for the same OMS shipment +- manual physical inventory sync remains immediate post-commit with `ignore-error="true"` + +This is acceptable for the first cut because: + +- the design stays small +- the business boundary stays clear +- support can inspect logs by source business key + +If failures become frequent, the next enhancement should be a small retry or outbox model. That should be justified by production behavior, not added upfront. + +## Service Interaction Example + +Example: `TO_Receive_Only` warehouse-to-store receipt + +1. OMS creates the transfer order and advances it through approval into pending receipt. +2. OMS creates `ShipmentReceipt` rows as the store receives inventory. +3. `SECA` fires after each `ShipmentReceipt` commit. +4. Receipt sync resolves the Shopify shop, destination location, and product inventory item mapping. +5. If `SHPFY_INV_SHIPMENTS` already exists on the OMS shipment, the service reuses those Shopify shipment ids. +6. If `SHPFY_INV_SHIPMENTS` is missing, the service initializes Shopify transfer and shipment from the OMS shipment. +7. For `TO_Receive_Only`, the created Shopify transfer uses destination location only, so origin remains blank on Shopify. +8. The service then calls `inventoryShipmentReceive` for the accepted quantity on the matching Shopify shipment line. +9. Subsequent receipt rows for the same OMS shipment reuse the stored Shopify shipment ids instead of creating new receipt-side Shopify documents. + +## Implementation Notes + +- fail fast on missing location or product mapping +- never hard reset inventory from these event paths +- use adjustment mutations only for adjustment-style events +- use transfer and shipment APIs only for actual transfer movement +- do not mirror OMS lifecycle for control purposes in Shopify +- skip non-Shopify facilities except the explicitly handled `_NA_` accumulated inventory reset path +- do not implement reservation sync in phase 1 +- persist Shopify transfer shipment ids on the OMS shipment using `ShipmentAttribute` `SHPFY_INV_SHIPMENTS` +- for `TO_Receive_Only`, treat shipment-level initialization as the normal path when no prior Shopify shipment exists +- one `ExternalInventoryReset` row currently results in one Shopify adjustment call; reset rows are not grouped by `resetDateResourceId` in phase 1 + +## Shopify Support Basis + +The above online inventory boundary is consistent with Shopify's documented behavior: + +- Shopify states that `available` inventory is the inventory that can be sold, while `incoming` inventory is not available to sell until it is received +- Shopify states that online orders are assigned based on available inventory at locations that fulfill online orders +- Shopify states that preventing a location from fulfilling online orders removes that location's inventory from the product's online quantity shown to customers + +Official references: + +- Shopify inventory states: https://help.shopify.com/en/manual/products/inventory/managing-inventory-quantities/inventory-states +- Shopify location fulfillment for online orders: https://help.shopify.com/en/manual/fulfillment/setup/locations/fulfillment +- Shopify multi-location inventory and online quantity: https://help.shopify.com/en/manual/locations/assigning-inventory-to-locations +- Shopify fulfillable inventory behavior: https://help.shopify.com/en/manual/fulfillment/setup/fulfillable-inventory + +## Shopify Resources Used In This Implementation + +This implementation relies on these Shopify Admin GraphQL resources and mutations: + +- `InventoryTransfer` object +- `InventoryShipment` object +- `inventoryTransferCreateAsReadyToShip` +- `inventoryShipmentCreateInTransit` +- `inventoryShipmentReceive` +- `inventoryAdjustQuantities` +- `fulfillmentOrderMove` +- `fulfillmentCreate` + +These resources are sufficient for phase 1 because the design goal is not to mirror OMS order orchestration in Shopify. The goal is to reflect store-side inventory movement and shipment events in Shopify as OMS commits them. + +## Idempotency Introduction Should Not Be Left Out + +A follow-up improvement should add Shopify-side idempotency for the inventory movement and inventory adjustment mutations that support it. This is important because OMS `SECA`-driven event sync can still face replay scenarios from manual rerun, service retry, duplicate trigger delivery, or uncertain remote outcome after network failure. + +Current implementation position: + +- OMS-side correlation already reduces duplicate transfer posting through `ShipmentAttribute` `SHPFY_INV_SHIPMENTS` +- OMS-side shipment fulfillment correlation already stores the returned Shopify fulfillment id on the OMS shipment +- phase 1 does not yet pass Shopify GraphQL idempotency for supported inventory mutations + +Recommended next step: + +- introduce deterministic idempotency keys derived from OMS event identity for `inventoryTransferCreateAsReadyToShip`, `inventoryShipmentCreateInTransit`, `inventoryShipmentReceive`, and `inventoryAdjustQuantities` +- keep OMS-side correlation checks as the first line of defense +- continue to treat fulfillment sync separately unless Shopify explicitly documents idempotent support for the fulfillment mutations being used + +This should be treated as an important reliability enhancement, especially for inventory adjustments, because duplicate replay of a delta mutation can apply the same quantity change twice. + +## Operational Note + +This approach is the right starting point for a small implementation. + +It gives immediate sync at the right OMS boundary without introducing extra entities. If reliability gaps appear later, then add persistent replay after observing actual failure patterns. diff --git a/project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md b/project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md new file mode 100644 index 00000000..a080a7b3 --- /dev/null +++ b/project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md @@ -0,0 +1,138 @@ +# Shopify Event-Based Inventory Sync Triggers + +## Purpose + +This document defines the trigger points for a simple OMS to Shopify inventory sync. + +OMS remains the system of record. Shopify is updated only for the inventory effect of OMS events at mapped Shopify locations. The design is delta-based only. It does not use daytime hard reset. + +The scope is POS/store locations that exist in Shopify. Non-Shopify facilities are not part of this event sync. + +## Pre-Requisites + +- OMS and Shopify inventory levels must match before this design is enabled. +- Sync must run only for product stores where a dedicated `ProductStoreSetting`, for example `SHOPIFY_INV_SYNC`, enables Shopify inventory sync. +- If the product store setting is off, the `SECA` must not attempt Shopify inventory sync. +- Facility must map to a Shopify POS/store location before any store inventory delta is posted. + +Without a matched starting baseline, a delta-only design will drift instead of converge. + +## Design Summary + +Phase 1 is intentionally simple: + +1. A service `SECA` runs after the OMS business service completes. +2. The `SECA` immediately tries to post the Shopify delta workflow. +3. If Shopify sync succeeds, nothing else is persisted just for sync tracking. +4. If Shopify sync fails or the trigger path is missed, log the failure context for support and manual replay. + +This design does not use `SystemMessage`. It also does not introduce sync history entities in phase 1. + +## Core Principles + +- OMS decides the business event. +- Shopify receives only the inventory effect of that event. +- Only deltas are posted to Shopify. +- No daytime hard reset should be used for these flows. +- `SECA` is the primary integration trigger. +- Failure handling is log-first in phase 1. +- Transfer lifecycle is not mirrored in Shopify for business control. Transfer APIs are used only when Shopify requires them for inventory movement. +- Phase 1 does not sync reservation events. Inventory is posted to Shopify when OMS creates the physical movement or correction event, such as shipment issuance, receipt, or cycle count variance. + +## Processing Flow + +```mermaid +flowchart TD + A[OMS business service completes] --> B[SECA checks ProductStoreSetting] + B -->|sync disabled| C[Skip] + B -->|sync enabled| D[Call Shopify sync service] + D -->|success| E[Finish] + D -->|failure| F[Write structured failure log] +``` + +## Sequence View + +```mermaid +sequenceDiagram + participant OMS as OMS Service + participant SECA as Service SECA + participant SYNC as Shopify Sync Service + participant SHOP as Shopify + + OMS->>SECA: business transaction committed + SECA->>SECA: check ProductStoreSetting + SECA->>SYNC: call lane-specific sync service + SYNC->>SHOP: post delta workflow + SHOP-->>SYNC: success or error + SYNC-->>SECA: result + SECA-->>SECA: log failure when needed +``` + +## Trigger Matrix + +| OMS event | SECA trigger boundary | Sync service | Shopify workflow | Notes | +| --- | --- | --- | --- | --- | +| Store-origin TO outbound shipment reduces QOH | `co.hotwax.poorti.TransferOrderFulfillmentServices.ship#TransferOrderShipment` post-service | `sync#TransferShipmentToShopify` | Create `InventoryShipment`, then mark it in transit | This reproduces origin `on_hand` reduction and destination `incoming` increase | +| TO inbound receipt into store increases ATP and QOH | `ShipmentReceipt` create or update, grouped by `shipmentId + datetimeReceived + facilityId` | `sync#TransferReceiptToShopify` | `inventoryShipmentReceive` | Receipt must be shipment-backed for Shopify; non-shipment OMS receipts stay in exception handling | +| Online order shipped from store | `co.hotwax.poorti.FulfillmentServices.ship#Shipment` post-service | `sync#StoreFulfillmentToShopify` | Move Fulfillment Order to actual store when needed, then create fulfillment | This ensures Shopify applies fulfillment against the actual shipping store | +| External POS sale or non-Shopify sale reduces inventory | dedicated sales posting or issuance boundary | `sync#InventoryAdjustmentToShopify` | `inventoryAdjustQuantities` | Use only when Shopify is not already the system that created the sale; Shopify POS orders are already handled by Shopify | +| Cycle count or approved manual variance changes QOH and ATP | `co.hotwax.cycleCount.InventoryCountServices.create#PhysicalInventory` when variance is applied | `sync#InventoryAdjustmentToShopify` | `inventoryAdjustQuantities` | Manual variance follows the same lane | +| `_NA_` facility reset for accumulated inventory | `create#ExternalInventoryReset` completion, using the created `ExternalInventoryReset` record as the source | `sync#ExternalInventoryResetToShopify` | `inventoryAdjustQuantities` using `quantityOnHandDiff` and `availableToPromiseDiff` from the reset record | `reset#InventoryItem` computes diffs, but the Shopify sync source should be the durable `ExternalInventoryReset` row created for `_NA_`; store-level POS inventory should still be event-driven by shipment, receipt, and correction events | + +## Shopify Workflow By Lane + +### 1. Store Fulfillment Lane + +Use this for online orders fulfilled from stores. + +Workflow: + +1. Resolve the Shopify order and open fulfillment orders. +2. Resolve the actual shipping facility in OMS. +3. If Shopify assigned a different location, move the fulfillment order to the actual store. +4. Create the Shopify fulfillment from that store. + +### 2. Transfer Shipment Lane + +Use this for store to warehouse, warehouse to store, and store to store transfer movement. + +Workflow: + +1. On OMS ship, create the minimum `InventoryTransfer` needed to support Shopify `InventoryShipment`. +2. Create `InventoryShipment` and mark it in transit. +3. On OMS receive, call `inventoryShipmentReceive`. + +No reservation sync is included in phase 1. + +### 3. Inventory Adjustment Lane + +Use this for: + +- cycle count +- manual variance +- external POS sale when Shopify did not create the sale +- external reset delta for `_NA_` accumulated inventory from the created `ExternalInventoryReset` record + +Workflow: + +1. Resolve location and inventory item. +2. Build delta quantity change. +3. Post `inventoryAdjustQuantities`. + +## Logging And Missed Events + +Phase 1 does not create retry entities or sync history entities. + +If a `SECA` call fails or a trigger path is missed, log enough information to support replay: + +- event type +- source service name +- orderId, shipmentId, receiptId, or physicalInventoryId +- productStoreId +- facilityId +- resolved shopId +- resolved Shopify location id +- payload summary +- Shopify error text + +This is enough to start with immediate sync and operational visibility. Persistent replay tables or scheduled retry can be added later if the failure pattern justifies the extra model. diff --git a/project-ideas/shopify-integration/transfer-order/postman/gorjana_sandbox_shopify_2026-01.postman_environment.template.json b/project-ideas/shopify-integration/transfer-order/postman/gorjana_sandbox_shopify_2026-01.postman_environment.template.json new file mode 100644 index 00000000..245f84da --- /dev/null +++ b/project-ideas/shopify-integration/transfer-order/postman/gorjana_sandbox_shopify_2026-01.postman_environment.template.json @@ -0,0 +1,278 @@ +{ + "id": "4d77f98b-1ea5-4c0a-ae17-f416090d2f0b", + "name": "gorjana sandbox Shopify 2026-01", + "values": [ + { + "key": "store_url", + "value": "gorjana-sandbox.myshopify.com", + "type": "default", + "enabled": true + }, + { + "key": "access_token", + "value": "", + "type": "secret", + "enabled": true + }, + { + "key": "api_version", + "value": "2026-01", + "type": "default", + "enabled": true + }, + { + "key": "first", + "value": "10", + "type": "default", + "enabled": true + }, + { + "key": "line_item_first", + "value": "25", + "type": "default", + "enabled": true + }, + { + "key": "shipment_first", + "value": "10", + "type": "default", + "enabled": true + }, + { + "key": "transfer_query", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "transfer_id", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "shipment_id", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "origin_location_id", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "destination_location_id", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "inventory_item_id", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "inventory_item_id_2", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "inventory_item_quantity", + "value": "1", + "type": "default", + "enabled": true + }, + { + "key": "inventory_item_quantity_2", + "value": "1", + "type": "default", + "enabled": true + }, + { + "key": "inventory_item_quantity_set", + "value": "2", + "type": "default", + "enabled": true + }, + { + "key": "inventory_item_quantity_set_2", + "value": "3", + "type": "default", + "enabled": true + }, + { + "key": "transfer_line_item_id", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "transfer_line_item_id_2", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "shipment_line_item_id", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "shipment_line_item_id_2", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "shipment_item_quantity", + "value": "1", + "type": "default", + "enabled": true + }, + { + "key": "shipment_item_quantity_2", + "value": "1", + "type": "default", + "enabled": true + }, + { + "key": "shipment_item_quantity_update", + "value": "2", + "type": "default", + "enabled": true + }, + { + "key": "shipment_receive_quantity", + "value": "1", + "type": "default", + "enabled": true + }, + { + "key": "shipment_receive_reason", + "value": "ACCEPTED", + "type": "default", + "enabled": true + }, + { + "key": "shipment_receive_bulk_action", + "value": "ACCEPTED", + "type": "default", + "enabled": true + }, + { + "key": "reference_name", + "value": "TO-POSTMAN-001", + "type": "default", + "enabled": true + }, + { + "key": "note", + "value": "Created from Postman collection", + "type": "default", + "enabled": true + }, + { + "key": "date_created_datetime", + "value": "2026-04-09T10:00:00Z", + "type": "default", + "enabled": true + }, + { + "key": "date_created_date", + "value": "2026-04-09", + "type": "default", + "enabled": true + }, + { + "key": "date_shipped", + "value": "2026-04-09T11:00:00Z", + "type": "default", + "enabled": true + }, + { + "key": "date_received", + "value": "2026-04-09T12:00:00Z", + "type": "default", + "enabled": true + }, + { + "key": "tracking_number", + "value": "TEST123456", + "type": "default", + "enabled": true + }, + { + "key": "tracking_company", + "value": "UPS", + "type": "default", + "enabled": true + }, + { + "key": "tracking_url", + "value": "https://www.ups.com/track?tracknum=TEST123456", + "type": "default", + "enabled": true + }, + { + "key": "tracking_arrives_at", + "value": "2026-04-10T12:00:00Z", + "type": "default", + "enabled": true + }, + { + "key": "latest_transfer_status", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "latest_shipment_status", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "location_id", + "value": "gid://shopify/Location/11125850169", + "enabled": true + }, + { + "key": "location_name", + "value": "3295 Laguna Canyon Rd", + "enabled": true + }, + { + "key": "after_cursor", + "value": "null", + "enabled": true + }, + { + "key": "bulk_operation_id", + "value": "", + "enabled": true + }, + { + "key": "bulk_operation_status", + "value": "", + "enabled": true + }, + { + "key": "bulk_operation_url", + "value": "", + "enabled": true + }, + { + "key": "bulk_operation_partial_data_url", + "value": "", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2026-04-09T11:00:00.000Z", + "_postman_exported_using": "Postman" +} diff --git a/project-ideas/shopify-integration/transfer-order/postman/shopify_inventory_transfer_and_shipment_2026-01.postman_collection.json b/project-ideas/shopify-integration/transfer-order/postman/shopify_inventory_transfer_and_shipment_2026-01.postman_collection.json new file mode 100644 index 00000000..c6e8d044 --- /dev/null +++ b/project-ideas/shopify-integration/transfer-order/postman/shopify_inventory_transfer_and_shipment_2026-01.postman_collection.json @@ -0,0 +1,1032 @@ +{ + "info": { + "_postman_id": "dc4362db-8937-4a5d-8f91-6a09670464f8", + "name": "Shopify Inventory Transfer And Shipment 2026-01", + "description": "Runnable Postman collection for Shopify Admin GraphQL inventory transfer and shipment flows on API version 2026-01.\n\nSequence:\n1. Fill the environment values for locations and inventory items.\n2. Run a request from 01 Draft Transfer Flow or 03 Ready-To-Ship Direct Flow.\n3. Run the matching detail query to capture generated line item and shipment IDs.\n4. Continue with shipment mutations and receive mutations as required.\n\nNotes:\n- The collection is pinned to 2026-01 because that is the requested version.\n- inventoryShipmentSetBarcode is intentionally excluded because it was added in 2026-04 and would not be runnable on 2026-01.\n- Idempotent mutations use Postman {{$guid}} values in the body so repeated runs do not reuse the same key.\n- Transfer and shipment detail queries store IDs back into the active Postman environment for the next request.\n\nScale support:\n- Use 00 Scale Setup > Location Inventory Levels to page live inventory by location.\n- Use 00 Scale Setup > Run Inventory Items Bulk Export and Current Bulk Operation when you need a full inventory export for large-route planning.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "00 Scale Setup", + "description": "Requests used to discover large route candidates before creating bulk inventory transfers.", + "item": [ + { + "name": "Location Inventory Levels", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "const location = json?.data?.location;", + "const pageInfo = location?.inventoryLevels?.pageInfo;", + "if (location?.name) pm.environment.set('location_name', location.name);", + "if (pageInfo?.hasNextPage && pageInfo?.endCursor) { pm.environment.set('after_cursor', JSON.stringify(pageInfo.endCursor)); } else { pm.environment.set('after_cursor', 'null'); }" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Pages live inventory levels for one location. Use this to identify tracked items with enough available quantity before building a large transfer route.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"query locationInventoryLevels($id: ID!, $first: Int!, $after: String) { location(id: $id) { id name inventoryLevels(first: $first, after: $after) { edges { cursor node { item { id sku tracked } quantities(names: [\\\"available\\\", \\\"incoming\\\", \\\"reserved\\\", \\\"on_hand\\\"]) { name quantity } } } pageInfo { hasNextPage endCursor } } } }\",\n \"variables\": {\n \"id\": \"{{location_id}}\",\n \"first\": {{first}},\n \"after\": {{after_cursor}}\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + }, + { + "name": "Run Inventory Items Bulk Export", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "const bulk = json?.data?.bulkOperationRunQuery?.bulkOperation;", + "if (bulk?.id) pm.environment.set('bulk_operation_id', bulk.id);", + "if (bulk?.status) pm.environment.set('bulk_operation_status', bulk.status);" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Starts a Shopify bulk export of inventory items with location inventory levels. Use this when you need a full large-scale inventory discovery pass.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"mutation runInventoryItemsBulkExport { bulkOperationRunQuery(query: \\\"\\\"\\\"{ inventoryItems { edges { node { id sku inventoryLevels { edges { node { location { id name } quantities(names: [\\\\\\\"available\\\\\\\", \\\\\\\"incoming\\\\\\\", \\\\\\\"reserved\\\\\\\", \\\\\\\"on_hand\\\\\\\"]) { name quantity } } } } } } } }\\\"\\\"\\\") { bulkOperation { id status } userErrors { field message } } }\"\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + }, + { + "name": "Current Bulk Operation", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "const bulk = json?.data?.currentBulkOperation;", + "if (bulk?.id) pm.environment.set('bulk_operation_id', bulk.id);", + "if (bulk?.status) pm.environment.set('bulk_operation_status', bulk.status);", + "if (bulk?.url) pm.environment.set('bulk_operation_url', bulk.url);", + "if (bulk?.partialDataUrl) pm.environment.set('bulk_operation_partial_data_url', bulk.partialDataUrl || '');" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Polls the currently running Shopify bulk operation and captures the download URL when it completes.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"query currentBulkOperation { currentBulkOperation { id status errorCode createdAt completedAt objectCount fileSize url partialDataUrl } }\"\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "00 Queries", + "description": "Read requests to inspect transfers and shipments and to hydrate environment variables for later mutation requests.", + "item": [ + { + "name": "Inventory Transfers List", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "const firstTransfer = json?.data?.inventoryTransfers?.nodes?.[0];", + "if (firstTransfer?.id) pm.environment.set('transfer_id', firstTransfer.id);", + "if (firstTransfer?.referenceName) pm.environment.set('reference_name', firstTransfer.referenceName);", + "if (firstTransfer?.status) pm.environment.set('latest_transfer_status', firstTransfer.status);" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Lists inventory transfers. Useful as a starting point to capture an existing transfer ID.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"query inventoryTransfers($first: Int!, $query: String) { inventoryTransfers(first: $first, query: $query) { nodes { id name referenceName status dateCreated totalQuantity receivedQuantity lineItemsCount { count } } pageInfo { hasNextPage endCursor } } }\",\n \"variables\": {\n \"first\": {{first}},\n \"query\": \"{{transfer_query}}\"\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + }, + { + "name": "Inventory Transfer Detail", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "const transfer = json?.data?.inventoryTransfer;", + "const lineItems = transfer?.lineItems?.nodes || [];", + "const shipments = transfer?.shipments?.nodes || [];", + "if (transfer?.id) pm.environment.set('transfer_id', transfer.id);", + "if (transfer?.referenceName) pm.environment.set('reference_name', transfer.referenceName);", + "if (transfer?.status) pm.environment.set('latest_transfer_status', transfer.status);", + "if (lineItems[0]?.id) pm.environment.set('transfer_line_item_id', lineItems[0].id);", + "if (lineItems[1]?.id) pm.environment.set('transfer_line_item_id_2', lineItems[1].id);", + "if (lineItems[0]?.inventoryItem?.id) pm.environment.set('inventory_item_id', lineItems[0].inventoryItem.id);", + "if (lineItems[1]?.inventoryItem?.id) pm.environment.set('inventory_item_id_2', lineItems[1].inventoryItem.id);", + "if (shipments[0]?.id) pm.environment.set('shipment_id', shipments[0].id);", + "if (shipments[0]?.status) pm.environment.set('latest_shipment_status', shipments[0].status);" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Gets a single transfer with line items and linked shipments. Run this after transfer creation, line-item changes, or shipment creation.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"query inventoryTransfer($id: ID!, $lineItemFirst: Int!, $shipmentFirst: Int!) { inventoryTransfer(id: $id) { id name referenceName status dateCreated note tags totalQuantity receivedQuantity lineItemsCount { count } lineItems(first: $lineItemFirst) { nodes { id title totalQuantity processableQuantity shippableQuantity shippedQuantity inventoryItem { id sku } } } shipments(first: $shipmentFirst) { nodes { id name status dateCreated dateShipped dateReceived } } } }\",\n \"variables\": {\n \"id\": \"{{transfer_id}}\",\n \"lineItemFirst\": {{line_item_first}},\n \"shipmentFirst\": {{shipment_first}}\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + }, + { + "name": "Inventory Shipment Detail", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "const shipment = json?.data?.inventoryShipment;", + "const lineItems = shipment?.lineItems?.nodes || [];", + "if (shipment?.id) pm.environment.set('shipment_id', shipment.id);", + "if (shipment?.status) pm.environment.set('latest_shipment_status', shipment.status);", + "if (lineItems[0]?.id) pm.environment.set('shipment_line_item_id', lineItems[0].id);", + "if (lineItems[1]?.id) pm.environment.set('shipment_line_item_id_2', lineItems[1].id);", + "if (lineItems[0]?.inventoryItem?.id) pm.environment.set('inventory_item_id', lineItems[0].inventoryItem.id);", + "if (lineItems[1]?.inventoryItem?.id) pm.environment.set('inventory_item_id_2', lineItems[1].inventoryItem.id);" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Gets a single shipment with line items, tracking, and receive totals. Run this after shipment create, add-items, tracking, in-transit, or receive requests.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"query inventoryShipment($id: ID!, $lineItemFirst: Int!) { inventoryShipment(id: $id) { id name status dateCreated dateShipped dateReceived lineItemsCount { count } lineItemTotalQuantity totalAcceptedQuantity totalReceivedQuantity totalRejectedQuantity tracking { trackingNumber company trackingUrl arrivesAt } lineItems(first: $lineItemFirst) { nodes { id quantity acceptedQuantity rejectedQuantity unreceivedQuantity inventoryItem { id sku } } } } }\",\n \"variables\": {\n \"id\": \"{{shipment_id}}\",\n \"lineItemFirst\": {{line_item_first}}\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "01 Draft Transfer Flow", + "description": "Create a draft transfer, then edit, set items, remove items, and mark it ready to ship.", + "item": [ + { + "name": "Create Draft Transfer", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "const transfer = json?.data?.inventoryTransferCreate?.inventoryTransfer;", + "if (transfer?.id) pm.environment.set('transfer_id', transfer.id);", + "if (transfer?.status) pm.environment.set('latest_transfer_status', transfer.status);" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Official mutation: inventoryTransferCreate. Creates a draft transfer with line items.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"mutation inventoryTransferCreate($input: InventoryTransferCreateInput!, $idempotencyKey: String!) { inventoryTransferCreate(input: $input) @idempotent(key: $idempotencyKey) { inventoryTransfer { id name referenceName status dateCreated } userErrors { field message } } }\",\n \"variables\": {\n \"input\": {\n \"originLocationId\": \"{{origin_location_id}}\",\n \"destinationLocationId\": \"{{destination_location_id}}\",\n \"lineItems\": [\n {\n \"inventoryItemId\": \"{{inventory_item_id}}\",\n \"quantity\": {{inventory_item_quantity}}\n },\n {\n \"inventoryItemId\": \"{{inventory_item_id_2}}\",\n \"quantity\": {{inventory_item_quantity_2}}\n }\n ],\n \"tags\": [\n \"postman\",\n \"transfer-test\"\n ],\n \"dateCreated\": \"{{date_created_datetime}}\",\n \"note\": \"{{note}}\",\n \"referenceName\": \"{{reference_name}}\"\n },\n \"idempotencyKey\": \"{{$guid}}\"\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + }, + { + "name": "Edit Transfer", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Official mutation: inventoryTransferEdit. Uses the edit-specific originId and destinationId fields.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"mutation inventoryTransferEdit($id: ID!, $input: InventoryTransferEditInput!) { inventoryTransferEdit(id: $id, input: $input) { inventoryTransfer { id name referenceName status note tags } userErrors { field message } } }\",\n \"variables\": {\n \"id\": \"{{transfer_id}}\",\n \"input\": {\n \"originId\": \"{{origin_location_id}}\",\n \"destinationId\": \"{{destination_location_id}}\",\n \"dateCreated\": \"{{date_created_date}}\",\n \"note\": \"{{note}}\",\n \"tags\": [\n \"postman\",\n \"edited\"\n ],\n \"referenceName\": \"{{reference_name}}\"\n }\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + }, + { + "name": "Set Transfer Items", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Official mutation: inventoryTransferSetItems. Adds or resets quantities by inventory item.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"mutation inventoryTransferSetItems($input: InventoryTransferSetItemsInput!, $idempotencyKey: String!) { inventoryTransferSetItems(input: $input) @idempotent(key: $idempotencyKey) { inventoryTransfer { id status lineItemsCount { count } } updatedLineItems { inventoryItemId newQuantity } userErrors { field message code } } }\",\n \"variables\": {\n \"input\": {\n \"id\": \"{{transfer_id}}\",\n \"lineItems\": [\n {\n \"inventoryItemId\": \"{{inventory_item_id}}\",\n \"quantity\": {{inventory_item_quantity_set}}\n },\n {\n \"inventoryItemId\": \"{{inventory_item_id_2}}\",\n \"quantity\": {{inventory_item_quantity_set_2}}\n }\n ]\n },\n \"idempotencyKey\": \"{{$guid}}\"\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + }, + { + "name": "Remove Transfer Items", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Official mutation: inventoryTransferRemoveItems. Requires transfer line item IDs, not inventory item IDs.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"mutation inventoryTransferRemoveItems($input: InventoryTransferRemoveItemsInput!) { inventoryTransferRemoveItems(input: $input) { inventoryTransfer { id status lineItemsCount { count } } removedQuantities { inventoryItemId newQuantity } userErrors { field message } } }\",\n \"variables\": {\n \"input\": {\n \"id\": \"{{transfer_id}}\",\n \"transferLineItemIds\": [\n \"{{transfer_line_item_id}}\"\n ]\n }\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + }, + { + "name": "Mark Transfer Ready To Ship", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Official mutation: inventoryTransferMarkAsReadyToShip.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"mutation inventoryTransferMarkAsReadyToShip($id: ID!) { inventoryTransferMarkAsReadyToShip(id: $id) { inventoryTransfer { id name referenceName status } userErrors { field message } } }\",\n \"variables\": {\n \"id\": \"{{transfer_id}}\"\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "02 Shipment Flow", + "description": "Shipment operations after a transfer is ready to ship.", + "item": [ + { + "name": "Create Draft Shipment", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "const shipment = json?.data?.inventoryShipmentCreate?.inventoryShipment;", + "if (shipment?.id) pm.environment.set('shipment_id', shipment.id);", + "if (shipment?.status) pm.environment.set('latest_shipment_status', shipment.status);" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Official mutation: inventoryShipmentCreate. Creates a draft shipment against the transfer.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"mutation inventoryShipmentCreate($input: InventoryShipmentCreateInput!, $idempotencyKey: String!) { inventoryShipmentCreate(input: $input) @idempotent(key: $idempotencyKey) { userErrors { field message code } inventoryShipment { id name status dateCreated } } }\",\n \"variables\": {\n \"input\": {\n \"movementId\": \"{{transfer_id}}\",\n \"lineItems\": [\n {\n \"inventoryItemId\": \"{{inventory_item_id}}\",\n \"quantity\": {{shipment_item_quantity}}\n }\n ]\n },\n \"idempotencyKey\": \"{{$guid}}\"\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + }, + { + "name": "Add Shipment Items", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Official mutation: inventoryShipmentAddItems.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"mutation inventoryShipmentAddItems($id: ID!, $lineItems: [InventoryShipmentLineItemInput!]!, $idempotencyKey: String!) { inventoryShipmentAddItems(id: $id, lineItems: $lineItems) @idempotent(key: $idempotencyKey) { userErrors { field message code } inventoryShipment { id name status } } }\",\n \"variables\": {\n \"id\": \"{{shipment_id}}\",\n \"lineItems\": [\n {\n \"inventoryItemId\": \"{{inventory_item_id_2}}\",\n \"quantity\": {{shipment_item_quantity_2}}\n }\n ],\n \"idempotencyKey\": \"{{$guid}}\"\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + }, + { + "name": "Update Shipment Item Quantities", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Official mutation: inventoryShipmentUpdateItemQuantities. Note that the payload field is shipment, not inventoryShipment.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"mutation inventoryShipmentUpdateItemQuantities($id: ID!, $items: [InventoryShipmentUpdateItemQuantitiesInput!]) { inventoryShipmentUpdateItemQuantities(id: $id, items: $items) { shipment { id name status } updatedLineItems { id quantity acceptedQuantity rejectedQuantity unreceivedQuantity } userErrors { field message } } }\",\n \"variables\": {\n \"id\": \"{{shipment_id}}\",\n \"items\": [\n {\n \"shipmentLineItemId\": \"{{shipment_line_item_id}}\",\n \"quantity\": {{shipment_item_quantity_update}}\n }\n ]\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + }, + { + "name": "Remove Shipment Items", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Official mutation: inventoryShipmentRemoveItems. Requires shipment line item IDs.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"mutation inventoryShipmentRemoveItems($id: ID!, $lineItems: [ID!]!) { inventoryShipmentRemoveItems(id: $id, lineItems: $lineItems) { inventoryShipment { id name status } userErrors { field message } } }\",\n \"variables\": {\n \"id\": \"{{shipment_id}}\",\n \"lineItems\": [\n \"{{shipment_line_item_id_2}}\"\n ]\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + }, + { + "name": "Set Shipment Tracking", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Official mutation: inventoryShipmentSetTracking.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"mutation inventoryShipmentSetTracking($id: ID!, $tracking: InventoryShipmentTrackingInput!) { inventoryShipmentSetTracking(id: $id, tracking: $tracking) { inventoryShipment { id name status tracking { trackingNumber company trackingUrl arrivesAt } } userErrors { field message } } }\",\n \"variables\": {\n \"id\": \"{{shipment_id}}\",\n \"tracking\": {\n \"trackingNumber\": \"{{tracking_number}}\",\n \"company\": \"{{tracking_company}}\",\n \"trackingUrl\": \"{{tracking_url}}\",\n \"arrivesAt\": \"{{tracking_arrives_at}}\"\n }\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + }, + { + "name": "Mark Shipment In Transit", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Official mutation: inventoryShipmentMarkInTransit.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"mutation inventoryShipmentMarkInTransit($id: ID!, $dateShipped: DateTime) { inventoryShipmentMarkInTransit(id: $id, dateShipped: $dateShipped) { inventoryShipment { id name status dateShipped } userErrors { field message } } }\",\n \"variables\": {\n \"id\": \"{{shipment_id}}\",\n \"dateShipped\": \"{{date_shipped}}\"\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + }, + { + "name": "Receive Shipment", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Official mutation: inventoryShipmentReceive. This request is aligned to the 2026-01 documented example shape for maximum runnability. If you want to test optional dateReceived or bulkReceiveAction arguments, extend this request after the basic flow succeeds.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"mutation inventoryShipmentReceive($id: ID!, $lineItems: [InventoryShipmentReceiveItemInput!], $idempotencyKey: String!) { inventoryShipmentReceive(id: $id, lineItems: $lineItems) @idempotent(key: $idempotencyKey) { userErrors { field message } inventoryShipment { id name status dateReceived } } }\",\n \"variables\": {\n \"id\": \"{{shipment_id}}\",\n \"lineItems\": [\n {\n \"shipmentLineItemId\": \"{{shipment_line_item_id}}\",\n \"quantity\": {{shipment_receive_quantity}},\n \"reason\": \"{{shipment_receive_reason}}\"\n }\n ],\n \"idempotencyKey\": \"{{$guid}}\"\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "03 Ready-To-Ship Direct Flow", + "description": "Alternate path when you want Shopify to start from a ready-to-ship transfer or to create the shipment directly in transit.", + "item": [ + { + "name": "Create Ready To Ship Transfer", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "const transfer = json?.data?.inventoryTransferCreateAsReadyToShip?.inventoryTransfer;", + "if (transfer?.id) pm.environment.set('transfer_id', transfer.id);", + "if (transfer?.status) pm.environment.set('latest_transfer_status', transfer.status);" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Official mutation: inventoryTransferCreateAsReadyToShip.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"mutation inventoryTransferCreateAsReadyToShip($input: InventoryTransferCreateAsReadyToShipInput!, $idempotencyKey: String!) { inventoryTransferCreateAsReadyToShip(input: $input) @idempotent(key: $idempotencyKey) { inventoryTransfer { id name referenceName status dateCreated } userErrors { field message } } }\",\n \"variables\": {\n \"input\": {\n \"originLocationId\": \"{{origin_location_id}}\",\n \"destinationLocationId\": \"{{destination_location_id}}\",\n \"lineItems\": [\n {\n \"inventoryItemId\": \"{{inventory_item_id}}\",\n \"quantity\": {{inventory_item_quantity}}\n },\n {\n \"inventoryItemId\": \"{{inventory_item_id_2}}\",\n \"quantity\": {{inventory_item_quantity_2}}\n }\n ],\n \"tags\": [\n \"postman\",\n \"ready-to-ship\"\n ],\n \"dateCreated\": \"{{date_created_datetime}}\",\n \"note\": \"{{note}}\",\n \"referenceName\": \"{{reference_name}}\"\n },\n \"idempotencyKey\": \"{{$guid}}\"\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + }, + { + "name": "Create Shipment In Transit", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "const shipment = json?.data?.inventoryShipmentCreateInTransit?.inventoryShipment;", + "if (shipment?.id) pm.environment.set('shipment_id', shipment.id);", + "if (shipment?.status) pm.environment.set('latest_shipment_status', shipment.status);" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Official mutation: inventoryShipmentCreateInTransit.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"mutation inventoryShipmentCreateInTransit($input: InventoryShipmentCreateInput!, $idempotencyKey: String!) { inventoryShipmentCreateInTransit(input: $input) @idempotent(key: $idempotencyKey) { userErrors { field message code } inventoryShipment { id name status dateCreated dateShipped } } }\",\n \"variables\": {\n \"input\": {\n \"movementId\": \"{{transfer_id}}\",\n \"lineItems\": [\n {\n \"inventoryItemId\": \"{{inventory_item_id}}\",\n \"quantity\": {{shipment_item_quantity}}\n }\n ]\n },\n \"idempotencyKey\": \"{{$guid}}\"\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "04 Cleanup And Variations", + "description": "Non-happy-path and variation requests for duplicate and cancel behavior.", + "item": [ + { + "name": "Duplicate Transfer", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "const transfer = json?.data?.inventoryTransferDuplicate?.inventoryTransfer;", + "if (transfer?.id) pm.environment.set('transfer_id', transfer.id);", + "if (transfer?.status) pm.environment.set('latest_transfer_status', transfer.status);" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Official mutation: inventoryTransferDuplicate.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"mutation inventoryTransferDuplicate($id: ID!, $idempotencyKey: String!) { inventoryTransferDuplicate(id: $id) @idempotent(key: $idempotencyKey) { inventoryTransfer { id name referenceName status } userErrors { field message } } }\",\n \"variables\": {\n \"id\": \"{{transfer_id}}\",\n \"idempotencyKey\": \"{{$guid}}\"\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + }, + { + "name": "Cancel Transfer", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "X-Shopify-Access-Token", + "value": "{{access_token}}", + "type": "text" + } + ], + "description": "Official mutation: inventoryTransferCancel.", + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"mutation inventoryTransferCancel($id: ID!) { inventoryTransferCancel(id: $id) { inventoryTransfer { id name referenceName status } userErrors { field message } } }\",\n \"variables\": {\n \"id\": \"{{transfer_id}}\"\n }\n}" + }, + "url": { + "raw": "https://{{store_url}}/admin/api/{{api_version}}/graphql.json", + "protocol": "https", + "host": [ + "{{store_url}}" + ], + "path": [ + "admin", + "api", + "{{api_version}}", + "graphql.json" + ] + } + }, + "response": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-bulk-live-test-evidence-2026-04-11.md b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-bulk-live-test-evidence-2026-04-11.md new file mode 100644 index 00000000..c4fe164d --- /dev/null +++ b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-bulk-live-test-evidence-2026-04-11.md @@ -0,0 +1,269 @@ +# Shopify Transfer Order Bulk Test Results using GraphQL APIs + +## Purpose + +This document records the large-scale Shopify transfer-order tests run on `gorjana-sandbox.myshopify.com` on April 11, 2026. + +The goals of this run were: + +- prove large TO creation behavior on Shopify with real gorjana inventory +- prove shipment and receipt behavior beyond a few sample products +- identify Shopify constraints that appear only when moving from small TO tests to large route tests +- compare the observed inventory movement with the OMS transfer-order process + +Raw evidence is stored under: + +- `runtime/component/shopify-oms-bridge/docs/evidence/shopify_transfer_bulk_tests_2026-04-11` +- `runtime/component/shopify-oms-bridge/docs/evidence/shopify_transfer_bulk_tests_2026-04-11_exec2` +- `runtime/component/shopify-oms-bridge/docs/evidence/shopify_transfer_bulk_tests_2026-04-11_exec_cache` + +## Executive Summary + +- Three large route tests were executed against live gorjana sandbox inventory. +- The final execution set covered `3,285` variants, `14` Shopify transfer batches, and `16,425` units at quantity `5` per item. +- The route types tested were: + - Laguna warehouse to store + - store to store + - store to warehouse +- Shopify did handle bulk transfer creation, shipment creation, in-transit movement, and receipt at this scale. +- Shopify also exposed two important large-scale constraints that must be accounted for before sync: + - inventory items that appear in location inventory data can still fail transfer creation because they do not track inventory + - shipment creation fails if the item is not already stocked and active at the destination location + +## Route Set Executed + +### Route 1: Laguna warehouse to Austin + +- Route code: `LGW-AUS-EXEC2` +- Logical route type: warehouse to store +- Selected variants: `285` +- Quantity per item: `5` +- Total units: `1,425` +- Shopify transfer batches: `2` + +Batch references: + +- `TO-BULK-20260411-LGW-AUS-EXEC2-B01` +- `TO-BULK-20260411-LGW-AUS-EXEC2-B02` + +### Route 2: Austin to Carlsbad + +- Route code: `AUS-CAR-EXEC2` +- Logical route type: store to store +- Selected variants: `1,500` +- Quantity per item: `5` +- Total units: `7,500` +- Shopify transfer batches: `6` + +Batch references: + +- `TO-BULK-20260411-AUS-CAR-EXEC2-B01` +- `TO-BULK-20260411-AUS-CAR-EXEC2-B02` +- `TO-BULK-20260411-AUS-CAR-EXEC2-B03` +- `TO-BULK-20260411-AUS-CAR-EXEC2-B04` +- `TO-BULK-20260411-AUS-CAR-EXEC2-B05` +- `TO-BULK-20260411-AUS-CAR-EXEC2-B06` + +### Route 3: Carlsbad to Laguna warehouse + +- Route code: `CAR-LGW-EXEC2` +- Logical route type: store to warehouse +- Selected variants: `1,500` +- Quantity per item: `5` +- Total units: `7,500` +- Shopify transfer batches: `6` + +Batch references: + +- `TO-BULK-20260411-CAR-LGW-EXEC2-B01` +- `TO-BULK-20260411-CAR-LGW-EXEC2-B02` +- `TO-BULK-20260411-CAR-LGW-EXEC2-B03` +- `TO-BULK-20260411-CAR-LGW-EXEC2-B04` +- `TO-BULK-20260411-CAR-LGW-EXEC2-B05` +- `TO-BULK-20260411-CAR-LGW-EXEC2-B06` + +## Overall Totals + +- Logical routes executed: `3` +- Shopify transfer batches created: `14` +- Variants executed through transfer and shipment flow: `3,285` +- Units executed through transfer and shipment flow: `16,425` + +This is the practical Shopify fan-out effect: + +- one logical large route becomes multiple Shopify transfers +- each Shopify transfer then gets its own shipment +- each shipment then gets its own receive call + +That fan-out is the core orchestration difference from OMS. + +## Inventory Movement Compared With OMS + +### Route 1: `LGW-AUS-EXEC2` + +Inventory totals across the selected `285` variants: + +| Stage | Origin available | Origin reserved | Origin on_hand | Destination available | Destination incoming | Destination on_hand | +| --- | --- | --- | --- | --- | --- | --- | +| Before | `518,506` | `1,425` | `519,972` | `1,553` | `0` | `1,553` | +| After ready | `517,081` | `2,850` | `519,972` | `1,553` | `0` | `1,553` | +| After in transit | `517,081` | `1,425` | `518,547` | `1,553` | `1,425` | `1,553` | +| After receive | `517,081` | `1,425` | `518,547` | `2,978` | `0` | `2,978` | + +Observed behavior: + +- Shopify reserved the route quantity at `READY_TO_SHIP`. +- Shopify moved the route quantity to destination `incoming` at shipment in-transit. +- Shopify moved the same quantity from destination `incoming` to destination `available` at receipt. +- This matches OMS inventory movement directionally. +- Shopify still leaves the receiver interaction shipment-driven, not TO-item-driven. + +### Route 2: `AUS-CAR-EXEC2` + +Inventory totals across the selected `1,500` variants before and through in-transit: + +| Stage | Origin available | Origin reserved | Origin on_hand | Destination available | Destination incoming | Destination on_hand | +| --- | --- | --- | --- | --- | --- | --- | +| Before | `240,750` | `0` | `240,827` | `238,246` | `0` | `238,246` | +| After ready | `233,250` | `7,500` | `240,827` | `238,246` | `0` | `238,246` | +| After in transit | `233,250` | `0` | `233,327` | `238,246` | `7,500` | `238,246` | + +Observed behavior: + +- all six Shopify transfer batches reached `TRANSFERRED` +- all six shipment receipts succeeded +- the immediate final inventory snapshot failed with a network `Connection reset by peer` during long location paging +- a later recovery snapshot was no longer isolated because the Carlsbad-to-Laguna route had already started using the same destination location + +Because of that, the cleanest proof for this route is: + +- all six transfer batches are `TRANSFERRED` +- all six `receivedQuantity` values equal `totalQuantity` +- the in-transit aggregate showed the full `7,500` units in destination `incoming` + +### Route 3: `CAR-LGW-EXEC2` + +Inventory totals across the selected `1,500` variants: + +| Stage | Origin available | Origin reserved | Origin on_hand | Destination available | Destination incoming | Destination on_hand | +| --- | --- | --- | --- | --- | --- | --- | +| Before | `246,849` | `0` | `246,849` | `63,714` | `0` | `64,224` | +| After ready | `239,349` | `7,500` | `246,849` | `63,714` | `0` | `64,224` | +| After in transit | `239,349` | `0` | `239,349` | `63,714` | `7,500` | `64,224` | +| After receive | `239,349` | `0` | `239,349` | `71,214` | `0` | `71,724` | + +Observed behavior: + +- Shopify again followed the same reservation -> in-transit -> receipt inventory pattern seen in smaller tests +- the batch fan-out was `6` Shopify transfers for one logical route +- this route is the strongest large-scale proof that Shopify can mirror the execution layer once items are already valid for both origin and destination + +## Shopify Breaking Points Found During Bulk Testing + +### 1. Transfer creation can fail on non-tracked inventory items + +This was observed during the first large Laguna-to-Austin attempt. + +Evidence: + +- file: `runtime/component/shopify-oms-bridge/docs/evidence/shopify_transfer_bulk_tests_2026-04-11/LGW-AUS/01_create_ready_transfer.json` +- Shopify error: `The inventory item does not track inventory.` + +Impact: + +- location inventory discovery alone is not enough to build a Shopify transfer payload safely +- the line set must be filtered to `tracked=true` before transfer creation +- OMS TO items can include products that need additional Shopify-side validation before mirror creation + +### 2. Shipment creation fails if the item is not stocked at the destination location + +This was observed after the first large Laguna-to-Austin create-only run. + +Evidence: + +- files: + - `runtime/component/shopify-oms-bridge/docs/evidence/shopify_transfer_bulk_tests_2026-04-11/LGW-AUS/01_create_ready_transfer.json` + - `runtime/component/shopify-oms-bridge/docs/evidence/shopify_transfer_bulk_tests_2026-04-11/LGW-AUS/02_create_ready_transfer.json` + - `runtime/component/shopify-oms-bridge/docs/evidence/shopify_transfer_bulk_tests_2026-04-11/LGW-AUS/01_create_shipment_in_transit.json` +- Shopify error code: `INVENTORY_STATE_NOT_ACTIVE` +- Shopify error message: `The item is not stocked at the destination location.` + +Impact: + +- Shopify allowed the ready-to-ship transfer creation +- Shopify then blocked shipment creation +- this means “transfer can be created” is not enough proof that the route is executable +- large-route planning must use the intersection of: + - tracked items + - origin items with enough available quantity + - destination items that are already stocked and active + +### 3. Large-route evidence capture itself is network-sensitive + +Observed behavior: + +- long location-inventory snapshot calls can hit `Connection reset by peer` +- long snapshot calls can also hit socket read timeouts +- this happened on evidence capture, not on the main transfer or receive mutations + +Impact: + +- Shopify mutation success and Shopify reporting reliability are not the same thing +- large-scale reconciliation and evidence collection should include retry logic +- this is another reason OMS should remain the authoritative operational ledger + +## What The Bulk Test Proves About Shopify + +### What Shopify can do + +- create large transfer batches as `READY_TO_SHIP` +- reserve inventory at ready-to-ship at bulk scale +- create in-transit shipments at bulk scale +- move inventory from origin reserved/on-hand to destination incoming at bulk scale +- receive shipment quantities and complete transfer batches at bulk scale + +### What Shopify still does not solve cleanly + +- one logical TO still becomes many Shopify transfers +- one logical execution test still becomes many shipment records and many receive calls +- destination inventory-state preconditions are a Shopify execution constraint that OMS does not expose the same way at TO authoring time +- receiver work remains shipment-driven, not TO-item-driven +- Shopify data-quality constraints such as `tracked=true` still have to be enforced outside the transfer mutation itself + +## OMS Comparison + +What matched OMS: + +- approval-like reservation timing at ready-to-ship +- issue-like inventory movement at shipment in-transit +- receipt moving destination `incoming -> available` + +What remained weaker than OMS: + +- batch fan-out across many transfer records +- shipment-centric execution and receipt +- destination-stocked execution precondition +- external-system retries needed even for evidence capture + +Operational conclusion: + +- Shopify can mirror large transfer execution when the route is prevalidated to Shopify’s rules +- OMS is still the better place to own TO orchestration, route validation, and exception handling + +## Evidence Pointers + +### Create-only failure evidence + +- `runtime/component/shopify-oms-bridge/docs/evidence/shopify_transfer_bulk_tests_2026-04-11/LGW-AUS` + +### Final execution evidence + +- `runtime/component/shopify-oms-bridge/docs/evidence/shopify_transfer_bulk_tests_2026-04-11_exec2/LGW-AUS-EXEC2` +- `runtime/component/shopify-oms-bridge/docs/evidence/shopify_transfer_bulk_tests_2026-04-11_exec2/AUS-CAR-EXEC2` +- `runtime/component/shopify-oms-bridge/docs/evidence/shopify_transfer_bulk_tests_2026-04-11_exec2/CAR-LGW-EXEC2` + +### Cached live location inventory used for route selection + +- `runtime/component/shopify-oms-bridge/docs/evidence/shopify_transfer_bulk_tests_2026-04-11_exec_cache/LGW.json` +- `runtime/component/shopify-oms-bridge/docs/evidence/shopify_transfer_bulk_tests_2026-04-11_exec_cache/AUS.json` +- `runtime/component/shopify-oms-bridge/docs/evidence/shopify_transfer_bulk_tests_2026-04-11_exec_cache/CAR.json` diff --git a/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-external-end-live-test-evidence-2026-04-12.md b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-external-end-live-test-evidence-2026-04-12.md new file mode 100644 index 00000000..dd8d6263 --- /dev/null +++ b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-external-end-live-test-evidence-2026-04-12.md @@ -0,0 +1,151 @@ +# Shopify Transfer Order Test Results + +## Purpose + +This document records the live Shopify tests for two one-sided transfer scenarios: + +1. outbound from a Shopify location to an external destination managed outside Shopify +2. inbound to a Shopify location from an external origin managed outside Shopify + +The goal was to prove whether Shopify `InventoryTransfer` and `InventoryShipment` support transfers where one side of the movement is omitted. + +## Environment + +- API version: `2026-01` +- SKU: `207-113-G` +- Quantity tested: `1` +- Shopify location used as origin: `Atlanta` +- Shopify location used as destination: `Austin` + +## Executive Result + +Both scenarios ran successfully. + +Shopify accepted: + +- a ready-to-ship transfer with `originLocationId` set and `destinationLocationId` omitted +- a ready-to-ship transfer with `destinationLocationId` set and `originLocationId` omitted + +In both cases Shopify also allowed an immediate `inventoryShipmentCreate` against the created transfer, and the shipment line item matched the transfer line item. + +## Scenario 1: Outbound From Shopify Location To External Destination + +Intent: + +- ship inventory out of one Shopify location +- destination exists outside Shopify and is not represented as a Shopify location + +Mutation shape: + +- `originLocationId = Atlanta` +- `destinationLocationId` omitted +- `1` line item +- immediate `inventoryShipmentCreate` using the transfer id as `movementId` + +Created records: + +- Transfer: `#T0028` +- Reference: `TO-EXT-OUT-20260412-A` +- Transfer id: `gid://shopify/InventoryTransfer/3875110956` +- Shopify admin: `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3875110956` +- Shipment: `#T0028-1` +- Shipment id: `gid://shopify/InventoryShipment/959316012` + +What Shopify returned: + +- transfer status: `READY_TO_SHIP` +- transfer `origin.name = Atlanta` +- transfer `destination = null` +- transfer line item: + - inventory item `46295996661804` + - SKU `207-113-G` + - `totalQuantity = 1` +- transfer `shipments` contained: + - shipment id `959316012` + - shipment name `#T0028-1` + - shipment status `DRAFT` +- shipment line item: + - inventory item `46295996661804` + - SKU `207-113-G` + - `quantity = 1` + - `unreceivedQuantity = 1` + +Inventory snapshot: + +| Stage | Atlanta available | Atlanta reserved | Atlanta on_hand | Austin available | Austin incoming | Austin on_hand | +| --- | --- | --- | --- | --- | --- | --- | +| Before scenario | `196` | `0` | `196` | `13` | `1` | `13` | +| After ready transfer and draft shipment | `195` | `1` | `196` | `13` | `1` | `13` | + +Conclusion: + +- Shopify supports an outbound transfer whose destination is external to Shopify +- at `READY_TO_SHIP`, Shopify reserved origin inventory even though no Shopify destination existed + +## Scenario 2: Inbound To Shopify Location From External Origin + +Intent: + +- receive stock into a Shopify location +- origin exists only in OMS or another external system and is not represented as a Shopify location + +Mutation shape: + +- `destinationLocationId = Austin` +- `originLocationId` omitted +- `1` line item +- immediate `inventoryShipmentCreate` using the transfer id as `movementId` + +Created records: + +- Transfer: `#T0029` +- Reference: `TO-EXT-IN-20260412-B` +- Transfer id: `gid://shopify/InventoryTransfer/3875143724` +- Shopify admin: `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3875143724` +- Shipment: `#T0029-1` +- Shipment id: `gid://shopify/InventoryShipment/959348780` + +What Shopify returned: + +- transfer status: `READY_TO_SHIP` +- transfer `origin = null` +- transfer `destination.name = Austin` +- transfer line item: + - inventory item `46295996661804` + - SKU `207-113-G` + - `totalQuantity = 1` +- transfer `shipments` contained: + - shipment id `959348780` + - shipment name `#T0029-1` + - shipment status `DRAFT` +- shipment line item: + - inventory item `46295996661804` + - SKU `207-113-G` + - `quantity = 1` + - `unreceivedQuantity = 1` + +Inventory snapshot: + +| Stage | Atlanta available | Atlanta reserved | Atlanta on_hand | Austin available | Austin incoming | Austin on_hand | +| --- | --- | --- | --- | --- | --- | --- | +| Before scenario | `195` | `1` | `196` | `13` | `1` | `13` | +| After ready transfer and draft shipment | `195` | `1` | `196` | `13` | `1` | `13` | + +Conclusion: + +- Shopify supports an inbound transfer whose origin is external to Shopify +- Shopify allowed the transfer header and the shipment header even without a Shopify origin +- creating the ready transfer and draft shipment did not yet change Austin inventory in this test + +## Final Conclusion + +These two specific scenarios are supported by Shopify: + +- Shopify location -> external destination +- external origin -> Shopify location + +What remains separate from this is later execution behavior such as: + +- marking these shipments in transit should move the inventory +- receiving the inbound flow +- comparing inventory movement after in-transit and receipt diff --git a/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-gap-proof-evidence-2026-04-11.md b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-gap-proof-evidence-2026-04-11.md new file mode 100644 index 00000000..3b21c91e --- /dev/null +++ b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-gap-proof-evidence-2026-04-11.md @@ -0,0 +1,212 @@ +# Shopify Transfer Order Gap Proof + +## Purpose + +This document records the follow-up Shopify tests run on April 11, 2026 to prove the remaining OMS Transfer Order gap claims that were still pending after the happy-path and bulk execution runs. + +The focus here is not basic transfer execution. The focus is proving the cases where OMS behavior cannot be represented cleanly on Shopify. + +Raw API evidence is stored under: + +- `runtime/component/shopify-oms-bridge/docs/evidence/shopify_transfer_gap_tests_2026-04-11` + +## Test Data Used + +### Shopify shop + +- Shop: `gorjana sandbox` +- Domain: `gorjana-sandbox.myshopify.com` +- API version: `2026-01` + +### Locations and SKU + +- Origin: + - `gid://shopify/Location/71900561452` + - `Atlanta` +- Destination: + - `gid://shopify/Location/63145443372` + - `Austin` +- Inventory item: + - `gid://shopify/InventoryItem/46295996661804` + - SKU `207-113-G` + +### Transfers created for this proof run + +- `#T0026` + - reference: `TO-LIVE-20260411-D` + - purpose: partial receipt plus post-shipment close and receive-shape proof + - Shopify admin: `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3875012652` +- `#T0027` + - reference: `TO-LIVE-20260411-E` + - purpose: cancel versus reject proof + - Shopify admin: `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3875045420` + +These transfers were left in Shopify. + +## Executive Summary + +This run proved the remaining important gaps directly with live API behavior: + +- Shopify has `inventoryShipmentReceive`, but no transfer-level receive mutation. +- Shopify receive input accepts only `shipmentLineItemId`, `quantity`, and `reason`. +- Passing a transfer id to `inventoryShipmentReceive` fails with `RESOURCE_NOT_FOUND`. +- Attempting to include `inventoryItemId` in shipment receive input fails validation because that field does not exist on the input type. +- After a partial receipt, `inventoryTransferRemoveItems` cannot be used to close the remaining quantity because the transfer is already `IN_PROGRESS`. +- Shopify cancel exists and works, but the `InventoryTransfer` type still has no reject-reason or reject-location fields, so this is not equivalent to OMS reject-to-parking behavior. + +## Scenario Proofs + +## `TO_Receive_Only` and receipt without shipment linkage + +OMS can create a receive-only TO and then receive against TO items. + +Shopify proof: + +- mutation root introspection showed `inventoryShipmentReceive` +- mutation root introspection did not show any `inventoryTransferReceive` mutation +- `InventoryShipmentReceiveItemInput` has only: + - `shipmentLineItemId` + - `quantity` + - `reason` +- attempting to call `inventoryShipmentReceive` with a transfer id returned: + - `Invalid id: gid://shopify/InventoryTransfer/3875012652` + - error code `RESOURCE_NOT_FOUND` + +Operational meaning: + +- receive is shipment-driven in Shopify +- there is no transfer-level receive action to mirror OMS TO-item receiving +- this is the concrete reason `TO_Receive_Only` cannot be mirrored cleanly when OMS does not have a Shopify shipment context + +Key evidence: + +- `39_mutation_root_introspection.json` +- `40_receive_input_type_introspection.json` +- `48_attempt_receive_with_transfer_id_D.json` + +## Unexpected-item receipt + +OMS can receive an unexpected item without an `orderItemSeqId`. + +Shopify proof: + +- attempted to send `inventoryItemId` inside `InventoryShipmentReceiveItemInput` +- Shopify rejected the request before execution +- exact validation message: + - `Field is not defined on InventoryShipmentReceiveItemInput` + +Operational meaning: + +- Shopify shipment receive cannot accept a new item not already represented as a shipment line +- this is not just a missing UI affordance +- the GraphQL input model itself does not allow that OMS behavior + +Key evidence: + +- `40_receive_input_type_introspection.json` +- `49_attempt_unexpected_item_field_D.json` + +## Receiver-driven close after partial receipt + +OMS allows the receiver to partially receive and then close the remaining expected quantity. + +Shopify proof flow: + +1. created ready-to-ship transfer `#T0026` +2. created shipment `#T0026-1` with quantity `2` +3. marked the shipment `IN_TRANSIT` +4. partially received quantity `1` +5. attempted `inventoryTransferRemoveItems` on the remaining line + +Live result: + +- partial receive succeeded +- shipment became `PARTIALLY_RECEIVED` +- transfer became `IN_PROGRESS` +- transfer line showed: + - `shippedQuantity = 2` + - `shippableQuantity = 0` +- `inventoryTransferRemoveItems` failed with: + - `Transfer can only have its items removed in a Draft or Ready-to-ship status.` + +Operational meaning: + +- once shipment linkage has advanced the transfer to `IN_PROGRESS`, Shopify no longer exposes a remove-and-close path for the remaining quantity +- this is weaker than OMS receive-and-close and weaker than OMS close-fulfillment after partial execution + +Key evidence: + +- `41_create_ready_transfer_D.json` +- `43_create_shipment_D.json` +- `45_mark_in_transit_D.json` +- `46_receive_partial_D.json` +- `47_attempt_remove_after_partial_D.json` +- `50_shipment_detail_after_partial_D.json` +- `51_transfer_detail_after_partial_D.json` + +## Inventory state after the failed close attempt + +Current inventory snapshot for SKU `207-113-G` after the `#T0026` partial receive: + +- Atlanta: + - `available = 196` + - `incoming = 0` + - `reserved = 0` + - `on_hand = 196` +- Austin: + - `available = 13` + - `incoming = 1` + - `reserved = 0` + - `on_hand = 13` + +Operational meaning: + +- one unit has been received into Austin availability +- one unit is still open in Austin incoming +- the failed close attempt did not resolve that remaining incoming quantity +- OMS can close that residual expectation explicitly; Shopify could not in this tested flow + +Key evidence: + +- `55_inventory_levels_after_gap_tests_item_207-113-G.json` + +## Reject versus cancel + +OMS reject is stronger than a simple cancel because it carries reject routing and reject state semantics. + +Shopify proof flow: + +1. created draft transfer `#T0027` +2. cancelled the draft transfer successfully +3. introspected the `InventoryTransfer` type fields + +Live result: + +- cancel succeeded +- transfer status became `CANCELED` +- `InventoryTransfer` fields did not include reject-specific fields such as reject reason or reject location + +Operational meaning: + +- Shopify cancel is a valid partial equivalent for OMS cancel before execution starts +- Shopify cancel is not the same as OMS reject-to-`REJECTED_ITM_PARKING` + +Key evidence: + +- `52_inventoryTransfer_type_introspection.json` +- `53_create_draft_transfer_E.json` +- `54_cancel_transfer_E.json` +- `cancel_summary.json` + +## Final Conclusion + +The earlier execution tests had already proven that Shopify can mirror the basic transfer lifecycle when shipment context exists. + +This follow-up run proved the opposite side as well: + +- Shopify cannot receive at transfer level +- Shopify cannot receive arbitrary unexpected items through shipment receive +- Shopify cannot cleanly express receiver-driven close after partial receipt once shipment execution has started +- Shopify cancel is not OMS reject + +That closes the main proof gap between the OMS scenarios and the Shopify transfer model. diff --git a/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-inventory-transfer-mapping.md b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-inventory-transfer-mapping.md new file mode 100644 index 00000000..e4d13dae --- /dev/null +++ b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-inventory-transfer-mapping.md @@ -0,0 +1,330 @@ +# Shopify Transfer Order Inventory Transfer Mapping + +## Purpose + +This document maps OMS Transfer Order events to Shopify `InventoryTransfer` and `InventoryShipment` mutations so that OMS behavior can be mirrored on Shopify as closely as possible. + +The focus is event mapping, not code structure. + +## Executive Summary + +OMS creates a Transfer Order for all three TO flow types: + +- `TO_Fulfill_Only` +- `TO_Receive_Only` +- `TO_Fulfill_And_Receive` + +That includes Warehouse to Store. The difference between the three flows is not whether a TO exists. The difference is what OMS does with that TO after creation. + +The practical Shopify mapping is: + +- mirror OMS TO creation as Shopify draft transfer creation +- mirror OMS approval as Shopify ready-to-ship transfer state +- mirror OMS shipment creation and ship events only when OMS actually performs those events +- mirror OMS receipt only when Shopify has a shipment context for that receipt + +This means: + +- Store to Store maps best +- Store to Warehouse maps well for creation and fulfillment-side mirroring +- Warehouse to Store does create a TO in OMS and can be mirrored as a transfer header on Shopify, but receipt mirroring still depends on shipment existence in Shopify + +## OMS Model That Drives The Mapping + +### TO Flow Types + +| OMS flow | Typical route | OMS ownership | +| --- | --- | --- | +| `TO_Fulfill_Only` | Store to Warehouse | OMS owns fulfillment side | +| `TO_Receive_Only` | Warehouse to Store | OMS owns receiving side | +| `TO_Fulfill_And_Receive` | Store to Store | OMS owns both fulfillment and receiving | + +### Important correction + +Warehouse to Store is still a Transfer Order in OMS. + +OMS creates the TO in `ORDER_CREATED` first. The `statusFlowId` then decides which approval path and item-state transitions apply afterward. + +For `TO_Receive_Only`: + +- the TO is created in OMS +- approval moves items from `ITEM_CREATED` to `ITEM_PENDING_RECEIPT` +- OMS then receives against TO items + +So the Shopify mapping should not treat Warehouse to Store as “no TO exists”. The correct statement is: + +- a TO exists in OMS +- Shopify can mirror the TO header and approval state +- Shopify cannot fully mirror OMS receiving behavior unless there is a usable shipment context in Shopify + +### OMS Lifecycle + +| OMS event | OMS service | OMS result | +| --- | --- | --- | +| Create TO | `create#TransferOrder` | Creates Transfer Order in `ORDER_CREATED` with `ITEM_CREATED` lines | +| Update draft item | `update#TransferOrderItem` | Changes draft item quantity while order is still `ORDER_CREATED` | +| Add draft item | `add#TransferOrderItem` | Adds a new draft item while order is still `ORDER_CREATED` | +| Approve store-fulfilled TO | `approve#StoreFulfillTransferOrder` | Moves order to `ORDER_APPROVED`, items to `ITEM_PENDING_FULFILL` | +| Approve warehouse-fulfilled TO | `approve#WhFulfillTransferOrder` | Moves order to `ORDER_APPROVED`, items to `ITEM_PENDING_RECEIPT` | +| Create transfer shipment | `create#TransferOrderShipment` | Creates outbound transfer shipment in OMS | +| Ship transfer shipment | `ship#TransferOrderShipment` | Issues inventory and marks shipment shipped | +| Receive TO | `receive#TransferOrder` | Records receipts against TO items | +| Reject TO | `reject#TransferOrder` | Rejects the full TO before fulfillment starts | +| Close fulfillment | `close#TransferOrderItemFulfillment` | Closes remaining fulfillable quantity | +| Cancel TO | `cancel#TransferOrder` | Cancels TO before shipment or receipt work starts | + +## Shopify Model Used For The Mapping + +| Shopify object or mutation | Use in the mapping | +| --- | --- | +| `inventoryTransferCreate` | Create draft transfer | +| `inventoryTransferCreateAsReadyToShip` | Create ready-to-ship transfer directly | +| `inventoryTransferMarkAsReadyToShip` | Move draft transfer to ready-to-ship | +| `inventoryTransferSetItems` | Replace transfer line set before shipment execution starts | +| `inventoryTransferRemoveItems` | Remove remaining shippable quantity before shipment linkage blocks it | +| `inventoryTransferCancel` | Cancel transfer before shipment or receipt execution | +| `inventoryShipmentCreate` | Create draft shipment under a transfer | +| `inventoryShipmentCreateInTransit` | Create shipment directly in transit | +| `inventoryShipmentReceive` | Receive shipment line quantities | + +## Mapping Principles + +### 1. OMS stays the source of truth + +Shopify should mirror OMS events. Shopify should not decide the TO business flow. + +### 2. Create the Shopify transfer when OMS creates the TO + +This applies to all three OMS flow types, including `TO_Receive_Only`. + +### 3. Use approval to move Shopify from draft to ready-to-ship + +This gives the cleanest header-state parity between OMS and Shopify. + +### 4. Only mirror shipment execution when shipment execution exists in OMS + +For `TO_Fulfill_Only` and `TO_Fulfill_And_Receive`, OMS shipment events can be mirrored directly. + +For `TO_Receive_Only`, OMS may receive without owning the fulfillment-side shipment creation. In that case Shopify shipment requirements become the main gap. + +### 5. Receipt is the hardest area to map + +OMS receives against TO items. + +Shopify receives against shipment line items. + +That is the main workflow mismatch. + +## Event-To-Mutation Mapping + +| OMS event | When it happens in OMS | Shopify mutation or action | Recommended mapping | Notes | +| --- | --- | --- | --- | --- | +| Create TO | `ORDER_CREATED` | `inventoryTransferCreate` | Create draft transfer | Use for `TO_Fulfill_Only`, `TO_Receive_Only`, and `TO_Fulfill_And_Receive` so the Shopify header exists from the same starting point as OMS | +| Update draft item | Draft TO edit | `inventoryTransferSetItems` | Replace full transfer line set | Keep this only before shipment execution starts | +| Add draft item | Draft TO edit | `inventoryTransferSetItems` | Replace full transfer line set | Shopify line identity is inventory-item based, so resend the full desired line set | +| Approve store-fulfilled TO | `approve#StoreFulfillTransferOrder` | `inventoryTransferMarkAsReadyToShip` or `inventoryTransferCreateAsReadyToShip` | Move transfer to ready-to-ship | Mirrors OMS approval for `TO_Fulfill_Only` and `TO_Fulfill_And_Receive` | +| Approve warehouse-fulfilled TO | `approve#WhFulfillTransferOrder` | `inventoryTransferMarkAsReadyToShip` or `inventoryTransferCreateAsReadyToShip` | Move transfer to ready-to-ship | This is the corrected Warehouse to Store mapping. The TO exists in OMS, so the Shopify transfer header should also move to ready-to-ship even though OMS is primarily controlling receipt next | +| Create transfer shipment | `create#TransferOrderShipment` | `inventoryShipmentCreate` | Create draft shipment | Use when OMS explicitly creates the shipment and you want shipment-level parity on Shopify | +| Ship transfer shipment | `ship#TransferOrderShipment` | `inventoryShipmentCreateInTransit` or `inventoryShipmentMarkInTransit` | Move shipment to in-transit | Use `inventoryShipmentCreateInTransit` when no draft shipment mirror is needed | +| Receive TO against shipment-backed quantity | `receive#TransferOrder` | `inventoryShipmentReceive` | Receive shipment line items | Works when Shopify has the shipment and shipment line context | +| Receive TO without shipment reference | `receive#TransferOrder` | No clean equivalent | Keep OMS as truth | OMS can receive against TO items even when shipment linkage is absent; Shopify cannot do that | +| Receive one TO item that was split across multiple shipments | `receive#TransferOrder` | Multiple `inventoryShipmentReceive` calls | Partial equivalent only | OMS can receive once at TO-item level; Shopify requires one receive call per shipment | +| Over-receipt | `receive#TransferOrder` | `inventoryShipmentReceive` | Partial equivalent only | Shopify does allow over-receipt, but only through shipment receive, not as TO-item-only receipt | +| Unexpected item receipt | `receive#TransferOrder` | No clean equivalent | Keep OMS as truth | OMS can receive an item not originally on the TO; Shopify shipment receive cannot | +| Receive and close | `receive#TransferOrder` with close semantics | No clean equivalent | Keep OMS as truth | Shopify receives quantity but does not expose the same receiver-driven close behavior | +| Close fulfillment | `close#TransferOrderItemFulfillment` | `inventoryTransferRemoveItems` at best | Partial equivalent only | Use only before shipment linkage makes the line immutable on Shopify | +| Reject TO | `reject#TransferOrder` | `inventoryTransferCancel` at best | Partial equivalent only | Shopify cancel does not represent reject-to-parking behavior | +| Cancel TO | `cancel#TransferOrder` | `inventoryTransferCancel` | Cancel transfer | Good fit while no shipment or receipt execution has started | + +## Flow-Specific Mapping + +### `TO_Fulfill_Only` mapped to Shopify + +OMS sequence: + +1. Create TO +2. Approve store-fulfilled TO +3. Create transfer shipment +4. Ship transfer shipment + +Recommended Shopify sequence: + +1. `inventoryTransferCreate` +2. `inventoryTransferMarkAsReadyToShip` +3. `inventoryShipmentCreate` +4. `inventoryShipmentMarkInTransit` or `inventoryShipmentCreateInTransit` + +Notes: + +- this is a strong fit for header and fulfillment-side mirroring +- OMS usually does not need OMS-side receipt completion in this flow +- if the receiving side is external, Shopify receipt does not need to be driven from OMS unless that is required for reporting + +### `TO_Receive_Only` mapped to Shopify + +OMS sequence: + +1. Create TO +2. Approve warehouse-fulfilled TO +3. Receive TO items + +Recommended Shopify sequence: + +1. `inventoryTransferCreate` +2. `inventoryTransferMarkAsReadyToShip` +3. `inventoryShipmentReceive` only if the shipment already exists in Shopify + +Notes: + +- this is the corrected Warehouse to Store mapping +- the TO should exist on Shopify because the TO does exist in OMS +- the gap is not TO creation +- the gap is that OMS receipt is TO-item-driven while Shopify receipt is shipment-driven +- if external fulfillment does not create shipment records in Shopify, OMS receipt has no direct Shopify mutation equivalent + +### `TO_Fulfill_And_Receive` mapped to Shopify + +OMS sequence: + +1. Create TO +2. Approve store-fulfilled TO +3. Create transfer shipment +4. Ship transfer shipment +5. Receive TO + +Recommended Shopify sequence: + +1. `inventoryTransferCreate` +2. `inventoryTransferMarkAsReadyToShip` +3. `inventoryShipmentCreate` +4. `inventoryShipmentMarkInTransit` or `inventoryShipmentCreateInTransit` +5. `inventoryShipmentReceive` + +Notes: + +- this is the best end-to-end fit +- the main remaining gap is still TO-item-based receiving versus shipment-based receiving + +## What Must Be True Before Posting To Shopify + +The live tests showed that transfer creation and shipment execution have stricter Shopify preconditions than OMS TO creation. + +Before posting a TO line to Shopify, the integration should confirm: + +1. the product is mapped to a Shopify inventory item +2. the Shopify inventory item tracks inventory +3. the source facility is mapped to a Shopify location +4. the destination facility is mapped to a Shopify location +5. for shipment execution, the item is already stocked and active at the destination location + +Without these checks: + +- transfer creation can fail because the inventory item does not track inventory +- shipment creation can fail with destination inventory-state errors even after transfer creation succeeded + +## Inventory Behavior To Expect On Shopify + +The live tests showed this pattern: + +1. Draft transfer creation does not move inventory. +2. Ready-to-ship reduces origin `available` and increases origin `reserved`. +3. In-transit reduces origin `on_hand`, releases origin `reserved`, and increases destination `incoming`. +4. Receipt reduces destination `incoming` and increases destination `available`. + +That sequence is close enough to OMS to use Shopify as an execution mirror. + +## Large TO Handling + +Shopify does not allow one very large OMS TO to be posted as one transfer payload. + +The immediate constraint is the mutation array limit: + +- one mutation input array can hold at most `250` line items + +For the provided TO sample `M111629`: + +- distinct products: `1,548` +- required Shopify transfer batches: `7` + +So the mapping for a large OMS TO is: + +1. keep one OMS TO as the business document +2. split the Shopify mirror into deterministic transfer batches +3. apply later shipment and receipt events batch by batch + +## Recommended Posting Sequence + +### For a new sync + +1. Read the OMS TO. +2. Read `statusFlowId`. +3. Build the Shopify line set from OMS items. +4. Validate location mapping and inventory-item mapping. +5. Split the line set into `250`-line batches if needed. +6. Create Shopify draft transfers for each batch. + +### When OMS approves the TO + +1. Find all Shopify transfer batches for the TO. +2. Move them to ready-to-ship. + +### When OMS ships + +1. Find the correct Shopify transfer batch or batches. +2. Create or update the Shopify shipment representation. +3. Move the shipment to in-transit. + +### When OMS receives + +1. If the receipt can be tied to Shopify shipment lines, call `inventoryShipmentReceive`. +2. If OMS received against TO items without shipment context, do not force a fake Shopify receive. +3. Keep OMS as the receipt truth for that case and record the mismatch explicitly. + +## Proposed Manual Sync Contract + +This section defines the expected behavior of a future sync service. + +### Input + +- `orderId` +- optional Shopify remote or shop override +- optional force-resync flag + +### Output + +- resolved Shopify shop +- total Shopify transfer batches used for the TO +- transfer ids created or reused +- target state reached on Shopify + +### Rules + +- one OMS TO can map to many Shopify transfers +- the mapping key should be `orderId + batch sequence` +- reruns should be idempotent +- do not partially post a TO when required mappings are missing +- do not try to make Shopify receive events represent OMS receipt behavior that Shopify cannot actually model + +## References + +- `runtime/component/oms/service/co/hotwax/orderledger/order/TransferOrderServices.xml` +- `runtime/component/poorti/service/co/hotwax/poorti/TransferOrderFulfillmentServices.xml` +- `runtime/component/oms/service/oms.rest.xml` +- `runtime/component/poorti/service/poorti.rest.xml` +- `runtime/component/oms/data/TransferOrderSeedData.xml` +- https://raw.githubusercontent.com/saastechacademy/foundation/main/project-ideas/fulfillment-center-mgmt/readme.md +- https://raw.githubusercontent.com/saastechacademy/foundation/main/project-ideas/fulfillment-center-mgmt/receiveTransferOrder.md +- https://raw.githubusercontent.com/saastechacademy/foundation/main/project-ideas/fulfillment-center-mgmt/closeTransferOrderItemFulfillment.md +- https://raw.githubusercontent.com/saastechacademy/foundation/main/project-ideas/fulfillment-center-mgmt/rejectTransferOrder.md +- https://shopify.dev/docs/api/admin-graphql/latest/objects/inventorytransfer +- https://shopify.dev/docs/api/admin-graphql/latest/objects/inventoryshipment +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryTransferCreate +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryTransferCreateAsReadyToShip +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryTransferMarkAsReadyToShip +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryTransferSetItems +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryTransferRemoveItems +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryTransferCancel +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryShipmentCreate +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryShipmentCreateInTransit +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryShipmentReceive +- https://shopify.dev/docs/api/usage/limits diff --git a/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-live-test-evidence-2026-04-11.md b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-live-test-evidence-2026-04-11.md new file mode 100644 index 00000000..f1131c79 --- /dev/null +++ b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-live-test-evidence-2026-04-11.md @@ -0,0 +1,250 @@ +# Shopify Transfer Order Test Results using GraphQL APIs + +## Purpose + +This document records the live Shopify transfer-order tests run against `gorjana-sandbox.myshopify.com` on April 11, 2026. + +The goals were: + +- prove the current Shopify transfer and shipment flow with live API evidence +- capture inventory impact at each major step and compare it to OMS flow semantics +- verify which previously documented gaps are true live gaps and which need correction + +Raw API responses are stored under: + +- `runtime/component/shopify-oms-bridge/docs/evidence/shopify_transfer_tests_2026-04-11` + +## Environment + +### Shopify shop + +- Shop: `gorjana sandbox` +- Domain: `gorjana-sandbox.myshopify.com` +- API version: `2026-01` + +### Live token scope result + +Confirmed present on April 11, 2026: + +- `read_inventory` +- `write_inventory` +- `read_locations` +- `read_inventory_transfers` +- `write_inventory_transfers` +- `read_inventory_shipments` +- `write_inventory_shipments` +- `read_inventory_shipments_received_items` +- `write_inventory_shipments_received_items` + +## Test Data Used + +### Locations + +- Origin: + - `gid://shopify/Location/71900561452` + - `Atlanta` +- Destination: + - `gid://shopify/Location/63145443372` + - `Austin` + +### Inventory item + +- Inventory item: + - `gid://shopify/InventoryItem/46295996661804` + - SKU: `207-113-G` + +### Shopify transfers created during this run + +- `#T0007` + - reference: `TO-LIVE-20260411-A` + - purpose: full single-shipment happy path with partial and final receipt +- `#T0008` + - reference: `TO-LIVE-20260411-B` + - purpose: one transfer split into two shipments, then received shipment by shipment +- `#T0009` + - reference: `TO-LIVE-20260411-C` + - purpose: over-receipt probe + +These transfers were intentionally left in Shopify at the user’s request. + +## Live Results Summary + +| Scenario | Live result | Classification | +| --- | --- | --- | +| Create draft transfer | succeeded | `Clean Equivalent` | +| Mark ready to ship | succeeded | `Clean Equivalent` | +| Create draft shipment | succeeded | `Clean Equivalent` | +| Set shipment tracking | succeeded | `Clean Equivalent` | +| Mark shipment in transit | succeeded | `Clean Equivalent` | +| Partial receive | succeeded | `Clean Equivalent` | +| Final receive | succeeded | `Clean Equivalent` | +| Two shipments under one transfer | succeeded | `Partial Equivalent` | +| Receive two shipments in one TO-item-style action | not available; receive is shipment-id based | `No Clean Equivalent` | +| Over-receipt against shipment line | succeeded live | `Partial Equivalent` | + +## Inventory Impact Compared With OMS + +### Single-shipment happy path: `#T0007` + +Item: + +- SKU `207-113-G` +- Quantity `2` +- Origin `Atlanta` +- Destination `Austin` + +### Inventory snapshots + +| Stage | Atlanta available | Atlanta reserved | Atlanta on_hand | Austin available | Austin incoming | Austin on_hand | +| --- | --- | --- | --- | --- | --- | --- | +| Before create | `203` | `0` | `203` | `11` | `0` | `11` | +| After draft create | `203` | `0` | `203` | `11` | `0` | `11` | +| After ready to ship | `201` | `2` | `203` | `11` | `0` | `11` | +| After shipment draft create | `201` | `2` | `203` | `11` | `0` | `11` | +| After mark in transit | `201` | `0` | `201` | `11` | `2` | `11` | +| After partial receive of `1` | `201` | `0` | `201` | `12` | `1` | `12` | +| After final receive of `1` | `201` | `0` | `201` | `13` | `0` | `13` | + +### OMS comparison + +| Shopify live behavior | OMS comparison | +| --- | --- | +| Draft transfer creation does not change inventory | Matches OMS TO creation behavior | +| `READY_TO_SHIP` reduces origin `available` and increases origin `reserved` | Matches OMS approval and reservation semantics more closely than previously assumed | +| Draft shipment creation causes no further inventory change | Consistent with OMS shipment-create being a staging step, not inventory issue | +| `IN_TRANSIT` releases origin `reserved`, reduces origin `on_hand`, and increases destination `incoming` | Matches OMS ship behavior more closely than a pure header-state transition | +| Partial receipt decreases destination `incoming` and increases destination `available`/`on_hand` only by the received quantity | Matches OMS partial receipt semantics at a quantity level | +| Final receipt consumes the remaining destination `incoming` and fully increases destination `available`/`on_hand` | Matches OMS completion behavior at inventory level | + +### Key conclusion + +Shopify’s live inventory behavior is closer to the OMS transfer lifecycle than the earlier pre-scope analysis suggested: + +- reservation happens at ready-to-ship +- issue happens at in-transit +- receipt moves incoming to available incrementally + +The main differences are not the core inventory movement itself. The main differences remain: + +- receive is shipment-scoped instead of TO-item-scoped +- OMS has richer control for receive-only, receive-and-close, reject-to-parking, and close-fulfillment flows + +## Transfer And Shipment State Evidence + +### `#T0007` happy path + +Observed state path: + +- Transfer: `DRAFT` -> `READY_TO_SHIP` -> `TRANSFERRED` +- Shipment: `DRAFT` -> `IN_TRANSIT` -> `PARTIALLY_RECEIVED` -> `RECEIVED` + +Key evidence files: + +- `02_create_draft_transfer_A.json` +- `04_mark_ready_transfer_A.json` +- `06_create_shipment_draft_A.json` +- `10_mark_shipment_in_transit_A.json` +- `12_receive_partial_shipment_A.json` +- `15_receive_final_shipment_A.json` +- `18_transfer_detail_after_full_receive_A.json` + +## Multi-Shipment Evidence + +### `#T0008` one transfer with two shipments + +Observed behavior: + +- one transfer was created with quantity `2` +- two separate shipments were created, each with quantity `1` +- the transfer moved to `IN_PROGRESS` while one shipment was received and the other remained `IN_TRANSIT` +- the transfer moved to `TRANSFERRED` only after both shipments were separately received + +Key evidence: + +- `22_transfer_detail_with_two_shipments_B.json` +- `26_transfer_detail_after_receiving_shipment1_B.json` +- `29_transfer_detail_after_receiving_shipment2_B.json` + +### OMS comparison + +OMS receiving is TO-item-centric: + +- the receiver can work against the TO item +- OMS can internally split one received quantity across multiple eligible shipments + +Shopify receiving is shipment-centric: + +- each receive mutation is tied to one `inventoryShipment` +- each shipment must be received in its own receive call + +This is a real live gap in workflow shape even though the net inventory movement can still be made correct. + +## Over-Receipt Evidence + +### `#T0009` over-receipt probe + +Transfer setup: + +- transfer quantity: `1` +- shipment quantity: `1` +- receive request quantity: `2` + +Live result: + +- Shopify accepted the receive call +- shipment line `acceptedQuantity` became `2` even though shipment line `quantity` was `1` +- transfer `receivedQuantity` became `2` even though transfer `totalQuantity` was `1` +- destination inventory increased by `2` + +Key evidence: + +- `35_attempt_over_receive_C.json` +- `37_shipment_detail_after_over_receive_C.json` +- `38_transfer_detail_after_over_receive_C.json` + +### OMS comparison + +This materially changes the earlier assumption: + +- Shopify does allow over-receipt live +- but the over-receipt is still represented through shipment receipt records, not as OMS-style TO-item-only receipt without shipment linkage + +So the corrected conclusion is: + +- over-receipt is not absent in Shopify +- over-receipt is present, but it is shipment-scoped rather than TO-item-scoped + +## Additional Gap Proof Run + +The remaining exception-path gaps were tested in a follow-up run on April 11, 2026. + +That proof set is documented separately in: + +- `runtime/component/shopify-oms-bridge/docs/shopify_transfer_order_gap_proof_evidence_2026-04-11.md` + +The follow-up run proved: + +1. `TO_Receive_Only` is still not a clean Shopify equivalent + - Shopify has `inventoryShipmentReceive`, but no transfer-level receive mutation +2. Receipt without shipment linkage is not supported + - passing a transfer id to `inventoryShipmentReceive` failed with `RESOURCE_NOT_FOUND` +3. Unexpected-item receipt is not supported through shipment receive + - `InventoryShipmentReceiveItemInput` does not allow `inventoryItemId` +4. Receiver-driven close after partial receipt is not supported cleanly + - after partial receipt, `inventoryTransferRemoveItems` failed because the transfer was already `IN_PROGRESS` +5. Reject to `REJECTED_ITM_PARKING` is still not represented + - Shopify cancel exists, but the `InventoryTransfer` type still has no reject-specific fields +6. OMS close-fulfillment semantics remain stronger + - Shopify line removal becomes unavailable once shipment execution has started + +## Operational Notes + +### Read-after-write timing + +The prior day’s run had shown one short read-after-write inconsistency on a newly created draft transfer. + +This day’s run did not reproduce that issue in the main happy path, but the integration should still be designed with short retry tolerance because Shopify remains an external system. + +### Date fields + +Some returned `dateCreated` and `dateReceived` values reflected Shopify’s own server-side handling and were not always identical to the requested timestamps. That should be treated as normal API behavior, not as a local OMS issue. diff --git a/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-scenarios.md b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-scenarios.md new file mode 100644 index 00000000..00a9469a --- /dev/null +++ b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-scenarios.md @@ -0,0 +1,500 @@ +# Shopify Transfer Order Scenarios To Run + +## Purpose + +This document defines the Transfer Order scenarios that should be run on Shopify to prove what Shopify offers, what maps cleanly from OMS, what only maps partially, and what cannot be done cleanly at all. + +The wording and scenario structure below intentionally follow the foundation Transfer Order design notes so that OMS and Shopify can be compared in the same language. + +### Scopes required + +- `read_inventory` +- `write_inventory` +- `read_locations` +- `read_inventory_transfers` +- `write_inventory_transfers` +- `read_inventory_shipments` +- `write_inventory_shipments` +- `read_inventory_shipments_received_items` +- `write_inventory_shipments_received_items` + +## Run These Scenarios + +Run the scenarios through both Shopify API and Shopify UI. + +### UI path + +- Use Shopify Admin transfer screens to verify what an end user can do without custom orchestration. +- Use UI evidence for: + - screen flow + - edit affordances + - missing actions + - operational usability + +## Test Data Preparation + +### Test-data sets + +1. Small happy-path set: + - 2 SKUs + - positive inventory at origin + - clean one-transfer path +2. Medium edit-path set: + - 3 to 5 SKUs + - one item later increased + - one item later removed +3. Large batching set: + - one OMS TO with high line count + - ideal if based on the Maarg TO sample already discussed + +## Scenario Execution Order + +Run the scenarios in this order so the evidence is easy to interpret. + +1. Pre-flight data validation +2. Create Transfer Order scenarios +3. Approve Transfer Order scenarios +4. Fulfil Transfer Order scenarios +5. Receive Transfer Order scenarios +6. Close Transfer Order Fulfillment scenarios +7. Reject Transfer Order scenarios +8. Large TO batching scenario +9. Gap-proof summary + +## Pre-Flight Data Validation + +### Scenario P1: Confirm Shopify locations and inventory items exist + +Foundation-style intent: + +- validate that the locations and products required for the Transfer Order exist in Shopify before attempting the workflow + +How to run: + +- Query Shopify locations. +- Query Shopify inventory items by SKU. +- Map Maarg TO products to Shopify inventory items. + +Expected result: + +- locations can be read. +- inventory items can be read. + +Evidence to capture: + +- location ids and names +- SKU to inventory-item-id mapping + +## Transfer Orders Will Facilitate Movement Of Inventory Between Locations, The Scenarios Being + +### Scenario T1: TOs where Fulfillment location is managed by OMS and Receiving location is managed by third party e.g. Store to Warehouse + +- OMS controls outbound fulfillment +- OMS does not need OMS-side receipt completion + +Steps to test on Shopify: + +1. Create a draft transfer. +2. Edit draft items. +3. Mark the transfer ready to ship. +4. Create shipment and mark it in transit. + +Shopify result classification: + +- transfer create and ready-to-ship are persisted + +Proof to capture: + +- draft transfer can be created +- ready-to-ship can be reached +- transfer line editing works before shipment starts +- inventory reservation starts at ready-to-ship + +### Scenario T2: TOs where Fulfillment location is managed by third party and Receiving location is managed by OMS e.g. Warehouse to Store + +- fulfillment is external +- OMS primarily controls approval-to-receipt + +How to test on Shopify: + +1. Attempt to represent the receive-only business flow without creating a normal Shopify-authored transfer and shipment path. +2. Attempt to receive inventory in a way that matches OMS receive-only behavior. + +Proof to capture: + +- Shopify transfer model expects a transfer and shipment context rather than OMS receive-only orchestration +- receipt operations are shipment-based, not TO-item-based + +### Scenario T3: TOs where both Fulfillment and Receiving locations are managed by OMS e.g. Store to Store + +- OMS controls both ship and receive + +How to test on Shopify: + +1. Create draft transfer. +2. Edit draft items. +3. Mark ready to ship. +4. Create draft shipment. +5. Mark shipment in transit. +6. Receive shipment. + +Proof to capture: + +- basic transfer execution can be mirrored +- receiver-side item semantics still do not map fully + +## Create Transfer Order + +### Scenario C1: Create Transfer Order + +- Use the API to create Transfer Order builds on createOrder. + +How to run on Shopify: + +- Run `Create Draft Transfer` from the Postman collection. + +Expected Shopify result: + +- supported now with current token + +Proof to capture: + +- transfer id +- status +- reference name +- line items count + +### Scenario C2: Update draft item quantity + +- draft TO items can be edited before approval + +How to run on Shopify: + +- Run `Set Transfer Items` while the transfer is still draft. + +Expected Shopify result: + +- supported before shipment starts + +Proof to capture: + +- quantity change succeeds before ready-to-ship +- quantity change can still succeed after ready-to-ship if shipment has not started +- same change becomes constrained after shipment linkage starts + +### Scenario C3: Add draft item + +- draft TO can be updated with additional items before approval + +How to run on Shopify: + +- Re-run `Set Transfer Items` with the full intended item set. + +Expected Shopify result: + +- supported before shipment starts + +Proof to capture: + +- Shopify does not add an OMS order item identity, only inventory-item line identity + +## Approve Transfer Order + +### Scenario A1: Approve Transfer Order for `TO_Fulfill_Only` + +- `ITEM_CREATED` moves to `ITEM_PENDING_FULFILL` when approving the TO + +How to run on Shopify: + +- Run `Mark Transfer Ready To Ship` or `Create Ready To Ship Transfer`. + +Expected Shopify result: + +- clean transfer-state equivalent + +Proof to capture: + +- draft to ready transition exists +- no OMS-style approval semantics are stored beyond transfer status + +### Scenario A2: Approve Transfer Order for `TO_Receive_Only` + +- `ITEM_CREATED` moves to `ITEM_PENDING_RECEIPT` when approving the TO + +How to run on Shopify: + +- Attempt to model the same flow without using a normal create-and-ship transfer authoring path. + +Expected Shopify result: + +- no clean equivalent + +Proof to capture: + +- no first-class receive-only transfer authoring semantics + +### Scenario A3: Approve Transfer Order for `TO_Fulfill_And_Receive` + +- `ITEM_CREATED` moves to `ITEM_PENDING_FULFILL` when approving the TO + +How to run on Shopify: + +- Same as `TO_Fulfill_Only` approval, then continue to shipment and receipt. + +Expected Shopify result: + +- partial overall fit + +Proof to capture: + +- approval exists as ready-to-ship +- later receiving semantics diverge from OMS + +## Fulfil Transfer Order + +### Scenario F1: Create OutTransferShipment + +- An inventory storage location will create the OutTransferShipment for a transfer order. + +How to run on Shopify: + +- Run `Create Draft Shipment`. + +Proof to capture: + +- shipment create succeeds +- inventory does not change at shipment-draft creation +- compare whether Shopify draft shipment is operationally rich enough versus OMS shipment create + +### Scenario F2: Ship OutTransferShipment + +Foundation-style intent: + +- The Transfer Shipment created will be shipped by adding tracking details. + +How to run on Shopify: + +1. Run `Set Shipment Tracking`. +2. Run `Mark Shipment In Transit`. +3. Or use `Create Shipment In Transit`. + +Proof to capture: + +- origin reserved quantity is released at in-transit +- origin on-hand quantity decreases at in-transit +- destination incoming quantity increases at in-transit + +## Receive Transfer Order + +### Scenario R1: Partial Receipt + +- Only a portion of the TO Item quantity is received. +- The remaining quantity stays open for future receipts. + +How to run on Shopify: + +- Receive less than the shipped quantity for one shipment line item. + +Expected Shopify result: +- partial equivalent because the remaining open quantity is tracked shipment-line-wise, not TO-item-wise + +Proof to capture: + +- partial receive succeeds against a shipment line item +- destination `incoming` decreases only by the received quantity +- destination `available` and `on_hand` increase only by the received quantity +- remaining open quantity is tracked on the shipment line, not as an OMS TO-item receive queue + +### Scenario R2: Multiple Shipments for Same Item + +- An item is shipped in multiple fulfillments. +- The full quantity can be received at once, even if it was split across shipments. + +How to run on Shopify: + +1. Create two shipments for the same transfer item. +2. Attempt one receiving action that behaves like OMS TO-item receipt. +3. Then receive the two shipments separately. + +Proof to capture: + +- OMS lets the user receive once at TO-item level and internally split +- Shopify expects receipt against shipment line items +- one transfer can hold multiple shipments +- Shopify requires separate receive calls for each shipment + +### Scenario R3: Receiving New Product (Not in TO) + +- A product not listed in the original TO arrives with the shipment. +- The system allows its receipt, marking it as an unexpected item without an `orderItemSeqId`. + +How to run on Shopify: + +- Attempt to receive an item that is not already on the shipment line set. + +Proof to capture: + +- inability to receive arbitrary unexpected product through shipment receive flow + +### Scenario R4: Receive TO Item and Close + +Foundation-style intent: + +- Item is fully received, and the system marks it as closed. +- No further receipts will be accepted for that item. + +How to run on Shopify: + +- Fully receive one shipment line item. + +Expected Shopify result: + +- supported as shipment completion +- partial equivalent, not an OMS TO-item close action + +Proof to capture: + +- compare shipment completion with OMS TO-item closure semantics + +### Scenario R5: Close Received TO Item (Even if Partial) + +- The receiver can choose to close a TO Item manually, even if the full quantity has not been received. + +How to run on Shopify: + +- Attempt to partially receive and then explicitly stop expecting more quantity for that item. + +Expected Shopify result: + +- no clean equivalent + +Proof to capture: + +- no receiver-side item close control matching OMS + +### Scenario R6: Receiving Against TO Items, Not Shipments + +- Receipts are recorded against TO Items, not individual shipments. + +How to run on Shopify: + +- Attempt to perform receiving without driving the interaction from shipment ids and shipment line ids. + +Expected Shopify result: + +- no clean equivalent + +Proof to capture: + +- Shopify receive API requires shipment and shipment line references + +### Scenario R7: Handling Over-Receipts + +- If the quantity received exceeds the sum of all known shipments, the extra quantity is recorded directly against the TO Item. + +How to run on Shopify: + +- Attempt to receive more than the total shipped quantity for the same item. + +Expected Shopify result: + +- partial equivalent +- supported only through shipment receive, not as OMS TO-item-only receipt + +Proof to capture: + +- Shopify accepts receive quantity greater than shipment line quantity +- transfer `receivedQuantity` can exceed transfer `totalQuantity` +- destination inventory increases by the over-received quantity +- OMS allows over-receipt with TO-item-only linkage +- Shopify receive remains shipment-scoped + +## Close Transfer Order Fulfillment + +### Scenario CF1: Close Transfer Order Item Fulfillment + +- give an option in the Fulfillment app to close the fulfillment of the item +- this could happen if the end-user wants to close the fulfillment after partially fulfilling the order items + +How to run on Shopify: + +- Attempt to remove or otherwise close the residual quantity after partial shipment activity. + +Expected Shopify result: + +- partial at best + +Proof to capture: + +- Shopify `inventoryTransferRemoveItems` only handles shippable quantity not already linked to shipments +- OMS close-fulfillment is stronger + +## Reject Transfer Order + +### Scenario J1: Reject Transfer Order + +- the TO can only be rejected if fulfilment has not been started +- the complete TO will be rejected +- the Transfer Order will be moved to `REJECTED_ITM_PARKING` +- the reservations will be cancelled + +How to run on Shopify: + +- Cancel a transfer before shipment work starts. + +Expected Shopify result: + +- partial equivalent only + +Proof to capture: + +- Shopify cancel exists +- Shopify does not model reject-to-facility, reject reason routing, or reservation cancellation semantics + +## Large Transfer Order Scenario + +### Scenario L1: Large TO needs batching + +How to run on Shopify: + +- Take one large OMS TO from Maarg. +- Count the distinct products to be posted to Shopify. +- Attempt to create the transfer payload or split it deterministically. + +Expected Shopify result: + +- line arrays are limited to `250` + +Proof to capture: + +- one OMS TO may require multiple Shopify transfers +- this changes orchestration, retry, and reconciliation behavior + +## How To Record Proof + +For each scenario, capture all of the following: + +1. OMS wording being tested +2. Shopify GraphQL query or mutation used +3. Request body +4. Response body +5. Notes on operational impact + +These can be run now: + +- P1 Confirm Shopify locations and inventory items exist +- T1 transfer create and ready-to-ship portion +- T3 transfer create, draft edit, ready-to-ship, shipment, and receipt portion +- C1 Create Transfer Order +- C2 Update draft item quantity +- C3 Add draft item +- A1 Approve Transfer Order for `TO_Fulfill_Only` +- A3 Approve Transfer Order for `TO_Fulfill_And_Receive` +- F1 Create OutTransferShipment +- F2 Ship OutTransferShipment +- R1 Partial Receipt +- R2 Multiple Shipments for Same Item +- R4 Receive TO Item and Close +- R7 Handling Over-Receipts +- J1 transfer-cancel portion before shipment work starts +- L1 Large TO batching proof \ No newline at end of file diff --git a/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-test-results-summary-2026-04-11.md b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-test-results-summary-2026-04-11.md new file mode 100644 index 00000000..e77dfb51 --- /dev/null +++ b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-test-results-summary-2026-04-11.md @@ -0,0 +1,306 @@ +# Shopify Transfer Order Test Results Summary + +## Purpose + +This document consolidates the Shopify transfer-order test results run on `gorjana-sandbox.myshopify.com` through April 11, 2026. + +It is the single entry point for: + +- small live transfer and shipment tests +- large bulk route tests +- Shopify admin links for the created transfers +- inventory snapshots observed at each major step +- the main gaps between OMS Transfer Order behavior and Shopify transfer behavior + +Supporting detail remains in the companion documents in this folder. + +## Executive Summary + +The test set proved that Shopify can execute transfer creation, ready-to-ship reservation, shipment creation, in-transit movement, and receipt with live gorjana sandbox inventory. + +The same test set also proved that Shopify is not a one-to-one replacement for OMS Transfer Order orchestration: + +- one logical OMS TO can fan out into many Shopify transfers because of the `250`-line mutation limit +- one logical OMS shipment or receive operation can fan out into many Shopify shipments and many receive calls +- Shopify receipt is shipment-line based, while OMS receipt is TO-item based +- Shopify execution has item and location preconditions that OMS TO authoring does not expose in the same way + +## Test Set Covered + +| Scenario group | Scenario | Route type | Variants | Units | Shopify transfers | Outcome | +| --- | --- | --- | --- | --- | --- | --- | +| Small live | `TO-LIVE-20260411-A` | Store to store | `1` | `2` | `1` | Full happy path succeeded | +| Small live | `TO-LIVE-20260411-B` | Store to store | `1` | `2` | `1` | One transfer with two shipments succeeded | +| Small live | `TO-LIVE-20260411-C` | Store to store | `1` | `1 requested / 2 received` | `1` | Over-receipt succeeded | +| Gap proof | `TO-LIVE-20260411-D` | Store to store | `1` | `2` | `1` | Partial receipt succeeded; transfer-level receive and post-shipment close failed cleanly | +| Gap proof | `TO-LIVE-20260411-E` | Store to store | `1` | `1` | `1` | Draft cancel succeeded; reject semantics still absent | +| External-end proof | `TO-EXT-OUT-20260412-A` | Shopify location to external destination | `1` | `1` | `1` | One-sided outbound transfer and shipment succeeded | +| External-end proof | `TO-EXT-IN-20260412-B` | External origin to Shopify location | `1` | `1` | `1` | One-sided inbound transfer and shipment succeeded | +| Bulk create-only probe | `TO-BULK-20260411-LGW-AUS-B01/B02` | Warehouse to store | `500` logical attempt split across `2` transfers | create-only probe | `2` | Transfer create succeeded, shipment execution exposed destination and tracking constraints | +| Bulk execution | `LGW-AUS-EXEC2` | Warehouse to store | `285` | `1,425` | `2` | End-to-end execution succeeded | +| Bulk execution | `AUS-CAR-EXEC2` | Store to store | `1,500` | `7,500` | `6` | End-to-end execution succeeded | +| Bulk execution | `CAR-LGW-EXEC2` | Store to warehouse | `1,500` | `7,500` | `6` | End-to-end execution succeeded | + +Overall executed scale: + +- logical bulk routes: `3` +- executed variants: `3,285` +- executed units: `16,425` +- executed Shopify transfer batches: `14` + +## Shopify Admin Links + +All links use the Shopify admin transfer URL pattern: + +`https://gorjana-sandbox.myshopify.com/admin/products/transfers/` + +### Small live tests + +| Scenario | Reference | Transfer number | Admin link | +| --- | --- | --- | --- | +| Happy path | `TO-LIVE-20260411-A` | `#T0007` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874193452` | +| Two shipments under one transfer | `TO-LIVE-20260411-B` | `#T0008` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874226220` | +| Over-receipt probe | `TO-LIVE-20260411-C` | `#T0009` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874258988` | +| Gap proof: partial receive and failed close | `TO-LIVE-20260411-D` | `#T0026` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3875012652` | +| Gap proof: cancel versus reject | `TO-LIVE-20260411-E` | `#T0027` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3875045420` | +| External-end proof: origin only | `TO-EXT-OUT-20260412-A` | `#T0028` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3875110956` | +| External-end proof: destination only | `TO-EXT-IN-20260412-B` | `#T0029` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3875143724` | + +### Bulk create-only probe + +| Scenario | Reference | Admin link | +| --- | --- | --- | +| Laguna to Austin create-only probe batch 1 | `TO-BULK-20260411-LGW-AUS-B01` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874488364` | +| Laguna to Austin create-only probe batch 2 | `TO-BULK-20260411-LGW-AUS-B02` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874521132` | + +### Bulk execution routes + +| Route | Reference | Admin link | +| --- | --- | --- | +| `LGW-AUS-EXEC2` | `TO-BULK-20260411-LGW-AUS-EXEC2-B01` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874553900` | +| `LGW-AUS-EXEC2` | `TO-BULK-20260411-LGW-AUS-EXEC2-B02` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874586668` | +| `AUS-CAR-EXEC2` | `TO-BULK-20260411-AUS-CAR-EXEC2-B01` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874619436` | +| `AUS-CAR-EXEC2` | `TO-BULK-20260411-AUS-CAR-EXEC2-B02` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874652204` | +| `AUS-CAR-EXEC2` | `TO-BULK-20260411-AUS-CAR-EXEC2-B03` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874684972` | +| `AUS-CAR-EXEC2` | `TO-BULK-20260411-AUS-CAR-EXEC2-B04` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874717740` | +| `AUS-CAR-EXEC2` | `TO-BULK-20260411-AUS-CAR-EXEC2-B05` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874750508` | +| `AUS-CAR-EXEC2` | `TO-BULK-20260411-AUS-CAR-EXEC2-B06` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874783276` | +| `CAR-LGW-EXEC2` | `TO-BULK-20260411-CAR-LGW-EXEC2-B01` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874816044` | +| `CAR-LGW-EXEC2` | `TO-BULK-20260411-CAR-LGW-EXEC2-B02` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874848812` | +| `CAR-LGW-EXEC2` | `TO-BULK-20260411-CAR-LGW-EXEC2-B03` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874881580` | +| `CAR-LGW-EXEC2` | `TO-BULK-20260411-CAR-LGW-EXEC2-B04` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874914348` | +| `CAR-LGW-EXEC2` | `TO-BULK-20260411-CAR-LGW-EXEC2-B05` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874947116` | +| `CAR-LGW-EXEC2` | `TO-BULK-20260411-CAR-LGW-EXEC2-B06` | `https://gorjana-sandbox.myshopify.com/admin/products/transfers/3874979884` | + +## Inventory Snapshot Summary + +### Small live happy path: `TO-LIVE-20260411-A` + +Item: + +- SKU `207-113-G` +- quantity `2` +- origin `Atlanta` +- destination `Austin` + +| Stage | Atlanta available | Atlanta reserved | Atlanta on_hand | Austin available | Austin incoming | Austin on_hand | +| --- | --- | --- | --- | --- | --- | --- | +| Before create | `203` | `0` | `203` | `11` | `0` | `11` | +| After draft create | `203` | `0` | `203` | `11` | `0` | `11` | +| After ready to ship | `201` | `2` | `203` | `11` | `0` | `11` | +| After shipment draft create | `201` | `2` | `203` | `11` | `0` | `11` | +| After mark in transit | `201` | `0` | `201` | `11` | `2` | `11` | +| After partial receive of `1` | `201` | `0` | `201` | `12` | `1` | `12` | +| After final receive of `1` | `201` | `0` | `201` | `13` | `0` | `13` | + +### Small live two-shipment path: `TO-LIVE-20260411-B` + +Observed state result: + +- one transfer with quantity `2` +- two shipments created with quantity `1` each +- transfer moved to `IN_PROGRESS` after the first shipment receipt +- transfer moved to `TRANSFERRED` only after the second shipment receipt + +Inventory conclusion: + +- Shopify kept the inventory movement correct +- the operational gap is the workflow shape because receipt must be called per shipment, not once per TO item + +### Small live over-receipt path: `TO-LIVE-20260411-C` + +Observed state result: + +- transfer quantity: `1` +- shipment quantity: `1` +- received quantity accepted by Shopify: `2` +- transfer `receivedQuantity` became `2` + +Inventory conclusion: + +- destination inventory increased by `2` +- Shopify does allow over-receipt live +- the gap is not absence of over-receipt +- the gap is that over-receipt is still shipment-scoped rather than TO-item-scoped + +### Gap proof path: `TO-LIVE-20260411-D` + +Observed state result: + +- transfer `#T0026` was created ready to ship with quantity `2` +- shipment `#T0026-1` was created and moved to `IN_TRANSIT` +- partial receipt of `1` succeeded +- shipment became `PARTIALLY_RECEIVED` +- transfer became `IN_PROGRESS` +- transfer line showed `shippedQuantity = 2` and `shippableQuantity = 0` +- `inventoryTransferRemoveItems` then failed with: + - `Transfer can only have its items removed in a Draft or Ready-to-ship status.` +- `inventoryShipmentReceive` called with the transfer id failed with: + - `Invalid id: gid://shopify/InventoryTransfer/3875012652` +- trying to add `inventoryItemId` to the shipment receive input failed schema validation because that field is not defined + +Inventory conclusion: + +- after the partial receipt, Austin held `available = 13` and `incoming = 1` for SKU `207-113-G` +- Atlanta held `available = 196` and `reserved = 0` +- Shopify preserved the open incoming remainder, but did not allow a receiver-side close of that remainder + +### Gap proof path: `TO-LIVE-20260411-E` + +Observed state result: + +- draft transfer `#T0027` was created successfully +- `inventoryTransferCancel` moved it to `CANCELED` +- `InventoryTransfer` schema introspection showed no reject-specific fields such as reject reason or reject destination + +Inventory conclusion: + +- Shopify cancel is valid as a cancel +- it is not a full reject equivalent to OMS reject-to-parking behavior + +### External-end proof path: `TO-EXT-OUT-20260412-A` + +Observed state result: + +- transfer `#T0028` was created with `origin = Atlanta` and `destination = null` +- transfer status was `READY_TO_SHIP` +- draft shipment `#T0028-1` was created immediately +- transfer `shipments` included the created shipment +- shipment line item matched the transfer line item exactly for SKU `207-113-G` and quantity `1` + +Inventory conclusion: + +- Atlanta changed from `available = 196, reserved = 0, on_hand = 196` +- to `available = 195, reserved = 1, on_hand = 196` +- Austin did not change during this scenario +- Shopify therefore supports a Shopify-origin to external-destination header and shipment flow + +### External-end proof path: `TO-EXT-IN-20260412-B` + +Observed state result: + +- transfer `#T0029` was created with `origin = null` and `destination = Austin` +- transfer status was `READY_TO_SHIP` +- draft shipment `#T0029-1` was created immediately +- transfer `shipments` included the created shipment +- shipment line item matched the transfer line item exactly for SKU `207-113-G` and quantity `1` + +Inventory conclusion: + +- Austin inventory did not change during ready-transfer and draft-shipment creation +- Shopify therefore supports an external-origin to Shopify-destination header and shipment flow +- this specific run proved create-and-link behavior; it did not advance shipment `#T0029-1` to in-transit or receive + +### Bulk route: `LGW-AUS-EXEC2` + +| Stage | Origin available | Origin reserved | Origin on_hand | Destination available | Destination incoming | Destination on_hand | +| --- | --- | --- | --- | --- | --- | --- | +| Before | `518,506` | `1,425` | `519,972` | `1,553` | `0` | `1,553` | +| After ready | `517,081` | `2,850` | `519,972` | `1,553` | `0` | `1,553` | +| After in transit | `517,081` | `1,425` | `518,547` | `1,553` | `1,425` | `1,553` | +| After receive | `517,081` | `1,425` | `518,547` | `2,978` | `0` | `2,978` | + +Route conclusion: + +- reservation happened at ready-to-ship +- issue happened at in-transit +- receipt moved destination `incoming -> available` +- one logical warehouse-to-store route became `2` Shopify transfers and `2` Shopify shipments + +### Bulk route: `AUS-CAR-EXEC2` + +| Stage | Origin available | Origin reserved | Origin on_hand | Destination available | Destination incoming | Destination on_hand | +| --- | --- | --- | --- | --- | --- | --- | +| Before | `240,750` | `0` | `240,827` | `238,246` | `0` | `238,246` | +| After ready | `233,250` | `7,500` | `240,827` | `238,246` | `0` | `238,246` | +| After in transit | `233,250` | `0` | `233,327` | `238,246` | `7,500` | `238,246` | +| After receive recovery snapshot | `233,250` | `0` | `233,327` | `238,901` | `0` | `238,901` | + +Route note: + +- all six transfers reached `TRANSFERRED` +- all six batch `receivedQuantity` values matched the transferred quantities +- the immediate isolated final snapshot failed during long pagination and the final values shown above are from the later recovery summary already present in the evidence set + +### Bulk route: `CAR-LGW-EXEC2` + +| Stage | Origin available | Origin reserved | Origin on_hand | Destination available | Destination incoming | Destination on_hand | +| --- | --- | --- | --- | --- | --- | --- | +| Before | `246,849` | `0` | `246,849` | `63,714` | `0` | `64,224` | +| After ready | `239,349` | `7,500` | `246,849` | `63,714` | `0` | `64,224` | +| After in transit | `239,349` | `0` | `239,349` | `63,714` | `7,500` | `64,224` | +| After receive | `239,349` | `0` | `239,349` | `71,214` | `0` | `71,724` | + +Route conclusion: + +- this was the cleanest large store-to-warehouse proof +- Shopify again mirrored the reservation, in-transit, and receipt inventory movement +- one logical route still became `6` Shopify transfers and `6` Shopify shipments + +## What Shopify Matched Well + +| OMS intent | Shopify live result | +| --- | --- | +| TO header creation without immediate inventory movement | matched | +| approval-like reservation timing | matched at `READY_TO_SHIP` | +| shipment staging before inventory issue | matched | +| issue inventory on ship | matched at `IN_TRANSIT` | +| receipt moves destination `incoming -> available` | matched | +| partial receipt by quantity | matched | +| multiple shipments under one transfer | supported | + +## What Broke Or Needed Extra Shopify Preconditions + +| Area | Observed result | Impact | +| --- | --- | --- | +| Large TO line count | `250` lines per mutation forced batch fan-out | one OMS TO becomes many Shopify transfers | +| Shipment and receipt execution | one shipment and one receive call per Shopify transfer/shipment set | one OMS operation becomes many Shopify operations | +| Inventory tracking | non-tracked items failed transfer creation | Shopify-side prevalidation is required | +| Destination inventory state | shipment create failed with `INVENTORY_STATE_NOT_ACTIVE` if the item was not stocked at destination | executable routes are a stricter subset of authorable OMS routes | +| Receipt model | receive is shipment-line based | OMS TO-item receiving does not map cleanly | +| Over-receipt | supported, but through shipment receive only | OMS and Shopify can reach the same inventory effect through different control shapes | +| External-end transfers | supported with one omitted end on the transfer header | Shopify can represent external origin or destination for transfer and draft shipment creation | +| Receive-only and close semantics | transfer-level receive and post-shipment close are not available | OMS exception handling remains stronger | +| Reject semantics | cancel exists but reject routing fields are absent | OMS reject-to-parking does not map cleanly | +| Evidence and reconciliation at scale | long inventory snapshots can time out or reset | retry logic is needed even when mutations succeed | + +## OMS Versus Shopify Operational Conclusion + +These tests prove that Shopify can mirror transfer execution when the line set is prevalidated to Shopify’s own rules and when the route is broken into Shopify-sized batches. + +These tests also prove that OMS should remain the system of record for Transfer Order orchestration because OMS is stronger in the areas that matter operationally: + +- one logical TO stays one logical TO +- receiving is TO-item oriented instead of shipment-call oriented +- receive-only and receiver-driven exception flows remain cleaner in OMS +- Shopify-specific execution preconditions can be enforced by OMS before any external mutation is attempted + +## Companion Documents + +- `shopify-transfer-order-inventory-transfer-mapping.md` +- `shopify-transfer-order-vs-oms-gap-analysis.md` +- `shopify-transfer-order-scenarios.md` +- `shopify-transfer-order-live-test-evidence-2026-04-11.md` +- `shopify-transfer-order-gap-proof-evidence-2026-04-11.md` +- `shopify-transfer-order-external-end-live-test-evidence-2026-04-12.md` +- `shopify-transfer-order-bulk-live-test-evidence-2026-04-11.md` diff --git a/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-vs-oms-gap-analysis.md b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-vs-oms-gap-analysis.md new file mode 100644 index 00000000..7e4f3258 --- /dev/null +++ b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-vs-oms-gap-analysis.md @@ -0,0 +1,298 @@ +# Shopify Transfer Order Vs OMS Gap Analysis + +## Purpose + +This document explains why OMS should remain the transfer-order system of record even if Shopify transfer APIs are adopted as a downstream mirror. + +## Method And Evidence Standard + +- Primary OMS evidence comes from the Maarg services, REST definitions, and transfer-order seed and status data in the local repo. +- Supporting OMS workflow intent comes from the foundation TO design docs on GitHub, reviewed on April 10, 2026. +- Shopify Admin GraphQL docs are treated as the normative source for transfer and shipment behavior. +- Shopify community threads are treated as operational evidence only, not as normative API rules. +- Conclusions that combine live behavior, OMS workflow, and Shopify docs are called out directly in the prose where needed. + +## Executive Verdict + +- Shopify now offers enough transfer and shipment APIs to mirror a basic draft -> ready -> ship -> receive execution flow. +- Shopify still does not model OMS flow ownership, TO-item-based receiving, unexpected-item receipt, receiver-side close behavior, reservation control, or reject-to-parking semantics cleanly. +- Shopify is suitable as an execution mirror and evidence surface, not as the primary TO workflow engine. + +## Capability Comparison Matrix + +| OMS capability | Shopify equivalent | Gap or breaking point | Operational impact | Evidence source | +| --- | --- | --- | --- | --- | +| Explicit TO flow types: `TO_Fulfill_Only`, `TO_Receive_Only`, `TO_Fulfill_And_Receive` | Transfer header plus shipment lifecycle | Shopify does not expose an equivalent flow-type model for authoring intent | Harder to encode whether OMS owns ship, receive, or both | OMS codebase, foundation TO docs, Shopify docs, and live behavior | +| Separate approval gates for store-fulfilled and warehouse-fulfilled TOs | Draft transfer then ready-to-ship transfer | Shopify supports draft and ready transitions, but not OMS-specific approval semantics | Approval meaning must stay in OMS | OMS codebase and Shopify docs | +| Order-item identity via `orderItemSeqId` | Transfer line items over `InventoryItem` | Shopify lines are inventory-item based, not OMS order-item based | Harder to preserve item-level business semantics and downstream reconciliation | OMS codebase and Shopify docs | +| Draft TO item edits before approval | `inventoryTransferSetItems` | Live testing showed edits also work after `READY_TO_SHIP` until shipment work starts, so the true breaking point is shipment linkage rather than draft status alone | Draft editing remains possible longer than a strict draft-only model suggests, but OMS still owns the business rule boundary | OMS codebase, Shopify docs, and live tests | +| Receive-only transfer flow | No clean equivalent | Shopify is oriented toward created transfer plus shipment plus receipt, not OMS receive-only orchestration | `TO_Receive_Only` should stay OMS-only | OMS codebase, foundation TO docs, and Shopify docs | +| Shipment creation separate from shipment ship | `inventoryShipmentCreate`, then `inventoryShipmentMarkInTransit` or `inventoryShipmentCreateInTransit` | Shopify can create and ship, but the transfer APIs do not replace OMS package, route-segment, and warehouse staging semantics | Shopify can mirror execution, but OMS remains richer as the operational workflow | OMS codebase and Shopify docs | +| Receipt can be entered against TO items and internally split across shipments | `inventoryShipmentReceive` | Shopify receives against shipment line items, not TO items | Receiving UI and workflow become shipment-centric in Shopify instead of item-centric | OMS codebase, foundation TO docs, and Shopify docs | +| Receipt without shipment reference | No clean equivalent | Shopify receipt requires shipment id and shipment receive line items | OMS can continue receiving when shipment linkage is missing; Shopify cannot mirror that directly | OMS codebase and Shopify docs | +| Over-receipt against TO item | Shipment-scoped over-receipt via `inventoryShipmentReceive` | Live testing showed Shopify accepts over-receipt, but only through shipment receive and not as TO-item-only receipt | OMS still owns the richer TO-item-centric over-receipt workflow and reconciliation | OMS codebase, foundation TO docs, Shopify docs, and live tests | +| Unexpected item receipt with no `orderItemSeqId` | No clean equivalent | Shopify receive flow works on existing shipment line items, not on arbitrary newly arrived products | Unexpected arrivals cannot be represented cleanly in Shopify TO flow | OMS codebase, foundation TO docs, and Shopify docs | +| Receiver can receive and close an item even when only part of the expected quantity arrived | No clean equivalent | Shopify has receipt and cancel tools, but not an OMS-style receiver-driven close of residual TO quantity | Residual receiving exceptions must stay in OMS | OMS codebase, foundation TO docs, and Shopify docs | +| Reject to `REJECTED_ITM_PARKING` with reject reason | `inventoryTransferCancel` at best | Cancel is weaker than reject-to-facility routing | Loss of reject destination semantics if OMS is not source of truth | OMS codebase, foundation TO docs, and Shopify docs | +| Close fulfillment with remaining reservation release and `cancelQuantity` handling | `inventoryTransferRemoveItems` at best | Shopify only removes shippable quantity not already tied to shipments | OMS can close partially fulfilled items more safely and explicitly | OMS codebase and Shopify docs | +| Reservation and reservation release as part of TO state changes | No reviewed first-class equivalent in transfer flow | Shopify reviewed transfer surface does not model OMS reservation control points | Reservation logic must remain in OMS | OMS codebase and Shopify docs | +| Large TO with `1,548` distinct items | One or more transfers | Shopify input arrays max out at `250` | Requires `7` transfer batches for the sample TO | TO sample, Shopify limits docs, and batching math | +| Transfer webhook visibility from draft state | Documented transfer webhooks start at item-change, ready, cancel, and complete topics | No documented create topic for draft transfer creation | Requires reconciliation or polling for draft visibility | Shopify docs and community posts | +| Shipment-to-transfer correlation in webhook-driven flows | `InventoryTransfer` has shipments; `InventoryShipment` object does not expose parent transfer field | Correlation is weak from the shipment side, especially through webhooks | More custom state correlation and reporting complexity | Shopify docs and community posts | +| PO-adjacent inbound workflow coverage | No public Purchase Order API | Shopify itself points developers toward workarounds and transfer APIs | Purchase-order-centric inbound orchestration cannot be built cleanly in Shopify today | Community posts and overall workflow analysis | + +## Official Shopify Limitations + +### 1. Array input limit forces batching + +- Shopify states that any input argument accepting an array has a maximum size of `250`. +- A single OMS TO may need multiple Shopify transfers even before any business exception occurs. + +Why this matters: + +- OMS can treat one transfer order as one business document. +- Shopify may force the same document into multiple transfer batches. +- Every later action then becomes a batch fan-out problem. + +### 2. Transfer lines are inventory-item based, not OMS order-item based + +- `InventoryTransfer` tracks movement of `InventoryItem` objects between locations. +- OMS TO logic is written around `orderId` and `orderItemSeqId`, with item status flow, cancel quantity, shipped quantity, received quantity, and rejection data. +- Shopify line identity is too coarse to replace OMS order-item identity. + +Why this matters: + +- OMS can reason about one TO item independently. +- Shopify sees a transfer line in terms of inventory item and quantity. +- Business semantics such as reject reasons and close-fulfillment intent do not map one-to-one. + +### 3. Receipt is shipment-centric + +- `inventoryShipmentReceive` requires an inventory shipment id and shipment receive line items. +- OMS receipt can allocate received quantity across shipments and can also receive residual or excess quantity directly against the TO item without shipment linkage. +- The foundation TO design explicitly states that receipts are recorded against TO items, not shipments. +- Live testing showed that partial receipt, final receipt, and over-receipt all worked when driven from shipment receive calls. +- Shopify receiving is still narrower than OMS receiving because the control point remains shipment-centric. + +Why this matters: + +- shipment-backed receiving maps reasonably well +- over-receipt is possible, but only through shipment receive +- receipt-without-shipment does not +- unexpected-item receipt and receiver-side close behavior do not +- OMS must remain the authoritative receipt ledger + +### 4. Shipment-side object model is incomplete for transfer correlation + +- `InventoryTransfer` includes `shipments` +- The reviewed `InventoryShipment` field list does not include a parent `InventoryTransfer` field + +Why this matters: + +- the header can point to shipments +- the shipment cannot cleanly point back to the header through the reviewed object shape +- webhook consumers must reconstruct the relationship + +### 5. Draft transfer webhook coverage is incomplete + +- The documented transfer webhook topics include `add_items`, `cancel`, `complete`, `ready_to_ship`, `remove_items`, and `update_item_quantities` +- There is no documented draft transfer create topic + +Why this matters: + +- draft transfer creation is not fully event-visible +- integrations that need draft awareness must reconcile or poll +- OMS does not have this visibility gap internally + +### 6. Late-stage item removal is limited + +- `inventoryTransferRemoveItems` removes only shippable quantities not already associated with shipments +- Live testing showed remove-items works on a `READY_TO_SHIP` transfer before shipment linkage begins +- OMS close-fulfillment can cancel remaining reserved quantity, set `cancelQuantity`, and update status flow even after partial fulfillment has happened +- Shopify line removal is a narrower tool than OMS close-fulfillment + +Why this matters: + +- OMS can intentionally close a partially fulfilled item +- Shopify can remove the part that is still shippable and not already shipment-linked +- that is not enough to replace OMS fulfillment-close semantics + +### 7. Mutation orchestration is heavier than it first appears + +- Transfer, shipment, and shipment-receive actions use different access scopes +- Idempotency becomes required for transfer and shipment mutations as of `2026-04` +- GraphQL Admin is query-cost rate limited +- Shopify transfer integration requires more operational plumbing than a simple create/update mirror + +Why this matters: + +- more scopes to request and maintain +- more idempotency rules to honor +- more batch fan-out and retry complexity +- more cost pressure when reconciliation is needed + +### 8. Shopify can mirror shipment execution but not the OMS receiving work queue + +- Shopify does provide `inventoryShipmentCreate`, `inventoryShipmentMarkInTransit`, and `inventoryShipmentReceive`. +- The April 11, 2026 gorjana sandbox run executed all three successfully with the updated token. +- Ready-to-ship reduced origin `available` and increased origin `reserved`. +- In-transit released origin `reserved`, reduced origin `on_hand`, and increased destination `incoming`. +- Partial receipt reduced destination `incoming` and increased destination `available` incrementally. +- That is enough to mirror a basic create -> ship -> receive path and its core inventory movements. +- It is not enough to replace OMS receiving behavior where the user works against TO items, not shipment records. + +Why this matters: + +- OMS receivers can ignore how shipments were split and still receive accurately +- Shopify receivers and integrations are pulled back toward shipment-line-state management +- operational simplicity stays on the OMS side + +## Community-Reported Operational Breaking Points + +These are not treated as normative API rules. They are treated as operational evidence that the reviewed Shopify transfer flow still has rough edges in real integrations. + +| Community observation | What was reported | Why it matters | +| --- | --- | --- | +| No draft transfer creation webhook | In the October 2025 thread, Shopify staff confirmed that there is no webhook for draft transfer creation and that the earliest webhook is ready-to-ship | Draft-state integrations must poll or reconcile | +| Weak shipment-to-transfer linkage | Developers reported that `InventoryShipment` lacks a transfer reference and shipment webhooks do not include transfer id | Harder reporting and downstream state reconstruction | +| Webhook behavior confusion | A December 2025 thread reports subscribing to transfer topics but only seeing `inventory_transfers/cancel`, and seeing it fire when the transfer was marked received | Suggests webhook observability and payload behavior are still immature | +| Transfer API rollout and scope friction | June 2025 threads show transfer API still stabilizing around unstable or release-candidate availability, including scope-access issues later fixed by Shopify | Early adopters faced avoidable integration friction | +| Admin UI inefficiency | Merchant complaints in the Shopify Community describe the new transfer UI as harder to use, with broken scrolling, weak filtering, and bulk transfer creation issues | Operational users handling large catalogs feel the pain before APIs even enter the picture | +| No public Purchase Order API | Shopify staff said purchase orders cannot currently be created via Admin API and there are no endpoints to work with purchase orders directly | Inbound inventory workflows remain fragmented | +| Purchase-order webhook topics not actually usable | January 2025 reports show purchase-order webhook topics present in unstable docs but not actually subscribable; Shopify staff said those topics should not have been displayed because the feature was being reworked | Another example of incomplete workflow surface for inbound inventory operations | + +## What OMS Supports That Shopify Does Not + +### Explicit flow ownership + +- OMS distinguishes between fulfill-only, receive-only, and fulfill-and-receive transfer orders. +- that distinction is operationally important because it tells the business which side of the transfer OMS owns. +- Shopify transfer APIs do not give the same first-class modeling control. + +### Approval before execution + +- OMS uses separate approval services for store-fulfill and warehouse-fulfill transfer orders. +- approval is not just cosmetic. It changes header status, item statuses, and reservation behavior. +- Shopify ready-to-ship is useful as an execution mirror, but it does not replace OMS approval semantics. + +### Richer item-level control + +- OMS item flow includes draft item creation, draft item quantity edits, item status flow, cancellation, close-fulfillment, and shipped versus received quantity tracking. +- Shopify transfer lines are too lightweight to carry that full control surface. + +### Reject with destination semantics + +- OMS reject routes the TO to `REJECTED_ITM_PARKING` and carries rejection reason plus reject-to-facility behavior. +- This is a warehouse operation model, not just a transfer cancel. +- Shopify cancel cannot replace it. + +### Receipt behavior that tolerates imperfect shipment history + +- OMS can receive against available shipments, split receipts across shipments, over-receive, receive unexpected items, and later reconcile receipts when shipment records arrive later. +- that is substantially more tolerant of real operational messiness than Shopify shipment-only receipt handling. + +### Receiver-driven close behavior + +- The foundation TO workflow explicitly allows receive-and-close and close-received-item-even-if-partial behavior. +- This lets a receiving team intentionally stop expecting more stock for a TO item. +- Shopify transfer and shipment APIs do not provide the same item-centric close control. + +### Reservation management + +- OMS approval, cancel, reject, and close-fulfillment all interact with reservation logic. +- Reservation control is part of the TO workflow, not an unrelated side effect. +- Reviewed Shopify transfer APIs do not expose this as a comparable workflow concern. + +### Better operational control for store and warehouse apps + +- OMS exposes TO creation and approval through OMS APIs and transfer shipment, receipt, reject, and close-fulfillment through Poorti endpoints. +- OMS already has an app boundary aligned to real warehouse and store work. +- Shopify transfer APIs are better suited as a downstream mirror than as the operational backbone for these app workflows. + +## Recommended System-Of-Record Position + +- OMS should remain the transfer-order system of record. +- Shopify should mirror only the execution states that have a clean transfer or shipment equivalent. +- the safest phase-1 mirror scope is: + - `ORDER_CREATED` -> draft transfer + - `ORDER_APPROVED` -> ready-to-ship transfer + - shipment create -> optional draft shipment mirror + - shipment ship -> in-transit shipment + - shipment-backed receipt -> shipment receive +- The following should remain OMS-only truth even if mirrored partially later: + - `TO_Receive_Only` + - reject-to-parking + - close-fulfillment + - receive-and-close and partial close at receiving time + - reservation changes + - unexpected-item receipt and receipt-without-shipment +- Shipment-scoped over-receipt can be mirrored in Shopify, but it should still be interpreted through OMS because OMS owns the TO-item-centric exception workflow. +- Shopify InventoryTransfer is an execution and reporting layer, not the right place to own OMS transfer-order orchestration. + +## Suggested Shopify Validation Plan + +Use this sequence when you want evidence of what Shopify actually offers: + +1. Create a draft transfer in Shopify UI and through GraphQL. +2. Edit quantities and remove items while the transfer is draft. +3. Mark the transfer ready to ship. +4. Create a draft shipment, then add or update shipment items. +5. Mark the shipment in transit. +6. Receive the shipment partially and then fully. +7. Repeat the same flow through the Postman collection so you have API evidence as well as UI evidence. +8. Try the OMS-only or OMS-richer exception cases and record the result: + - receive without shipment + - over-receipt beyond shipped quantity + - unexpected item not already on the transfer + - receive and close with partial receipt + - reject to parking facility + - close fulfillment after partial shipment +9. Capture screenshots, GraphQL responses, webhook payloads, and any missing UI affordance for each case. + +The proof you want is not just "Shopify cannot do X". It is: + +- where Shopify has a native object and mutation +- where the object exists but loses OMS semantics +- where a workaround is required +- where there is no clean equivalent at all + +## References + +### Local sources + +- `runtime/component/oms/service/co/hotwax/orderledger/order/TransferOrderServices.xml` +- `runtime/component/poorti/service/co/hotwax/poorti/TransferOrderFulfillmentServices.xml` +- `runtime/component/oms/service/oms.rest.xml` +- `runtime/component/poorti/service/poorti.rest.xml` +- `runtime/component/oms/data/TransferOrderSeedData.xml` +- Provided sample CSV analysis: `sqllab_untitled_query_33_20260402T145306.csv` +- https://raw.githubusercontent.com/saastechacademy/foundation/main/project-ideas/fulfillment-center-mgmt/readme.md +- https://raw.githubusercontent.com/saastechacademy/foundation/main/project-ideas/fulfillment-center-mgmt/receiveTransferOrder.md +- https://raw.githubusercontent.com/saastechacademy/foundation/main/project-ideas/fulfillment-center-mgmt/closeTransferOrderItemFulfillment.md +- https://raw.githubusercontent.com/saastechacademy/foundation/main/project-ideas/fulfillment-center-mgmt/rejectTransferOrder.md + +### Shopify official docs reviewed on April 10, 2026 + +- https://shopify.dev/docs/api/admin-graphql/latest/objects/inventorytransfer +- https://shopify.dev/docs/api/admin-graphql/latest/objects/inventoryshipment +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryTransferCreate +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryTransferCreateAsReadyToShip +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryTransferMarkAsReadyToShip +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryTransferSetItems +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryTransferRemoveItems +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryTransferCancel +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryShipmentCreate +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryShipmentMarkInTransit +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryShipmentCreateInTransit +- https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryShipmentReceive +- https://shopify.dev/docs/api/usage/limits +- https://shopify.dev/docs/api/admin-graphql/latest/enums/WebhookSubscriptionTopic + +### Shopify community sources + +- https://community.shopify.dev/t/tracking-of-inventorytransfer-and-inventoryshipment/23275 +- https://community.shopify.dev/t/inventory-transfers-webhook-behavior-payloads-and-bugs/26946 +- https://community.shopify.dev/t/purchase-orders/2183 +- https://community.shopify.dev/t/purchase-orders-api/23300 +- https://community.shopify.dev/t/cant-create-a-subscription-to-purchase-orders-create-webhook/6869 +- https://community.shopify.com/t/is-the-new-inventory-transfer-interface-less-efficient/47184/2