From 292de20ce2ab83ba04d6f8deda755006983e6af5 Mon Sep 17 00:00:00 2001 From: Chinmay2107 Date: Sat, 11 Apr 2026 19:24:23 +0530 Subject: [PATCH 01/20] Add Shopify transfer order documentation --- ..._2026-01.postman_environment.template.json | 278 +++++ ...d_shipment_2026-01.postman_collection.json | 1032 +++++++++++++++++ ...rder-bulk-live-test-evidence-2026-04-11.md | 269 +++++ ...ansfer-order-inventory-transfer-mapping.md | 330 ++++++ ...fer-order-live-test-evidence-2026-04-11.md | 294 +++++ .../shopify-transfer-order-scenarios.md | 500 ++++++++ ...r-order-test-results-summary-2026-04-11.md | 225 ++++ ...pify-transfer-order-vs-oms-gap-analysis.md | 298 +++++ 8 files changed, 3226 insertions(+) create mode 100644 project-ideas/shopify-integration/transfer-order/postman/gorjana_sandbox_shopify_2026-01.postman_environment.template.json create mode 100644 project-ideas/shopify-integration/transfer-order/postman/shopify_inventory_transfer_and_shipment_2026-01.postman_collection.json create mode 100644 project-ideas/shopify-integration/transfer-order/shopify-transfer-order-bulk-live-test-evidence-2026-04-11.md create mode 100644 project-ideas/shopify-integration/transfer-order/shopify-transfer-order-inventory-transfer-mapping.md create mode 100644 project-ideas/shopify-integration/transfer-order/shopify-transfer-order-live-test-evidence-2026-04-11.md create mode 100644 project-ideas/shopify-integration/transfer-order/shopify-transfer-order-scenarios.md create mode 100644 project-ideas/shopify-integration/transfer-order/shopify-transfer-order-test-results-summary-2026-04-11.md create mode 100644 project-ideas/shopify-integration/transfer-order/shopify-transfer-order-vs-oms-gap-analysis.md 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..c4ad889d --- /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 Live Test Evidence + +## 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-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..4a820931 --- /dev/null +++ b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-live-test-evidence-2026-04-11.md @@ -0,0 +1,294 @@ +# Shopify Transfer Order Live Test Evidence + +## 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 + +## Remaining Gaps Still Standing + +These were not disproven by the live test and should still be treated as real gaps unless later evidence shows otherwise: + +1. `TO_Receive_Only` + - Shopify still has no first-class receive-only transfer orchestration equivalent +2. Receipt without shipment linkage + - live Shopify receive calls still require `inventoryShipment` and shipment line ids +3. Unexpected-item receipt + - live Shopify receive flow still operates on shipment line ids, not arbitrary newly arrived products +4. Receiver-driven close of partial receipt + - not shown in the live run +5. Reject to `REJECTED_ITM_PARKING` + - not represented in Shopify transfer cancel flow +6. OMS close-fulfillment semantics + - Shopify still has remove-item and shipment-flow tools, not the same item-close semantics + +## 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. + +## Raw Evidence Files + +- `01_before_create_inventory_levels_item_207-113-G.json` +- `02_create_draft_transfer_A.json` +- `03_after_create_draft_inventory_levels_item_207-113-G.json` +- `04_mark_ready_transfer_A.json` +- `05_after_ready_inventory_levels_item_207-113-G.json` +- `06_create_shipment_draft_A.json` +- `07_shipment_detail_after_create_A.json` +- `08_after_shipment_create_inventory_levels_item_207-113-G.json` +- `09_set_tracking_shipment_A.json` +- `10_mark_shipment_in_transit_A.json` +- `11_after_mark_in_transit_inventory_levels_item_207-113-G.json` +- `12_receive_partial_shipment_A.json` +- `13_shipment_detail_after_partial_receive_A.json` +- `14_after_partial_receive_inventory_levels_item_207-113-G.json` +- `15_receive_final_shipment_A.json` +- `16_shipment_detail_after_final_receive_A.json` +- `17_after_final_receive_inventory_levels_item_207-113-G.json` +- `18_transfer_detail_after_full_receive_A.json` +- `19_create_ready_transfer_B.json` +- `20_create_shipment_B_1.json` +- `20_create_shipment_B_2.json` +- `21_mark_in_transit_B_1.json` +- `21_mark_in_transit_B_2.json` +- `22_transfer_detail_with_two_shipments_B.json` +- `23_shipment_B_1_detail_before_receive.json` +- `23_shipment_B_2_detail_before_receive.json` +- `24_before_receive_transfer_B_inventory_levels_item_207-113-G.json` +- `25_receive_shipment_B_1.json` +- `26_transfer_detail_after_receiving_shipment1_B.json` +- `27_after_receiving_shipment1_B_inventory_levels_item_207-113-G.json` +- `28_receive_shipment_B_2.json` +- `29_transfer_detail_after_receiving_shipment2_B.json` +- `30_after_receiving_shipment2_B_inventory_levels_item_207-113-G.json` +- `31_create_ready_transfer_C.json` +- `32_create_shipment_C.json` +- `33_mark_in_transit_C.json` +- `34_shipment_detail_C_before_receive.json` +- `35_attempt_over_receive_C.json` +- `36_after_over_receive_attempt_inventory_levels_item_207-113-G.json` +- `37_shipment_detail_after_over_receive_C.json` +- `38_transfer_detail_after_over_receive_C.json` + +## References + +- `runtime/component/shopify-oms-bridge/docs/shopify_transfer_order_shopify_scenarios.md` +- `runtime/component/shopify-oms-bridge/docs/shopify_transfer_order_inventory_transfer_mapping.md` +- `runtime/component/shopify-oms-bridge/docs/shopify_transfer_order_vs_oms_gap_analysis.md` 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..40abebaf --- /dev/null +++ b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-test-results-summary-2026-04-11.md @@ -0,0 +1,225 @@ +# 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 | +| 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` | + +### 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 + +### 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 | +| 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-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 From fc55b6fafc1c0dd3f0f17092f1e462b1bcda5bd4 Mon Sep 17 00:00:00 2001 From: Chinmay Sharma <152295063+Chinmay2107@users.noreply.github.com> Date: Sat, 11 Apr 2026 19:32:39 +0530 Subject: [PATCH 02/20] Update shopify-transfer-order-live-test-evidence-2026-04-11.md --- ...fer-order-live-test-evidence-2026-04-11.md | 50 ------------------- 1 file changed, 50 deletions(-) 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 index 4a820931..3c40bd15 100644 --- 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 @@ -242,53 +242,3 @@ This day’s run did not reproduce that issue in the main happy path, but the in ### 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. - -## Raw Evidence Files - -- `01_before_create_inventory_levels_item_207-113-G.json` -- `02_create_draft_transfer_A.json` -- `03_after_create_draft_inventory_levels_item_207-113-G.json` -- `04_mark_ready_transfer_A.json` -- `05_after_ready_inventory_levels_item_207-113-G.json` -- `06_create_shipment_draft_A.json` -- `07_shipment_detail_after_create_A.json` -- `08_after_shipment_create_inventory_levels_item_207-113-G.json` -- `09_set_tracking_shipment_A.json` -- `10_mark_shipment_in_transit_A.json` -- `11_after_mark_in_transit_inventory_levels_item_207-113-G.json` -- `12_receive_partial_shipment_A.json` -- `13_shipment_detail_after_partial_receive_A.json` -- `14_after_partial_receive_inventory_levels_item_207-113-G.json` -- `15_receive_final_shipment_A.json` -- `16_shipment_detail_after_final_receive_A.json` -- `17_after_final_receive_inventory_levels_item_207-113-G.json` -- `18_transfer_detail_after_full_receive_A.json` -- `19_create_ready_transfer_B.json` -- `20_create_shipment_B_1.json` -- `20_create_shipment_B_2.json` -- `21_mark_in_transit_B_1.json` -- `21_mark_in_transit_B_2.json` -- `22_transfer_detail_with_two_shipments_B.json` -- `23_shipment_B_1_detail_before_receive.json` -- `23_shipment_B_2_detail_before_receive.json` -- `24_before_receive_transfer_B_inventory_levels_item_207-113-G.json` -- `25_receive_shipment_B_1.json` -- `26_transfer_detail_after_receiving_shipment1_B.json` -- `27_after_receiving_shipment1_B_inventory_levels_item_207-113-G.json` -- `28_receive_shipment_B_2.json` -- `29_transfer_detail_after_receiving_shipment2_B.json` -- `30_after_receiving_shipment2_B_inventory_levels_item_207-113-G.json` -- `31_create_ready_transfer_C.json` -- `32_create_shipment_C.json` -- `33_mark_in_transit_C.json` -- `34_shipment_detail_C_before_receive.json` -- `35_attempt_over_receive_C.json` -- `36_after_over_receive_attempt_inventory_levels_item_207-113-G.json` -- `37_shipment_detail_after_over_receive_C.json` -- `38_transfer_detail_after_over_receive_C.json` - -## References - -- `runtime/component/shopify-oms-bridge/docs/shopify_transfer_order_shopify_scenarios.md` -- `runtime/component/shopify-oms-bridge/docs/shopify_transfer_order_inventory_transfer_mapping.md` -- `runtime/component/shopify-oms-bridge/docs/shopify_transfer_order_vs_oms_gap_analysis.md` From bf8987a4835f017ecf6590f3cc2a3f481883679a Mon Sep 17 00:00:00 2001 From: Chinmay2107 Date: Sat, 11 Apr 2026 19:43:21 +0530 Subject: [PATCH 03/20] Add Shopify transfer gap proof evidence --- ...fer-order-gap-proof-evidence-2026-04-11.md | 212 ++++++++++++++++++ ...fer-order-live-test-evidence-2026-04-11.md | 38 ++-- ...r-order-test-results-summary-2026-04-11.md | 42 ++++ 3 files changed, 276 insertions(+), 16 deletions(-) create mode 100644 project-ideas/shopify-integration/transfer-order/shopify-transfer-order-gap-proof-evidence-2026-04-11.md 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..312ce592 --- /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 Evidence + +## 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-live-test-evidence-2026-04-11.md b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-live-test-evidence-2026-04-11.md index 3c40bd15..406caa61 100644 --- 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 @@ -214,22 +214,28 @@ 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 -## Remaining Gaps Still Standing - -These were not disproven by the live test and should still be treated as real gaps unless later evidence shows otherwise: - -1. `TO_Receive_Only` - - Shopify still has no first-class receive-only transfer orchestration equivalent -2. Receipt without shipment linkage - - live Shopify receive calls still require `inventoryShipment` and shipment line ids -3. Unexpected-item receipt - - live Shopify receive flow still operates on shipment line ids, not arbitrary newly arrived products -4. Receiver-driven close of partial receipt - - not shown in the live run -5. Reject to `REJECTED_ITM_PARKING` - - not represented in Shopify transfer cancel flow -6. OMS close-fulfillment semantics - - Shopify still has remove-item and shipment-flow tools, not the same item-close semantics +## 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 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 index 40abebaf..c445b7d9 100644 --- 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 @@ -32,6 +32,8 @@ The same test set also proved that Shopify is not a one-to-one replacement for O | 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 | | 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 | @@ -57,6 +59,8 @@ All links use the Shopify admin transfer URL pattern: | 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` | ### Bulk create-only probe @@ -135,6 +139,41 @@ Inventory conclusion: - 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 + ### Bulk route: `LGW-AUS-EXEC2` | Stage | Origin available | Origin reserved | Origin on_hand | Destination available | Destination incoming | Destination on_hand | @@ -203,6 +242,8 @@ Route conclusion: | 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 | +| 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 @@ -222,4 +263,5 @@ These tests also prove that OMS should remain the system of record for Transfer - `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-bulk-live-test-evidence-2026-04-11.md` From 39cfcd4b2fc78f6cfb38dda98cd5a5c9190bc086 Mon Sep 17 00:00:00 2001 From: Chinmay2107 Date: Sun, 12 Apr 2026 14:04:08 +0530 Subject: [PATCH 04/20] Add external-end Shopify transfer evidence --- ...ernal-end-live-test-evidence-2026-04-12.md | 172 ++++++++++++++++++ ...r-order-test-results-summary-2026-04-11.md | 39 ++++ 2 files changed, 211 insertions(+) create mode 100644 project-ideas/shopify-integration/transfer-order/shopify-transfer-order-external-end-live-test-evidence-2026-04-12.md 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..73af8a80 --- /dev/null +++ b/project-ideas/shopify-integration/transfer-order/shopify-transfer-order-external-end-live-test-evidence-2026-04-12.md @@ -0,0 +1,172 @@ +# Shopify Transfer Order External-End Live Test Evidence + +## Purpose + +This document records the live Shopify tests run on April 12, 2026 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. + +Raw API evidence is stored under: + +- `runtime/component/shopify-oms-bridge/docs/evidence/shopify_transfer_external_end_tests_2026-04-12` + +## Environment + +- Shop: `gorjana-sandbox.myshopify.com` +- API version: `2026-01` +- SKU: `207-113-G` +- Inventory item: `gid://shopify/InventoryItem/46295996661804` +- 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 +- the immediate shipment create worked and remained linked through the transfer’s `shipments` collection + +## 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 + +## Important Technical Note + +`InventoryShipment` still does not expose a direct parent-transfer field in the reviewed schema. + +So the shipment-to-transfer linkage in these tests was proven by: + +- creating the shipment with `movementId = inventoryTransfer.id` +- querying the transfer back and confirming the created shipment appeared under `inventoryTransfer.shipments` +- verifying the shipment line items matched the transfer line items exactly + +## Final Conclusion + +These two specific scenarios are supported by Shopify: + +- Shopify location -> external destination +- external origin -> Shopify location + +The tested result is not theoretical. Both flows created real Shopify transfers and real Shopify shipments with one omitted end of the movement. + +What remains separate from this proof is later execution behavior such as: + +- marking these shipments in transit +- receiving the inbound flow +- comparing inventory movement after in-transit and receipt + +This run proves the create-and-link behavior only, which is the exact scope requested. 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 index c445b7d9..e77dfb51 100644 --- 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 @@ -34,6 +34,8 @@ The same test set also proved that Shopify is not a one-to-one replacement for O | 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 | @@ -61,6 +63,8 @@ All links use the Shopify admin transfer URL pattern: | 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 @@ -174,6 +178,39 @@ 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 | @@ -242,6 +279,7 @@ Route conclusion: | 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 | @@ -264,4 +302,5 @@ These tests also prove that OMS should remain the system of record for Transfer - `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` From d6a92950b9ce13d4a41d3884a0a7ce34b917a3ba Mon Sep 17 00:00:00 2001 From: Chinmay Sharma <152295063+Chinmay2107@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:43:11 +0530 Subject: [PATCH 05/20] Update shopify-transfer-order-external-end-live-test-evidence-2026-04-12.md --- ...ernal-end-live-test-evidence-2026-04-12.md | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) 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 index 73af8a80..156cd1c8 100644 --- 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 @@ -2,23 +2,17 @@ ## Purpose -This document records the live Shopify tests run on April 12, 2026 for two one-sided transfer scenarios: +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. -Raw API evidence is stored under: - -- `runtime/component/shopify-oms-bridge/docs/evidence/shopify_transfer_external_end_tests_2026-04-12` - ## Environment -- Shop: `gorjana-sandbox.myshopify.com` - API version: `2026-01` - SKU: `207-113-G` -- Inventory item: `gid://shopify/InventoryItem/46295996661804` - Quantity tested: `1` - Shopify location used as origin: `Atlanta` - Shopify location used as destination: `Austin` @@ -87,7 +81,6 @@ 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 -- the immediate shipment create worked and remained linked through the transfer’s `shipments` collection ## Scenario 2: Inbound To Shopify Location From External Origin @@ -144,16 +137,6 @@ Conclusion: - 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 -## Important Technical Note - -`InventoryShipment` still does not expose a direct parent-transfer field in the reviewed schema. - -So the shipment-to-transfer linkage in these tests was proven by: - -- creating the shipment with `movementId = inventoryTransfer.id` -- querying the transfer back and confirming the created shipment appeared under `inventoryTransfer.shipments` -- verifying the shipment line items matched the transfer line items exactly - ## Final Conclusion These two specific scenarios are supported by Shopify: @@ -161,12 +144,8 @@ These two specific scenarios are supported by Shopify: - Shopify location -> external destination - external origin -> Shopify location -The tested result is not theoretical. Both flows created real Shopify transfers and real Shopify shipments with one omitted end of the movement. +What remains separate from this is later execution behavior such as: -What remains separate from this proof is later execution behavior such as: - -- marking these shipments in transit +- marking these shipments in transit should move the inventory - receiving the inbound flow - comparing inventory movement after in-transit and receipt - -This run proves the create-and-link behavior only, which is the exact scope requested. From 6ca9039975f8f39502000bed1de29da57607cbbc Mon Sep 17 00:00:00 2001 From: Chinmay Sharma <152295063+Chinmay2107@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:27:11 +0530 Subject: [PATCH 06/20] Update shopify-transfer-order-external-end-live-test-evidence-2026-04-12.md --- ...transfer-order-external-end-live-test-evidence-2026-04-12.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 156cd1c8..dd8d6263 100644 --- 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 @@ -1,4 +1,4 @@ -# Shopify Transfer Order External-End Live Test Evidence +# Shopify Transfer Order Test Results ## Purpose From 041a4b8303cb179a9ec84cbf9e382c2531b71dd9 Mon Sep 17 00:00:00 2001 From: Chinmay Sharma <152295063+Chinmay2107@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:27:44 +0530 Subject: [PATCH 07/20] Revise title and purpose of Shopify test evidence Updated the document title and clarified the purpose of the test evidence. --- ...shopify-transfer-order-bulk-live-test-evidence-2026-04-11.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index c4ad889d..c4fe164d 100644 --- 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 @@ -1,4 +1,4 @@ -# Shopify Transfer Order Bulk Live Test Evidence +# Shopify Transfer Order Bulk Test Results using GraphQL APIs ## Purpose From 0f7298cb13fe0245ea199c7c9cf4a4896b7233a5 Mon Sep 17 00:00:00 2001 From: Chinmay Sharma <152295063+Chinmay2107@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:28:08 +0530 Subject: [PATCH 08/20] Update shopify-transfer-order-gap-proof-evidence-2026-04-11.md --- .../shopify-transfer-order-gap-proof-evidence-2026-04-11.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 312ce592..3b21c91e 100644 --- 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 @@ -1,4 +1,4 @@ -# Shopify Transfer Order Gap Proof Evidence +# Shopify Transfer Order Gap Proof ## Purpose From efd2c168464e7bc58d422c50f792e5e144798c04 Mon Sep 17 00:00:00 2001 From: Chinmay Sharma <152295063+Chinmay2107@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:28:26 +0530 Subject: [PATCH 09/20] Update title and purpose of test evidence document --- .../shopify-transfer-order-live-test-evidence-2026-04-11.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 406caa61..f1131c79 100644 --- 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 @@ -1,4 +1,4 @@ -# Shopify Transfer Order Live Test Evidence +# Shopify Transfer Order Test Results using GraphQL APIs ## Purpose From 2c8c99b0f809ed49eaa50e4328da48ebb968c69e Mon Sep 17 00:00:00 2001 From: Chinmay2107 Date: Tue, 14 Apr 2026 11:17:47 +0530 Subject: [PATCH 10/20] Add Shopify inventory sync trigger design doc --- ...ify-event-based-inventory-sync-triggers.md | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md 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..51b84fe5 --- /dev/null +++ b/project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md @@ -0,0 +1,192 @@ +# Shopify Event-Based Inventory Sync Triggers + +## Purpose + +This document summarizes the OMS events that should drive Shopify inventory updates and recommends the lowest-load trigger boundary for each event. + +The target design is event-based. OMS remains the system of record. Shopify is updated so store inventory stays usable during the day without relying on repeated full resets. + +## Executive Summary + +The event model should be split into four lanes: + +1. Store fulfillment events +2. Transfer shipment events +3. Inventory adjustment events +4. External reset events + +Do not drive this from broad `InventoryItemDetail` or `Shipment` watches. Those entities are too generic and too noisy. Use the business event that actually changed inventory, queue a small outbox message, and let a dedicated Shopify feed service post the corresponding Shopify workflow. + +For transfer orders, this means: + +- create only the minimum Shopify `InventoryTransfer` records needed to support `InventoryShipment` +- do not use Shopify to manage OMS transfer order lifecycle +- use Shopify transfer and shipment APIs only to reproduce the inventory effect that OMS already decided + +## Recommended Trigger Model + +Use one shared queue service and multiple small sender services. + +### Shared Queue + +- `queue#ShopifyInventoryEvent` +- Inputs: `eventType`, `entityName`, `primaryKey`, `shopId`, `facilityId`, `eventDate` +- Responsibility: + - create one outbox/system-message style event + - deduplicate by a deterministic key + - retry safely + - hand off to the correct sender service + +### Sender Services + +- `send#ShopifyStoreFulfillmentEvent` +- `send#ShopifyTransferShipmentOutEvent` +- `send#ShopifyTransferReceiptInEvent` +- `send#ShopifyInventoryAdjustmentEvent` +- `send#ShopifyInventoryResetEvent` + +This split is better than one large receiver because each lane has different source entities, grouping rules, and Shopify APIs. + +## Trigger Matrix + +| OMS event | Recommended trigger boundary | Why this is the right trigger | Shopify workflow | Feed service behavior | +| --- | --- | --- | --- | --- | +| Store-origin TO reservation reduces ATP | `co.hotwax.oms.impl.OrderReservationServices.process#OrderItemAllocation` after commit, only for transfer orders originating from stores | This is the exact point where OMS reserves sellable inventory for the TO | Create or update a Shopify `InventoryTransfer` and move it to `READY_TO_SHIP` so Shopify origin `available` decreases and `reserved` increases | Build one transfer payload per OMS TO and store route, resolve origin and destination Shopify locations, aggregate line quantities by `inventoryItemId`, create draft transfer if needed, then mark ready to ship | +| Store-origin TO shipment reduces QOH | `co.hotwax.poorti.TransferOrderFulfillmentServices.ship#TransferOrderShipment` post-service | This service creates `ItemIssuance`, writes `InventoryItemDetail.quantityOnHandDiff = -qty`, and marks the shipment `SHIPMENT_SHIPPED` | Ensure prerequisite `InventoryTransfer` exists, create Shopify `InventoryShipment`, then mark shipment in transit | Read the shipped OMS transfer shipment, map order items to Shopify inventory items, create the shipment against the transfer, push tracking if available, and mark in transit so Shopify origin `on_hand` drops and destination `incoming` rises | +| TO receipt into store increases ATP and QOH | `ShipmentReceipt` create or update, grouped by `shipmentId + datetimeReceived + facilityId` | Receipt quantity is stored on `ShipmentReceipt`, not on a single shipment status transition | `inventoryShipmentReceive` against the existing Shopify shipment | Group shipment-backed receipts from the same receiving session, sum by `shipmentItemId`, and post receipt quantities to Shopify; if OMS creates receipt rows without shipment linkage, keep them in exception handling because Shopify cannot receive against a transfer item without a shipment line | +| Online order shipped from store | `co.hotwax.poorti.FulfillmentServices.ship#Shipment` post-service | This is the point where OMS issues stock and marks the shipment shipped | First move the Shopify Fulfillment Order to the actual shipping store when needed, then create the Shopify fulfillment | Resolve Shopify order and fulfillment order, compare assigned location with actual OMS shipping facility, call Fulfillment Order move when the store differs, then create the fulfillment with tracking | +| External POS sale or non-Shopify-origin sale reduces ATP or QOH | A dedicated sales posting boundary, not Shopify fulfillment | There is no Shopify order or fulfillment order to absorb this inventory change | Use `inventoryAdjustQuantities` as an inventory-only correction path | For final sale, reduce `available` and `on_hand`; for reservation-only behavior, reduce `available` only; do not do this for Shopify POS orders because Shopify already owns those reservations and fulfillments | +| Cycle count or approved manual variance changes ATP and QOH | `co.hotwax.cycleCount.InventoryCountServices.create#PhysicalInventory` when `decisionOutcomeEnumId = APPLIED`, or an outbox event on created `PhysicalInventory` / `InventoryItemVariance` | This is the business-approved inventory correction point | `inventoryAdjustQuantities` | Post the applied variance to Shopify for the mapped location and inventory item; when OMS changed both ATP and QOH together, send both quantity-state adjustments; manual variance should use this same lane | +| External authoritative reset from ERP, WMS, or NetSuite | `reset#InventoryItem` result or `create#ExternalInventoryReset` completion | This is the authoritative reconciliation boundary | Absolute set workflow using Shopify inventory set mutations, not transfer or fulfillment APIs | Resolve the final authoritative quantities and set them in Shopify; use delta mutations only when the external source explicitly sends deltas, otherwise use set-style mutations so Shopify lands on the same truth as OMS | + +## Why These Trigger Boundaries Are Lower Load + +### Do Not Watch `Shipment` + +`Shipment` changes for many reasons that do not all require Shopify inventory updates: + +- creation +- package changes +- route segment changes +- tracking updates +- status changes + +A broad `Shipment` feed would create unnecessary reads and false positives. + +### Use `ShipmentReceipt` For Inbound TO Inventory + +Inbound transfer receipt should not be driven only from `receive#TransferOrder` or from `ShipmentStatus`. + +OMS receiving can: + +- split one receiving action across multiple shipments +- receive at item level +- create over-receipt rows without shipment linkage +- stamp all rows from one receiving session with the same `datetimeReceived` + +That means `ShipmentReceipt` is the durable source for inbound quantity, and `shipmentId + datetimeReceived + facilityId` is the correct grouping key for Shopify receipt posting. + +### Use Post-Service Or Tx-Commit Boundaries For Outbound Events + +For outbound TO shipment and sales shipment, the correct event happens only after OMS has already written the issuance and inventory ledger entries. That keeps Shopify sync aligned with committed OMS state and avoids duplicate or premature calls. + +## Shopify Workflow By Lane + +### 1. Store Fulfillment Events + +Use this for online orders shipped from stores. + +Workflow: + +1. Find the Shopify order and open fulfillment orders. +2. Identify the actual shipping store from OMS. +3. If Shopify assigned a different location, move the fulfillment order to the actual store. +4. Create the Shopify fulfillment from that store. + +This is the correct Shopify equivalent because store-shipped orders should be fulfilled against the actual retail location that issued the inventory. + +### 2. Transfer Shipment Events + +Use this for store to warehouse, warehouse to store, and store to store transfer movement. + +Workflow: + +1. On reservation, create the minimum Shopify `InventoryTransfer` representation and move it to `READY_TO_SHIP` when OMS has reserved ATP that Shopify must reflect. +2. On ship, create `InventoryShipment` from the transfer and mark it in transit. +3. On receive, call `inventoryShipmentReceive`. + +This is the cleanest Shopify equivalent available today because Shopify does not allow `InventoryShipment` without `InventoryTransfer`. + +### 3. Inventory Adjustment Events + +Use this for cycle count and approved manual correction. + +Workflow: + +1. Read the approved variance from OMS. +2. Resolve the Shopify inventory item and location. +3. Post `inventoryAdjustQuantities`. + +Use this when the business action is a correction, not a shipment or fulfillment. + +### 4. External Reset Events + +Use this when an external system is the source of truth and OMS has already accepted the reconciliation. + +Workflow: + +1. Read the final authoritative quantity after `reset#InventoryItem` or `create#ExternalInventoryReset`. +2. Resolve the Shopify inventory item and location. +3. Set Shopify to the final target quantity using set-style inventory mutations. + +This lane should not reuse transfer or fulfillment APIs because the business event is reconciliation, not movement. + +## Transfer-Specific Notes + +Live Shopify transfer testing proved the following behavior: + +- Shopify cannot create `InventoryShipment` without `InventoryTransfer` +- `READY_TO_SHIP` reduces origin `available` and increases origin `reserved` +- marking shipment in transit reduces origin `on_hand` and increases destination `incoming` +- receiving increases destination `available` and decreases destination `incoming` + +That means the minimum Shopify flow needed to reproduce OMS transfer inventory is: + +1. reserve using `InventoryTransfer` in ready state when OMS reservation must affect Shopify ATP +2. ship using `InventoryShipment` +3. receive using `inventoryShipmentReceive` + +## Implementation Notes + +- Prefer service ECAs or tx-commit hooks for outbound reservation and shipment events. +- Prefer `ShipmentReceipt`-driven feeds for inbound transfer receipt. +- Keep one idempotency key per business event, not per retry. +- If event-based posting fails, replay from the outbox. Do not fall back first to broad polling of `InventoryItemDetail`. +- Scheduled recovery can exist as a safety net, but it should read failed outbox events, not re-derive all business changes from scratch. + +## Recommended Initial Cut + +Implement these three triggers first: + +1. `process#OrderItemAllocation` for store-origin transfer reservation +2. `ship#TransferOrderShipment` for outbound transfer shipment +3. `ShipmentReceipt` create or update for inbound transfer receipt + +Then add: + +4. `ship#Shipment` for store fulfillment of online orders +5. `create#PhysicalInventory` for cycle count and manual variance +6. `reset#InventoryItem` or `create#ExternalInventoryReset` for authoritative resets + +This gives the shortest path to reducing daytime inventory drift on Shopify while keeping OMS as the only system that owns the business process. + +## References + +- `runtime/component/oms/service/co/hotwax/oms/impl/OrderReservationServices.xml` +- `runtime/component/oms/service/co/hotwax/orderledger/order/TransferOrderServices.xml` +- `runtime/component/poorti/service/co/hotwax/poorti/TransferOrderFulfillmentServices.xml` +- `runtime/component/poorti/service/co/hotwax/poorti/FulfillmentServices.xml` +- `runtime/component/poorti/service/co/hotwax/cycleCount/InventoryCountServices.xml` +- `project-ideas/fulfillment-center-mgmt/inventory-mgmt/reset_inventory_item_spec.md` +- `project-ideas/fulfillment-center-mgmt/inventory-mgmt/external_inventory_reset_design.md` +- Shopify Admin GraphQL: `inventoryTransferCreate`, `inventoryTransferMarkAsReadyToShip`, `inventoryShipmentCreate`, `inventoryShipmentMarkInTransit`, `inventoryShipmentReceive`, `inventoryAdjustQuantities`, `inventorySetQuantities`, `fulfillmentOrderMove`, `fulfillmentCreate` From b6bb76e3e218a79bcc1a33d2676fc32682366c28 Mon Sep 17 00:00:00 2001 From: Chinmay2107 Date: Tue, 14 Apr 2026 12:31:23 +0530 Subject: [PATCH 11/20] Refine Shopify inventory sync trigger design --- ...ify-event-based-inventory-sync-triggers.md | 307 ++++++++++-------- 1 file changed, 180 insertions(+), 127 deletions(-) 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 index 51b84fe5..2fc0fbf5 100644 --- a/project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md +++ b/project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md @@ -2,191 +2,244 @@ ## Purpose -This document summarizes the OMS events that should drive Shopify inventory updates and recommends the lowest-load trigger boundary for each event. +This document defines a simple event-driven design for keeping Shopify inventory aligned with OMS. -The target design is event-based. OMS remains the system of record. Shopify is updated so store inventory stays usable during the day without relying on repeated full resets. +OMS remains the system of record. Shopify is updated only to reflect inventory impact at mapped Shopify locations. The design is delta-based only. It does not use hard inventory reset during the day. -## Executive Summary +## Pre-Requisites -The event model should be split into four lanes: +- 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, no trigger should create history and no batch should post inventory to Shopify. + +Without a matched starting baseline, a delta-only design will drift instead of converge. + +## Design Summary + +The model has four lanes: 1. Store fulfillment events 2. Transfer shipment events 3. Inventory adjustment events 4. External reset events -Do not drive this from broad `InventoryItemDetail` or `Shipment` watches. Those entities are too generic and too noisy. Use the business event that actually changed inventory, queue a small outbox message, and let a dedicated Shopify feed service post the corresponding Shopify workflow. - -For transfer orders, this means: - -- create only the minimum Shopify `InventoryTransfer` records needed to support `InventoryShipment` -- do not use Shopify to manage OMS transfer order lifecycle -- use Shopify transfer and shipment APIs only to reproduce the inventory effect that OMS already decided - -## Recommended Trigger Model +The design should work like this: + +1. A service SECA runs after the OMS business service completes. +2. The SECA creates one row in a source-specific Shopify sync history entity. +3. A batch service runs every 15 minutes. +4. The batch finds history rows not yet sent to Shopify and posts the required Shopify delta workflow. +5. The batch also finds source records for which history was never created and backfills the missing history rows before sending them. + +This gives two layers of safety: + +- primary event capture through service SECAs +- recovery through the 15-minute batch + +This design does not use `SystemMessage`. + +## 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. +- Each trigger source gets its own sync history entity. +- Batch posting reads from sync history first, not from raw ledger noise. +- Transfer lifecycle is not mirrored in Shopify for business control. Transfer APIs are used only when Shopify requires them for inventory movement. + +## Sync Architecture + +### Trigger Model + +Use service SECAs on the OMS services that already represent the committed business boundary. + +Do not use a broad data feed on `InventoryItemDetail` or `Shipment`. Those are too generic and will create unnecessary load and ambiguity. + +### History Model + +Each trigger source should have its own history entity. Example split: + +| Source event | History entity | +| --- | --- | +| TO reservation at store | `ShopifyToReservationSyncHistory` | +| TO outbound shipment | `ShopifyTransferShipmentSyncHistory` | +| TO inbound receipt | `ShopifyTransferReceiptSyncHistory` | +| Store fulfillment shipment | `ShopifyStoreFulfillmentSyncHistory` | +| Cycle count or manual variance | `ShopifyInventoryAdjustmentSyncHistory` | +| External reset delta | `ShopifyExternalResetSyncHistory` | +| SO cancel or reject reservation release | `ShopifySalesReservationReleaseSyncHistory` | +| TO cancel or reject reservation release | `ShopifyToReservationReleaseSyncHistory` | + +Each history row should store at least: + +- source business key +- productStoreId +- facilityId +- shopId when resolved +- event type +- delta quantity payload or enough data to derive it +- status such as `PENDING`, `SENT`, `FAILED`, `SKIPPED` +- retry count +- last error text +- created date and sent date + +## Processing Flow + +```mermaid +flowchart TD + A[OMS business service completes] --> B[SECA checks ProductStoreSetting] + B -->|sync disabled| C[Skip] + B -->|sync enabled| D[Create source-specific sync history row] + D --> E[15 minute batch] + E --> F[Backfill missing history rows] + F --> G[Read pending history rows] + G --> H[Build Shopify delta workflow] + H --> I[Post to Shopify] + I -->|success| J[Mark history SENT] + I -->|failure| K[Mark history FAILED for retry] +``` + +## Sequence View + +```mermaid +sequenceDiagram + participant OMS as OMS Service + participant SECA as Service SECA + participant HIST as Sync History Entity + participant BATCH as 15 Min Batch + participant SHOP as Shopify + + OMS->>SECA: business transaction committed + SECA->>SECA: check ProductStoreSetting + SECA->>HIST: create pending history row + BATCH->>HIST: find missing or pending rows + BATCH->>SHOP: post delta workflow + SHOP-->>BATCH: success or error + BATCH->>HIST: update SENT or FAILED +``` -Use one shared queue service and multiple small sender services. - -### Shared Queue - -- `queue#ShopifyInventoryEvent` -- Inputs: `eventType`, `entityName`, `primaryKey`, `shopId`, `facilityId`, `eventDate` -- Responsibility: - - create one outbox/system-message style event - - deduplicate by a deterministic key - - retry safely - - hand off to the correct sender service - -### Sender Services +## Trigger Matrix -- `send#ShopifyStoreFulfillmentEvent` -- `send#ShopifyTransferShipmentOutEvent` -- `send#ShopifyTransferReceiptInEvent` -- `send#ShopifyInventoryAdjustmentEvent` -- `send#ShopifyInventoryResetEvent` +| OMS event | SECA trigger boundary | Sync history entity | Shopify workflow | Notes | +| --- | --- | --- | --- | --- | +| Store-origin TO reservation reduces ATP | `co.hotwax.oms.impl.OrderReservationServices.process#OrderItemAllocation` after commit, only for transfer orders originating from stores | `ShopifyToReservationSyncHistory` | Create or update a minimal Shopify `InventoryTransfer` and move it to `READY_TO_SHIP` so origin `available` decreases and `reserved` increases | This is needed only because Shopify cannot create `InventoryShipment` without `InventoryTransfer` | +| Store-origin TO outbound shipment reduces QOH | `co.hotwax.poorti.TransferOrderFulfillmentServices.ship#TransferOrderShipment` post-service | `ShopifyTransferShipmentSyncHistory` | 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` | `ShopifyTransferReceiptSyncHistory` | `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 | `ShopifyStoreFulfillmentSyncHistory` | 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 service boundary | `ShopifyInventoryAdjustmentSyncHistory` | `inventoryAdjustQuantities` | Use only when Shopify is not already the system that created the sale | +| Cycle count or approved manual variance changes QOH and ATP | `co.hotwax.cycleCount.InventoryCountServices.create#PhysicalInventory` when variance is applied | `ShopifyInventoryAdjustmentSyncHistory` | `inventoryAdjustQuantities` | Manual variance follows the same lane | +| External reset accepted by OMS | `reset#InventoryItem` or `create#ExternalInventoryReset` completion | `ShopifyExternalResetSyncHistory` | `inventoryAdjustQuantities` using computed delta only | Even this lane stays delta-based after OMS computes the difference | +| TO cancel before shipment or receipt | `co.hotwax.orderledger.order.TransferOrderServices.cancel#TransferOrder` post-service | `ShopifyToReservationReleaseSyncHistory` | Cancel the related Shopify transfer or remove remaining draft-ready quantities so origin `available` is restored | OMS cancels reservation before item status moves to `ITEM_CANCELLED` | +| TO reject before shipment | `co.hotwax.poorti.TransferOrderFulfillmentServices.reject#TransferOrder` post-service | `ShopifyToReservationReleaseSyncHistory` | Cancel the related Shopify transfer or remove remaining quantities from the sellable route | OMS cancels reservation and moves the item to `REJECTED_ITM_PARKING`; Shopify should restore sellable stock at the original mapped location | +| SO reject or SO cancel before shipment | `co.hotwax.oms.order.OrderServices.reject#OrderItem` and sales-order cancel service boundary | `ShopifySalesReservationReleaseSyncHistory` | Reverse the earlier reservation delta so facility `available` increases | This is required when OMS owned the reservation that reduced Shopify inventory | -This split is better than one large receiver because each lane has different source entities, grouping rules, and Shopify APIs. +## Cancellation And Rejection Behavior -## Trigger Matrix +### Transfer Order Cancel -| OMS event | Recommended trigger boundary | Why this is the right trigger | Shopify workflow | Feed service behavior | -| --- | --- | --- | --- | --- | -| Store-origin TO reservation reduces ATP | `co.hotwax.oms.impl.OrderReservationServices.process#OrderItemAllocation` after commit, only for transfer orders originating from stores | This is the exact point where OMS reserves sellable inventory for the TO | Create or update a Shopify `InventoryTransfer` and move it to `READY_TO_SHIP` so Shopify origin `available` decreases and `reserved` increases | Build one transfer payload per OMS TO and store route, resolve origin and destination Shopify locations, aggregate line quantities by `inventoryItemId`, create draft transfer if needed, then mark ready to ship | -| Store-origin TO shipment reduces QOH | `co.hotwax.poorti.TransferOrderFulfillmentServices.ship#TransferOrderShipment` post-service | This service creates `ItemIssuance`, writes `InventoryItemDetail.quantityOnHandDiff = -qty`, and marks the shipment `SHIPMENT_SHIPPED` | Ensure prerequisite `InventoryTransfer` exists, create Shopify `InventoryShipment`, then mark shipment in transit | Read the shipped OMS transfer shipment, map order items to Shopify inventory items, create the shipment against the transfer, push tracking if available, and mark in transit so Shopify origin `on_hand` drops and destination `incoming` rises | -| TO receipt into store increases ATP and QOH | `ShipmentReceipt` create or update, grouped by `shipmentId + datetimeReceived + facilityId` | Receipt quantity is stored on `ShipmentReceipt`, not on a single shipment status transition | `inventoryShipmentReceive` against the existing Shopify shipment | Group shipment-backed receipts from the same receiving session, sum by `shipmentItemId`, and post receipt quantities to Shopify; if OMS creates receipt rows without shipment linkage, keep them in exception handling because Shopify cannot receive against a transfer item without a shipment line | -| Online order shipped from store | `co.hotwax.poorti.FulfillmentServices.ship#Shipment` post-service | This is the point where OMS issues stock and marks the shipment shipped | First move the Shopify Fulfillment Order to the actual shipping store when needed, then create the Shopify fulfillment | Resolve Shopify order and fulfillment order, compare assigned location with actual OMS shipping facility, call Fulfillment Order move when the store differs, then create the fulfillment with tracking | -| External POS sale or non-Shopify-origin sale reduces ATP or QOH | A dedicated sales posting boundary, not Shopify fulfillment | There is no Shopify order or fulfillment order to absorb this inventory change | Use `inventoryAdjustQuantities` as an inventory-only correction path | For final sale, reduce `available` and `on_hand`; for reservation-only behavior, reduce `available` only; do not do this for Shopify POS orders because Shopify already owns those reservations and fulfillments | -| Cycle count or approved manual variance changes ATP and QOH | `co.hotwax.cycleCount.InventoryCountServices.create#PhysicalInventory` when `decisionOutcomeEnumId = APPLIED`, or an outbox event on created `PhysicalInventory` / `InventoryItemVariance` | This is the business-approved inventory correction point | `inventoryAdjustQuantities` | Post the applied variance to Shopify for the mapped location and inventory item; when OMS changed both ATP and QOH together, send both quantity-state adjustments; manual variance should use this same lane | -| External authoritative reset from ERP, WMS, or NetSuite | `reset#InventoryItem` result or `create#ExternalInventoryReset` completion | This is the authoritative reconciliation boundary | Absolute set workflow using Shopify inventory set mutations, not transfer or fulfillment APIs | Resolve the final authoritative quantities and set them in Shopify; use delta mutations only when the external source explicitly sends deltas, otherwise use set-style mutations so Shopify lands on the same truth as OMS | +OMS transfer order cancel already reverts reservation before shipment starts. -## Why These Trigger Boundaries Are Lower Load +The cancel service: -### Do Not Watch `Shipment` +- blocks cancel if shipment is packed or shipped +- blocks cancel if any receipt already exists +- cancels reservation for each TO item +- moves item status to `ITEM_CANCELLED` -`Shipment` changes for many reasons that do not all require Shopify inventory updates: +So the Shopify side must reverse the reservation effect already posted earlier. That means restoring origin sellable inventory, not creating a new shipment flow. -- creation -- package changes -- route segment changes -- tracking updates -- status changes +### Transfer Order Reject -A broad `Shipment` feed would create unnecessary reads and false positives. +OMS transfer order reject already cancels reservation before shipment starts. -### Use `ShipmentReceipt` For Inbound TO Inventory +The reject service: -Inbound transfer receipt should not be driven only from `receive#TransferOrder` or from `ShipmentStatus`. +- blocks reject if fulfillment already started +- rejects all items together +- calls `reject#OrderItem` +- `reject#OrderItem` cancels reservation on the original facility +- then moves the item to `REJECTED_ITM_PARKING` -OMS receiving can: +For Shopify, the important impact is the release of origin sellable reservation. The rejected virtual facility does not need a Shopify mirror unless the business explicitly maps such a facility to Shopify, which is not the normal case. -- split one receiving action across multiple shipments -- receive at item level -- create over-receipt rows without shipment linkage -- stamp all rows from one receiving session with the same `datetimeReceived` +### Sales Order Reject Or Cancel -That means `ShipmentReceipt` is the durable source for inbound quantity, and `shipmentId + datetimeReceived + facilityId` is the correct grouping key for Shopify receipt posting. +OMS sales order reject and cancel also release reservation. -### Use Post-Service Or Tx-Commit Boundaries For Outbound Events +The sales order cancel path calls `reject#OrderItem`, and `reject#OrderItem` cancels reservation on the original facility before reallocating to the reject facility. -For outbound TO shipment and sales shipment, the correct event happens only after OMS has already written the issuance and inventory ledger entries. That keeps Shopify sync aligned with committed OMS state and avoids duplicate or premature calls. +For Shopify, when OMS owned the reservation, the facility that lost ATP earlier must gain it back on reject or cancel. ## Shopify Workflow By Lane -### 1. Store Fulfillment Events +### 1. Store Fulfillment Lane -Use this for online orders shipped from stores. +Use this for online orders fulfilled from a retail store. Workflow: -1. Find the Shopify order and open fulfillment orders. -2. Identify the actual shipping store from OMS. +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. -This is the correct Shopify equivalent because store-shipped orders should be fulfilled against the actual retail location that issued the inventory. - -### 2. Transfer Shipment Events +### 2. Transfer Shipment Lane Use this for store to warehouse, warehouse to store, and store to store transfer movement. Workflow: -1. On reservation, create the minimum Shopify `InventoryTransfer` representation and move it to `READY_TO_SHIP` when OMS has reserved ATP that Shopify must reflect. -2. On ship, create `InventoryShipment` from the transfer and mark it in transit. -3. On receive, call `inventoryShipmentReceive`. +1. On OMS reservation, create the minimum `InventoryTransfer` needed for Shopify inventory reservation. +2. On OMS ship, create `InventoryShipment` and mark it in transit. +3. On OMS receive, call `inventoryShipmentReceive`. +4. On OMS cancel or reject before ship, cancel or shrink the Shopify transfer so reserved stock is released. -This is the cleanest Shopify equivalent available today because Shopify does not allow `InventoryShipment` without `InventoryTransfer`. +### 3. Inventory Adjustment Lane -### 3. Inventory Adjustment Events +Use this for: -Use this for cycle count and approved manual correction. +- cycle count +- manual variance +- external POS sale when Shopify did not create the sale +- reservation release adjustments when a compensation delta is simpler than a transfer cancellation Workflow: -1. Read the approved variance from OMS. -2. Resolve the Shopify inventory item and location. +1. Resolve location and inventory item. +2. Build delta quantity change. 3. Post `inventoryAdjustQuantities`. -Use this when the business action is a correction, not a shipment or fulfillment. - -### 4. External Reset Events +### 4. External Reset Lane -Use this when an external system is the source of truth and OMS has already accepted the reconciliation. +Use this when an external system sends a new truth to OMS and OMS computes a difference. Workflow: -1. Read the final authoritative quantity after `reset#InventoryItem` or `create#ExternalInventoryReset`. -2. Resolve the Shopify inventory item and location. -3. Set Shopify to the final target quantity using set-style inventory mutations. - -This lane should not reuse transfer or fulfillment APIs because the business event is reconciliation, not movement. - -## Transfer-Specific Notes - -Live Shopify transfer testing proved the following behavior: +1. OMS computes the delta. +2. Store the delta in reset sync history. +3. Post only the delta to Shopify. -- Shopify cannot create `InventoryShipment` without `InventoryTransfer` -- `READY_TO_SHIP` reduces origin `available` and increases origin `reserved` -- marking shipment in transit reduces origin `on_hand` and increases destination `incoming` -- receiving increases destination `available` and decreases destination `incoming` +This lane does not hard reset Shopify. It remains delta-only. -That means the minimum Shopify flow needed to reproduce OMS transfer inventory is: +## Why This Model Is Lower Load -1. reserve using `InventoryTransfer` in ready state when OMS reservation must affect Shopify ATP -2. ship using `InventoryShipment` -3. receive using `inventoryShipmentReceive` - -## Implementation Notes - -- Prefer service ECAs or tx-commit hooks for outbound reservation and shipment events. -- Prefer `ShipmentReceipt`-driven feeds for inbound transfer receipt. -- Keep one idempotency key per business event, not per retry. -- If event-based posting fails, replay from the outbox. Do not fall back first to broad polling of `InventoryItemDetail`. -- Scheduled recovery can exist as a safety net, but it should read failed outbox events, not re-derive all business changes from scratch. +- SECAs fire only when the real OMS business service succeeds. +- History rows isolate Shopify sync from the transactional service path. +- The 15-minute batch is simple and bounded. +- Recovery is based on missing or unsent history, not on rescanning all inventory ledger rows. +- Each lane is independent, so TO receipt logic does not pollute store fulfillment or cycle count logic. ## Recommended Initial Cut -Implement these three triggers first: - -1. `process#OrderItemAllocation` for store-origin transfer reservation -2. `ship#TransferOrderShipment` for outbound transfer shipment -3. `ShipmentReceipt` create or update for inbound transfer receipt - -Then add: - -4. `ship#Shipment` for store fulfillment of online orders -5. `create#PhysicalInventory` for cycle count and manual variance -6. `reset#InventoryItem` or `create#ExternalInventoryReset` for authoritative resets - -This gives the shortest path to reducing daytime inventory drift on Shopify while keeping OMS as the only system that owns the business process. +Implement first: -## References +1. TO reservation +2. TO shipment +3. TO receipt +4. TO cancel or reject reversal +5. Store fulfillment shipment +6. SO cancel or reject reversal +7. Cycle count and manual variance -- `runtime/component/oms/service/co/hotwax/oms/impl/OrderReservationServices.xml` -- `runtime/component/oms/service/co/hotwax/orderledger/order/TransferOrderServices.xml` -- `runtime/component/poorti/service/co/hotwax/poorti/TransferOrderFulfillmentServices.xml` -- `runtime/component/poorti/service/co/hotwax/poorti/FulfillmentServices.xml` -- `runtime/component/poorti/service/co/hotwax/cycleCount/InventoryCountServices.xml` -- `project-ideas/fulfillment-center-mgmt/inventory-mgmt/reset_inventory_item_spec.md` -- `project-ideas/fulfillment-center-mgmt/inventory-mgmt/external_inventory_reset_design.md` -- Shopify Admin GraphQL: `inventoryTransferCreate`, `inventoryTransferMarkAsReadyToShip`, `inventoryShipmentCreate`, `inventoryShipmentMarkInTransit`, `inventoryShipmentReceive`, `inventoryAdjustQuantities`, `inventorySetQuantities`, `fulfillmentOrderMove`, `fulfillmentCreate` +Add external reset delta once the baseline match and store setting guard are in place. From baa0f9212c405b9824eb10a05557c0c4ee5e7621 Mon Sep 17 00:00:00 2001 From: Chinmay2107 Date: Tue, 14 Apr 2026 12:40:13 +0530 Subject: [PATCH 12/20] Update Shopify trigger matrix for cancellations --- ...ify-event-based-inventory-sync-triggers.md | 58 ++----------------- 1 file changed, 4 insertions(+), 54 deletions(-) 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 index 2fc0fbf5..ecc661c2 100644 --- a/project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md +++ b/project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md @@ -130,46 +130,10 @@ sequenceDiagram | External POS sale or non-Shopify sale reduces inventory | dedicated sales posting service boundary | `ShopifyInventoryAdjustmentSyncHistory` | `inventoryAdjustQuantities` | Use only when Shopify is not already the system that created the sale | | Cycle count or approved manual variance changes QOH and ATP | `co.hotwax.cycleCount.InventoryCountServices.create#PhysicalInventory` when variance is applied | `ShopifyInventoryAdjustmentSyncHistory` | `inventoryAdjustQuantities` | Manual variance follows the same lane | | External reset accepted by OMS | `reset#InventoryItem` or `create#ExternalInventoryReset` completion | `ShopifyExternalResetSyncHistory` | `inventoryAdjustQuantities` using computed delta only | Even this lane stays delta-based after OMS computes the difference | -| TO cancel before shipment or receipt | `co.hotwax.orderledger.order.TransferOrderServices.cancel#TransferOrder` post-service | `ShopifyToReservationReleaseSyncHistory` | Cancel the related Shopify transfer or remove remaining draft-ready quantities so origin `available` is restored | OMS cancels reservation before item status moves to `ITEM_CANCELLED` | -| TO reject before shipment | `co.hotwax.poorti.TransferOrderFulfillmentServices.reject#TransferOrder` post-service | `ShopifyToReservationReleaseSyncHistory` | Cancel the related Shopify transfer or remove remaining quantities from the sellable route | OMS cancels reservation and moves the item to `REJECTED_ITM_PARKING`; Shopify should restore sellable stock at the original mapped location | -| SO reject or SO cancel before shipment | `co.hotwax.oms.order.OrderServices.reject#OrderItem` and sales-order cancel service boundary | `ShopifySalesReservationReleaseSyncHistory` | Reverse the earlier reservation delta so facility `available` increases | This is required when OMS owned the reservation that reduced Shopify inventory | - -## Cancellation And Rejection Behavior - -### Transfer Order Cancel - -OMS transfer order cancel already reverts reservation before shipment starts. - -The cancel service: - -- blocks cancel if shipment is packed or shipped -- blocks cancel if any receipt already exists -- cancels reservation for each TO item -- moves item status to `ITEM_CANCELLED` - -So the Shopify side must reverse the reservation effect already posted earlier. That means restoring origin sellable inventory, not creating a new shipment flow. - -### Transfer Order Reject - -OMS transfer order reject already cancels reservation before shipment starts. - -The reject service: - -- blocks reject if fulfillment already started -- rejects all items together -- calls `reject#OrderItem` -- `reject#OrderItem` cancels reservation on the original facility -- then moves the item to `REJECTED_ITM_PARKING` - -For Shopify, the important impact is the release of origin sellable reservation. The rejected virtual facility does not need a Shopify mirror unless the business explicitly maps such a facility to Shopify, which is not the normal case. - -### Sales Order Reject Or Cancel - -OMS sales order reject and cancel also release reservation. - -The sales order cancel path calls `reject#OrderItem`, and `reject#OrderItem` cancels reservation on the original facility before reallocating to the reject facility. - -For Shopify, when OMS owned the reservation, the facility that lost ATP earlier must gain it back on reject or cancel. +| TO cancel before shipment or receipt | `co.hotwax.orderledger.order.TransferOrderServices.cancel#TransferOrder` post-service | `ShopifyToReservationReleaseSyncHistory` | Cancel the related Shopify transfer or remove remaining draft or ready quantities so origin `available` is restored | Use this only to reverse inventory already deducted on Shopify by the earlier reservation delta; OMS blocks TO cancel once shipment or receipt has started | +| TO reject before shipment | `co.hotwax.poorti.TransferOrderFulfillmentServices.reject#TransferOrder` post-service | `ShopifyToReservationReleaseSyncHistory` | Cancel the related Shopify transfer or remove remaining quantities from the sellable route | Use this only to reverse inventory already deducted on Shopify by the earlier reservation delta; OMS reject also cancels reservation and moves the item to `REJECTED_ITM_PARKING` | +| SO cancel before shipment | `co.hotwax.orderledger.order.OrderServices.cancel#SalesOrderItem` post-service | `ShopifySalesReservationReleaseSyncHistory` | Reverse the earlier reservation delta so facility `available` increases | Use this when OMS owned the reservation that had already reduced Shopify ATP | +| SO reject before shipment | `co.hotwax.oms.order.OrderServices.reject#OrderItem` post-service | `ShopifySalesReservationReleaseSyncHistory` | Reverse the earlier reservation delta so facility `available` increases | Use this when OMS owned the reservation that had already reduced Shopify ATP | ## Shopify Workflow By Lane @@ -229,17 +193,3 @@ This lane does not hard reset Shopify. It remains delta-only. - The 15-minute batch is simple and bounded. - Recovery is based on missing or unsent history, not on rescanning all inventory ledger rows. - Each lane is independent, so TO receipt logic does not pollute store fulfillment or cycle count logic. - -## Recommended Initial Cut - -Implement first: - -1. TO reservation -2. TO shipment -3. TO receipt -4. TO cancel or reject reversal -5. Store fulfillment shipment -6. SO cancel or reject reversal -7. Cycle count and manual variance - -Add external reset delta once the baseline match and store setting guard are in place. From 0fff203a1bfb3448ab009e3181ccabf2b31febe7 Mon Sep 17 00:00:00 2001 From: Chinmay2107 Date: Tue, 14 Apr 2026 14:25:30 +0530 Subject: [PATCH 13/20] Simplify Shopify sync history behavior --- ...ify-event-based-inventory-sync-triggers.md | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) 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 index ecc661c2..ca6bef07 100644 --- a/project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md +++ b/project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md @@ -26,10 +26,10 @@ The model has four lanes: The design should work like this: 1. A service SECA runs after the OMS business service completes. -2. The SECA creates one row in a source-specific Shopify sync history entity. +2. The SECA does not create history. It only marks the source event as eligible for sync by virtue of the source record existing. 3. A batch service runs every 15 minutes. -4. The batch finds history rows not yet sent to Shopify and posts the required Shopify delta workflow. -5. The batch also finds source records for which history was never created and backfills the missing history rows before sending them. +4. The batch finds source records for which no success history exists and posts the required Shopify delta workflow. +5. A history row is written only after the Shopify call succeeds. This gives two layers of safety: @@ -44,8 +44,8 @@ This design does not use `SystemMessage`. - 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. -- Each trigger source gets its own sync history entity. -- Batch posting reads from sync history first, not from raw ledger noise. +- Each trigger source gets its own sync history entity, but history is success-only. +- If no history exists for a source record, it is treated as not yet synced. - Transfer lifecycle is not mirrored in Shopify for business control. Transfer APIs are used only when Shopify requires them for inventory movement. ## Sync Architecture @@ -79,10 +79,10 @@ Each history row should store at least: - shopId when resolved - event type - delta quantity payload or enough data to derive it -- status such as `PENDING`, `SENT`, `FAILED`, `SKIPPED` -- retry count -- last error text -- created date and sent date +- Shopify reference created by the successful call when relevant +- sent date + +No history row should be written for failed or skipped attempts. Absence of history means the source record is still unsynced. ## Processing Flow @@ -90,14 +90,13 @@ Each history row should store at least: flowchart TD A[OMS business service completes] --> B[SECA checks ProductStoreSetting] B -->|sync disabled| C[Skip] - B -->|sync enabled| D[Create source-specific sync history row] + B -->|sync enabled| D[Leave source record available for batch pickup] D --> E[15 minute batch] - E --> F[Backfill missing history rows] - F --> G[Read pending history rows] - G --> H[Build Shopify delta workflow] - H --> I[Post to Shopify] - I -->|success| J[Mark history SENT] - I -->|failure| K[Mark history FAILED for retry] + E --> F[Find source records with no success history] + F --> G[Build Shopify delta workflow] + G --> H[Post to Shopify] + H -->|success| I[Create success history row] + H -->|failure| J[No history written] ``` ## Sequence View @@ -112,11 +111,11 @@ sequenceDiagram OMS->>SECA: business transaction committed SECA->>SECA: check ProductStoreSetting - SECA->>HIST: create pending history row - BATCH->>HIST: find missing or pending rows + SECA-->>BATCH: source record remains eligible + BATCH->>HIST: find source records with no success history BATCH->>SHOP: post delta workflow SHOP-->>BATCH: success or error - BATCH->>HIST: update SENT or FAILED + BATCH->>HIST: create row only on success ``` ## Trigger Matrix @@ -191,5 +190,5 @@ This lane does not hard reset Shopify. It remains delta-only. - SECAs fire only when the real OMS business service succeeds. - History rows isolate Shopify sync from the transactional service path. - The 15-minute batch is simple and bounded. -- Recovery is based on missing or unsent history, not on rescanning all inventory ledger rows. +- Recovery is based on missing success history, not on rescanning all inventory ledger rows. - Each lane is independent, so TO receipt logic does not pollute store fulfillment or cycle count logic. From 86d3bba8f0204250e14562b3f2d21b9d5a29a795 Mon Sep 17 00:00:00 2001 From: Chinmay2107 Date: Tue, 14 Apr 2026 17:02:44 +0530 Subject: [PATCH 14/20] Clarify SECA as primary Shopify sync path --- ...ify-event-based-inventory-sync-triggers.md | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) 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 index ca6bef07..ad125eeb 100644 --- a/project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md +++ b/project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md @@ -26,14 +26,14 @@ The model has four lanes: The design should work like this: 1. A service SECA runs after the OMS business service completes. -2. The SECA does not create history. It only marks the source event as eligible for sync by virtue of the source record existing. +2. The SECA immediately tries to post the Shopify delta workflow. 3. A batch service runs every 15 minutes. -4. The batch finds source records for which no success history exists and posts the required Shopify delta workflow. -5. A history row is written only after the Shopify call succeeds. +4. The batch finds source records for which no success history exists and retries the required Shopify delta workflow. +5. A history row is written only after a successful Shopify call, whether the call came from the SECA path or the batch path. This gives two layers of safety: -- primary event capture through service SECAs +- primary immediate sync through service SECAs - recovery through the 15-minute batch This design does not use `SystemMessage`. @@ -45,7 +45,7 @@ This design does not use `SystemMessage`. - Only deltas are posted to Shopify. - No daytime hard reset should be used for these flows. - Each trigger source gets its own sync history entity, but history is success-only. -- If no history exists for a source record, it is treated as not yet synced. +- If no history exists for a source record, it is treated as not yet synced and eligible for batch recovery. - Transfer lifecycle is not mirrored in Shopify for business control. Transfer APIs are used only when Shopify requires them for inventory movement. ## Sync Architecture @@ -90,13 +90,14 @@ No history row should be written for failed or skipped attempts. Absence of hist flowchart TD A[OMS business service completes] --> B[SECA checks ProductStoreSetting] B -->|sync disabled| C[Skip] - B -->|sync enabled| D[Leave source record available for batch pickup] - D --> E[15 minute batch] - E --> F[Find source records with no success history] - F --> G[Build Shopify delta workflow] - G --> H[Post to Shopify] - H -->|success| I[Create success history row] - H -->|failure| J[No history written] + B -->|sync enabled| D[SECA posts Shopify delta] + D -->|success| E[Create success history row] + D -->|failure| F[No history written] + F --> G[15 minute batch] + G --> H[Find source records with no success history] + H --> I[Retry Shopify delta] + I -->|success| J[Create success history row] + I -->|failure| K[Leave without history for next retry] ``` ## Sequence View @@ -111,9 +112,11 @@ sequenceDiagram OMS->>SECA: business transaction committed SECA->>SECA: check ProductStoreSetting - SECA-->>BATCH: source record remains eligible + SECA->>SHOP: post delta workflow + SHOP-->>SECA: success or error + SECA->>HIST: create row only on success BATCH->>HIST: find source records with no success history - BATCH->>SHOP: post delta workflow + BATCH->>SHOP: retry delta workflow SHOP-->>BATCH: success or error BATCH->>HIST: create row only on success ``` @@ -187,7 +190,7 @@ This lane does not hard reset Shopify. It remains delta-only. ## Why This Model Is Lower Load -- SECAs fire only when the real OMS business service succeeds. +- SECAs fire only when the real OMS business service succeeds and handle the common case immediately. - History rows isolate Shopify sync from the transactional service path. - The 15-minute batch is simple and bounded. - Recovery is based on missing success history, not on rescanning all inventory ledger rows. From 1592c195a3b0a37d956a2e90cd8fd22e99cb06ae Mon Sep 17 00:00:00 2001 From: Chinmay2107 Date: Tue, 14 Apr 2026 23:04:49 +0530 Subject: [PATCH 15/20] Simplify Shopify inventory sync implementation docs --- ...ent-based-inventory-sync-implementation.md | 289 ++++++++++++++++++ ...ify-event-based-inventory-sync-triggers.md | 152 +++------ 2 files changed, 336 insertions(+), 105 deletions(-) create mode 100644 project-ideas/shopify-integration/shopify-event-based-inventory-sync-implementation.md 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..eb2f6225 --- /dev/null +++ b/project-ideas/shopify-integration/shopify-event-based-inventory-sync-implementation.md @@ -0,0 +1,289 @@ +# Shopify Event-Based Inventory Sync Implementation + +## Purpose + +This document describes a 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. + +## Scope + +This implementation covers these lanes: + +1. Transfer reservation from store +2. Transfer shipment +3. Transfer receipt +4. Transfer cancel or reject reversal +5. Store fulfillment shipment +6. Sales reservation release on cancel or reject +7. Inventory adjustment for cycle count, manual variance, external POS sale, and external reset delta + +## 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 SyncRouterService { + +resolve shop context + +validate facility mapping + +validate product mapping + +route to lane helper + } + class TransferSyncService { + +sync reservation + +sync shipment + +sync receipt + +sync release + } + class FulfillmentSyncService { + +sync store fulfillment + } + class AdjustmentSyncService { + +sync inventory adjustment + } + class ShopifyHelperService { + +resolve shop + +resolve locations + +resolve inventory item ids + +execute graphql + } + + SecaTrigger --> SyncRouterService + SyncRouterService --> TransferSyncService + SyncRouterService --> FulfillmentSyncService + SyncRouterService --> AdjustmentSyncService + TransferSyncService --> ShopifyHelperService + FulfillmentSyncService --> ShopifyHelperService + AdjustmentSyncService --> ShopifyHelperService +``` + +## Service Roles + +### 1. Setting Guard Service + +Suggested role: + +- `is#ShopifyInventorySyncEnabled` + +Responsibility: + +- read `ProductStoreSetting` +- return true only when the store is explicitly enabled for Shopify inventory sync +- stop all downstream sync when disabled + +This service should be called first by every `SECA` path. + +### 2. Sync Router Service + +Suggested role: + +- `route#ShopifyInventorySync` + +Responsibility: + +- receive the business key from `SECA` +- determine the event lane +- resolve `productStoreId` +- call the setting guard +- route to the correct lane-specific sync service + +This keeps the `SECA` thin and reusable. + +### 3. Shopify Context Resolver + +Suggested role: + +- `resolve#ShopifyInventorySyncContext` + +Responsibility: + +- resolve `shopId` +- resolve Shopify Admin config +- resolve OMS facility to Shopify location +- resolve OMS product to Shopify inventory item +- fail fast if required mapping is missing + +This service should be shared by all lanes. + +### 4. Transfer Sync Services + +Suggested roles: + +- `sync#TransferReservationToShopify` +- `sync#TransferShipmentToShopify` +- `sync#TransferReceiptToShopify` +- `sync#TransferReservationReleaseToShopify` + +Responsibilities: + +`sync#TransferReservationToShopify` +- build one transfer payload from OMS reserved quantities +- create or update the minimum Shopify `InventoryTransfer` +- move the transfer to `READY_TO_SHIP` + +`sync#TransferShipmentToShopify` +- find the shipped OMS transfer shipment +- ensure the prerequisite Shopify transfer exists +- create `InventoryShipment` +- set tracking when available +- mark shipment in transit + +`sync#TransferReceiptToShopify` +- group receipt quantities by `shipmentId + datetimeReceived + facilityId` +- map to the correct Shopify shipment items +- call `inventoryShipmentReceive` + +`sync#TransferReservationReleaseToShopify` +- handle TO cancel and TO reject +- reverse the sellable reservation effect already posted to Shopify +- prefer cancelling or shrinking the transfer instead of using a generic adjustment when the transfer is still the correct carrier of the event + +### 5. Store Fulfillment Sync Service + +Suggested role: + +- `sync#StoreFulfillmentToShopify` + +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. + +### 6. Inventory Adjustment Sync Service + +Suggested role: + +- `sync#InventoryAdjustmentToShopify` + +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 +- reservation release compensation when transfer cancellation is not the right representation +- external reset delta after OMS computes the difference + +### 7. Logging Service + +Suggested role: + +- `log#ShopifyInventorySyncFailure` + +Responsibility: + +- write one structured application log entry +- keep logging format stable so support can search by business key + +Suggested log fields: + +- event type +- source service +- orderId +- orderItemSeqId +- shipmentId +- receiptId +- physicalInventoryId +- productStoreId +- facilityId +- shopId +- Shopify location id +- payload summary +- Shopify error text + +## SECA Responsibilities + +The `SECA` should do only three things: + +1. identify the source business key +2. call the router 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 | Routed sync service | +| --- | --- | --- | +| `co.hotwax.oms.impl.OrderReservationServices.process#OrderItemAllocation` | `tx-commit` | `sync#TransferReservationToShopify` when the order is a store-origin TO | +| `co.hotwax.poorti.TransferOrderFulfillmentServices.ship#TransferOrderShipment` | `post-service` | `sync#TransferShipmentToShopify` | +| `ShipmentReceipt` create or update support service | `post-service` or entity hook wrapper | `sync#TransferReceiptToShopify` | +| `co.hotwax.orderledger.order.TransferOrderServices.cancel#TransferOrder` | `post-service` | `sync#TransferReservationReleaseToShopify` | +| `co.hotwax.poorti.TransferOrderFulfillmentServices.reject#TransferOrder` | `post-service` | `sync#TransferReservationReleaseToShopify` | +| `co.hotwax.poorti.FulfillmentServices.ship#Shipment` | `post-service` | `sync#StoreFulfillmentToShopify` | +| `co.hotwax.orderledger.order.OrderServices.cancel#SalesOrderItem` | `post-service` | `sync#SalesReservationReleaseToShopify` or `sync#InventoryAdjustmentToShopify` | +| `co.hotwax.oms.order.OrderServices.reject#OrderItem` | `post-service` | `sync#SalesReservationReleaseToShopify` or `sync#InventoryAdjustmentToShopify` | +| `co.hotwax.cycleCount.InventoryCountServices.create#PhysicalInventory` | `post-service` | `sync#InventoryAdjustmentToShopify` | +| `reset#InventoryItem` or `create#ExternalInventoryReset` | `post-service` | `sync#InventoryAdjustmentToShopify` | + +## 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 + +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: store-origin TO shipment + +1. OMS ships the transfer shipment. +2. `SECA` fires after `ship#TransferOrderShipment`. +3. Router checks `SHOPIFY_INV_SYNC` for the product store. +4. Transfer shipment sync service resolves Shopify context. +5. Service ensures the transfer exists. +6. Service creates Shopify shipment. +7. Service marks shipment in transit. +8. On failure, log the error with `orderId`, `shipmentId`, `facilityId`, and `shopId`. + +## Implementation Notes + +- keep all Shopify GraphQL calls in helper services, not inside `SECA` +- 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 + +## 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 index ad125eeb..950decdd 100644 --- a/project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md +++ b/project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md @@ -2,41 +2,28 @@ ## Purpose -This document defines a simple event-driven design for keeping Shopify inventory aligned with OMS. +This document defines the trigger points for a simple OMS to Shopify inventory sync. -OMS remains the system of record. Shopify is updated only to reflect inventory impact at mapped Shopify locations. The design is delta-based only. It does not use hard inventory reset during the day. +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. ## 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, no trigger should create history and no batch should post inventory to Shopify. +- If the product store setting is off, the `SECA` must not attempt Shopify inventory sync. Without a matched starting baseline, a delta-only design will drift instead of converge. ## Design Summary -The model has four lanes: +Phase 1 is intentionally simple: -1. Store fulfillment events -2. Transfer shipment events -3. Inventory adjustment events -4. External reset events +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. -The design should work like this: - -1. A service SECA runs after the OMS business service completes. -2. The SECA immediately tries to post the Shopify delta workflow. -3. A batch service runs every 15 minutes. -4. The batch finds source records for which no success history exists and retries the required Shopify delta workflow. -5. A history row is written only after a successful Shopify call, whether the call came from the SECA path or the batch path. - -This gives two layers of safety: - -- primary immediate sync through service SECAs -- recovery through the 15-minute batch - -This design does not use `SystemMessage`. +This design does not use `SystemMessage`. It also does not introduce sync history entities in phase 1. ## Core Principles @@ -44,60 +31,19 @@ This design does not use `SystemMessage`. - 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. -- Each trigger source gets its own sync history entity, but history is success-only. -- If no history exists for a source record, it is treated as not yet synced and eligible for batch recovery. +- `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. -## Sync Architecture - -### Trigger Model - -Use service SECAs on the OMS services that already represent the committed business boundary. - -Do not use a broad data feed on `InventoryItemDetail` or `Shipment`. Those are too generic and will create unnecessary load and ambiguity. - -### History Model - -Each trigger source should have its own history entity. Example split: - -| Source event | History entity | -| --- | --- | -| TO reservation at store | `ShopifyToReservationSyncHistory` | -| TO outbound shipment | `ShopifyTransferShipmentSyncHistory` | -| TO inbound receipt | `ShopifyTransferReceiptSyncHistory` | -| Store fulfillment shipment | `ShopifyStoreFulfillmentSyncHistory` | -| Cycle count or manual variance | `ShopifyInventoryAdjustmentSyncHistory` | -| External reset delta | `ShopifyExternalResetSyncHistory` | -| SO cancel or reject reservation release | `ShopifySalesReservationReleaseSyncHistory` | -| TO cancel or reject reservation release | `ShopifyToReservationReleaseSyncHistory` | - -Each history row should store at least: - -- source business key -- productStoreId -- facilityId -- shopId when resolved -- event type -- delta quantity payload or enough data to derive it -- Shopify reference created by the successful call when relevant -- sent date - -No history row should be written for failed or skipped attempts. Absence of history means the source record is still unsynced. - ## Processing Flow ```mermaid flowchart TD A[OMS business service completes] --> B[SECA checks ProductStoreSetting] B -->|sync disabled| C[Skip] - B -->|sync enabled| D[SECA posts Shopify delta] - D -->|success| E[Create success history row] - D -->|failure| F[No history written] - F --> G[15 minute batch] - G --> H[Find source records with no success history] - H --> I[Retry Shopify delta] - I -->|success| J[Create success history row] - I -->|failure| K[Leave without history for next retry] + B -->|sync enabled| D[Call Shopify sync service] + D -->|success| E[Finish] + D -->|failure| F[Write structured failure log] ``` ## Sequence View @@ -106,42 +52,39 @@ flowchart TD sequenceDiagram participant OMS as OMS Service participant SECA as Service SECA - participant HIST as Sync History Entity - participant BATCH as 15 Min Batch + participant SYNC as Shopify Sync Service participant SHOP as Shopify OMS->>SECA: business transaction committed SECA->>SECA: check ProductStoreSetting - SECA->>SHOP: post delta workflow - SHOP-->>SECA: success or error - SECA->>HIST: create row only on success - BATCH->>HIST: find source records with no success history - BATCH->>SHOP: retry delta workflow - SHOP-->>BATCH: success or error - BATCH->>HIST: create row only on success + 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 history entity | Shopify workflow | Notes | +| OMS event | SECA trigger boundary | Sync service | Shopify workflow | Notes | | --- | --- | --- | --- | --- | -| Store-origin TO reservation reduces ATP | `co.hotwax.oms.impl.OrderReservationServices.process#OrderItemAllocation` after commit, only for transfer orders originating from stores | `ShopifyToReservationSyncHistory` | Create or update a minimal Shopify `InventoryTransfer` and move it to `READY_TO_SHIP` so origin `available` decreases and `reserved` increases | This is needed only because Shopify cannot create `InventoryShipment` without `InventoryTransfer` | -| Store-origin TO outbound shipment reduces QOH | `co.hotwax.poorti.TransferOrderFulfillmentServices.ship#TransferOrderShipment` post-service | `ShopifyTransferShipmentSyncHistory` | 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` | `ShopifyTransferReceiptSyncHistory` | `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 | `ShopifyStoreFulfillmentSyncHistory` | 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 service boundary | `ShopifyInventoryAdjustmentSyncHistory` | `inventoryAdjustQuantities` | Use only when Shopify is not already the system that created the sale | -| Cycle count or approved manual variance changes QOH and ATP | `co.hotwax.cycleCount.InventoryCountServices.create#PhysicalInventory` when variance is applied | `ShopifyInventoryAdjustmentSyncHistory` | `inventoryAdjustQuantities` | Manual variance follows the same lane | -| External reset accepted by OMS | `reset#InventoryItem` or `create#ExternalInventoryReset` completion | `ShopifyExternalResetSyncHistory` | `inventoryAdjustQuantities` using computed delta only | Even this lane stays delta-based after OMS computes the difference | -| TO cancel before shipment or receipt | `co.hotwax.orderledger.order.TransferOrderServices.cancel#TransferOrder` post-service | `ShopifyToReservationReleaseSyncHistory` | Cancel the related Shopify transfer or remove remaining draft or ready quantities so origin `available` is restored | Use this only to reverse inventory already deducted on Shopify by the earlier reservation delta; OMS blocks TO cancel once shipment or receipt has started | -| TO reject before shipment | `co.hotwax.poorti.TransferOrderFulfillmentServices.reject#TransferOrder` post-service | `ShopifyToReservationReleaseSyncHistory` | Cancel the related Shopify transfer or remove remaining quantities from the sellable route | Use this only to reverse inventory already deducted on Shopify by the earlier reservation delta; OMS reject also cancels reservation and moves the item to `REJECTED_ITM_PARKING` | -| SO cancel before shipment | `co.hotwax.orderledger.order.OrderServices.cancel#SalesOrderItem` post-service | `ShopifySalesReservationReleaseSyncHistory` | Reverse the earlier reservation delta so facility `available` increases | Use this when OMS owned the reservation that had already reduced Shopify ATP | -| SO reject before shipment | `co.hotwax.oms.order.OrderServices.reject#OrderItem` post-service | `ShopifySalesReservationReleaseSyncHistory` | Reverse the earlier reservation delta so facility `available` increases | Use this when OMS owned the reservation that had already reduced Shopify ATP | +| Store-origin TO reservation reduces ATP | `co.hotwax.oms.impl.OrderReservationServices.process#OrderItemAllocation` after commit, only for transfer orders originating from stores | `sync#TransferReservationToShopify` | Create or update a minimal Shopify `InventoryTransfer` and move it to `READY_TO_SHIP` so origin `available` decreases and `reserved` increases | This exists only because Shopify cannot create `InventoryShipment` without `InventoryTransfer` | +| 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 service boundary | `sync#InventoryAdjustmentToShopify` | `inventoryAdjustQuantities` | Use only when Shopify is not already the system that created the sale | +| 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 | +| External reset accepted by OMS | `reset#InventoryItem` or `create#ExternalInventoryReset` completion | `sync#InventoryAdjustmentToShopify` | `inventoryAdjustQuantities` using computed delta only | Even this lane stays delta-based after OMS computes the difference | +| TO cancel before shipment or receipt | `co.hotwax.orderledger.order.TransferOrderServices.cancel#TransferOrder` post-service | `sync#TransferReservationReleaseToShopify` | Cancel the related Shopify transfer or remove remaining draft or ready quantities so origin `available` is restored | Use this only to reverse inventory already deducted on Shopify by the earlier reservation delta; OMS blocks TO cancel once shipment or receipt has started | +| TO reject before shipment | `co.hotwax.poorti.TransferOrderFulfillmentServices.reject#TransferOrder` post-service | `sync#TransferReservationReleaseToShopify` | Cancel the related Shopify transfer or remove remaining quantities from the sellable route | Use this only to reverse inventory already deducted on Shopify by the earlier reservation delta | +| SO cancel before shipment | `co.hotwax.orderledger.order.OrderServices.cancel#SalesOrderItem` post-service | `sync#SalesReservationReleaseToShopify` | Reverse the earlier reservation delta so facility `available` increases | Use this when OMS owned the reservation that had already reduced Shopify ATP | +| SO reject before shipment | `co.hotwax.oms.order.OrderServices.reject#OrderItem` post-service | `sync#SalesReservationReleaseToShopify` | Reverse the earlier reservation delta so facility `available` increases | Use this when OMS owned the reservation that had already reduced Shopify ATP | ## Shopify Workflow By Lane ### 1. Store Fulfillment Lane -Use this for online orders fulfilled from a retail store. +Use this for online orders fulfilled from stores. Workflow: @@ -169,6 +112,7 @@ Use this for: - manual variance - external POS sale when Shopify did not create the sale - reservation release adjustments when a compensation delta is simpler than a transfer cancellation +- external reset delta after OMS computes the difference Workflow: @@ -176,22 +120,20 @@ Workflow: 2. Build delta quantity change. 3. Post `inventoryAdjustQuantities`. -### 4. External Reset Lane +## Logging And Missed Events -Use this when an external system sends a new truth to OMS and OMS computes a difference. +Phase 1 does not create retry entities or sync history entities. -Workflow: +If a `SECA` call fails or a trigger path is missed, log enough information to support replay: -1. OMS computes the delta. -2. Store the delta in reset sync history. -3. Post only the delta to Shopify. - -This lane does not hard reset Shopify. It remains delta-only. - -## Why This Model Is Lower Load +- event type +- source service name +- orderId, shipmentId, receiptId, or physicalInventoryId +- productStoreId +- facilityId +- resolved shopId +- resolved Shopify location id +- payload summary +- Shopify error text -- SECAs fire only when the real OMS business service succeeds and handle the common case immediately. -- History rows isolate Shopify sync from the transactional service path. -- The 15-minute batch is simple and bounded. -- Recovery is based on missing success history, not on rescanning all inventory ledger rows. -- Each lane is independent, so TO receipt logic does not pollute store fulfillment or cycle count logic. +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. From bb968f3f637c1b7ad84ae821a386a44b30a9f814 Mon Sep 17 00:00:00 2001 From: Chinmay2107 Date: Wed, 15 Apr 2026 12:44:05 +0530 Subject: [PATCH 16/20] Limit Shopify inventory sync to store movements --- ...ent-based-inventory-sync-implementation.md | 45 ++++++------------- ...ify-event-based-inventory-sync-triggers.md | 23 +++++----- 2 files changed, 25 insertions(+), 43 deletions(-) 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 index eb2f6225..d29ee784 100644 --- a/project-ideas/shopify-integration/shopify-event-based-inventory-sync-implementation.md +++ b/project-ideas/shopify-integration/shopify-event-based-inventory-sync-implementation.md @@ -6,17 +6,18 @@ This document describes a phase 1 implementation for Shopify inventory sync with 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 reservation from store -2. Transfer shipment -3. Transfer receipt -4. Transfer cancel or reject reversal -5. Store fulfillment shipment -6. Sales reservation release on cancel or reject -7. Inventory adjustment for cycle count, manual variance, external POS sale, and external reset delta +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 + +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. ## What Phase 1 Does Not Add @@ -44,10 +45,8 @@ classDiagram +route to lane helper } class TransferSyncService { - +sync reservation +sync shipment +sync receipt - +sync release } class FulfillmentSyncService { +sync store fulfillment @@ -113,7 +112,7 @@ Responsibility: - resolve `shopId` - resolve Shopify Admin config -- resolve OMS facility to Shopify location +- resolve OMS POS/store facility to Shopify location - resolve OMS product to Shopify inventory item - fail fast if required mapping is missing @@ -123,21 +122,14 @@ This service should be shared by all lanes. Suggested roles: -- `sync#TransferReservationToShopify` - `sync#TransferShipmentToShopify` - `sync#TransferReceiptToShopify` -- `sync#TransferReservationReleaseToShopify` Responsibilities: -`sync#TransferReservationToShopify` -- build one transfer payload from OMS reserved quantities -- create or update the minimum Shopify `InventoryTransfer` -- move the transfer to `READY_TO_SHIP` - `sync#TransferShipmentToShopify` - find the shipped OMS transfer shipment -- ensure the prerequisite Shopify transfer exists +- create the prerequisite Shopify transfer if needed - create `InventoryShipment` - set tracking when available - mark shipment in transit @@ -147,11 +139,6 @@ Responsibilities: - map to the correct Shopify shipment items - call `inventoryShipmentReceive` -`sync#TransferReservationReleaseToShopify` -- handle TO cancel and TO reject -- reverse the sellable reservation effect already posted to Shopify -- prefer cancelling or shrinking the transfer instead of using a generic adjustment when the transfer is still the correct carrier of the event - ### 5. Store Fulfillment Sync Service Suggested role: @@ -183,8 +170,7 @@ This service should be reused for: - cycle count - manual variance - external POS sale where Shopify did not create the sale -- reservation release compensation when transfer cancellation is not the right representation -- external reset delta after OMS computes the difference +- `_NA_` accumulated inventory reset delta after OMS computes the difference ### 7. Logging Service @@ -231,16 +217,11 @@ The `SECA` should not: | OMS service | SECA timing | Routed sync service | | --- | --- | --- | -| `co.hotwax.oms.impl.OrderReservationServices.process#OrderItemAllocation` | `tx-commit` | `sync#TransferReservationToShopify` when the order is a store-origin TO | | `co.hotwax.poorti.TransferOrderFulfillmentServices.ship#TransferOrderShipment` | `post-service` | `sync#TransferShipmentToShopify` | | `ShipmentReceipt` create or update support service | `post-service` or entity hook wrapper | `sync#TransferReceiptToShopify` | -| `co.hotwax.orderledger.order.TransferOrderServices.cancel#TransferOrder` | `post-service` | `sync#TransferReservationReleaseToShopify` | -| `co.hotwax.poorti.TransferOrderFulfillmentServices.reject#TransferOrder` | `post-service` | `sync#TransferReservationReleaseToShopify` | | `co.hotwax.poorti.FulfillmentServices.ship#Shipment` | `post-service` | `sync#StoreFulfillmentToShopify` | -| `co.hotwax.orderledger.order.OrderServices.cancel#SalesOrderItem` | `post-service` | `sync#SalesReservationReleaseToShopify` or `sync#InventoryAdjustmentToShopify` | -| `co.hotwax.oms.order.OrderServices.reject#OrderItem` | `post-service` | `sync#SalesReservationReleaseToShopify` or `sync#InventoryAdjustmentToShopify` | | `co.hotwax.cycleCount.InventoryCountServices.create#PhysicalInventory` | `post-service` | `sync#InventoryAdjustmentToShopify` | -| `reset#InventoryItem` or `create#ExternalInventoryReset` | `post-service` | `sync#InventoryAdjustmentToShopify` | +| `reset#InventoryItem` or `create#ExternalInventoryReset` for `_NA_` facility | `post-service` | `sync#InventoryAdjustmentToShopify` | ## Failure Handling @@ -281,6 +262,8 @@ Example: store-origin TO shipment - 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 ## Operational Note 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 index 950decdd..7913cd5b 100644 --- a/project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md +++ b/project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md @@ -6,11 +6,14 @@ This document defines the trigger points for a simple OMS to Shopify inventory s 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. @@ -34,6 +37,7 @@ This design does not use `SystemMessage`. It also does not introduce sync histor - `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 @@ -68,17 +72,12 @@ sequenceDiagram | OMS event | SECA trigger boundary | Sync service | Shopify workflow | Notes | | --- | --- | --- | --- | --- | -| Store-origin TO reservation reduces ATP | `co.hotwax.oms.impl.OrderReservationServices.process#OrderItemAllocation` after commit, only for transfer orders originating from stores | `sync#TransferReservationToShopify` | Create or update a minimal Shopify `InventoryTransfer` and move it to `READY_TO_SHIP` so origin `available` decreases and `reserved` increases | This exists only because Shopify cannot create `InventoryShipment` without `InventoryTransfer` | | 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 service boundary | `sync#InventoryAdjustmentToShopify` | `inventoryAdjustQuantities` | Use only when Shopify is not already the system that created the sale | +| 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 | -| External reset accepted by OMS | `reset#InventoryItem` or `create#ExternalInventoryReset` completion | `sync#InventoryAdjustmentToShopify` | `inventoryAdjustQuantities` using computed delta only | Even this lane stays delta-based after OMS computes the difference | -| TO cancel before shipment or receipt | `co.hotwax.orderledger.order.TransferOrderServices.cancel#TransferOrder` post-service | `sync#TransferReservationReleaseToShopify` | Cancel the related Shopify transfer or remove remaining draft or ready quantities so origin `available` is restored | Use this only to reverse inventory already deducted on Shopify by the earlier reservation delta; OMS blocks TO cancel once shipment or receipt has started | -| TO reject before shipment | `co.hotwax.poorti.TransferOrderFulfillmentServices.reject#TransferOrder` post-service | `sync#TransferReservationReleaseToShopify` | Cancel the related Shopify transfer or remove remaining quantities from the sellable route | Use this only to reverse inventory already deducted on Shopify by the earlier reservation delta | -| SO cancel before shipment | `co.hotwax.orderledger.order.OrderServices.cancel#SalesOrderItem` post-service | `sync#SalesReservationReleaseToShopify` | Reverse the earlier reservation delta so facility `available` increases | Use this when OMS owned the reservation that had already reduced Shopify ATP | -| SO reject before shipment | `co.hotwax.oms.order.OrderServices.reject#OrderItem` post-service | `sync#SalesReservationReleaseToShopify` | Reverse the earlier reservation delta so facility `available` increases | Use this when OMS owned the reservation that had already reduced Shopify ATP | +| `_NA_` facility reset for accumulated inventory | `reset#InventoryItem` or `create#ExternalInventoryReset` completion for `_NA_` facility | `sync#InventoryAdjustmentToShopify` | `inventoryAdjustQuantities` using computed delta only | This is for accumulated inventory pushed through `_NA_`; store-level POS inventory should still be event-driven by shipment, receipt, and correction events | ## Shopify Workflow By Lane @@ -99,10 +98,11 @@ Use this for store to warehouse, warehouse to store, and store to store transfer Workflow: -1. On OMS reservation, create the minimum `InventoryTransfer` needed for Shopify inventory reservation. -2. On OMS ship, create `InventoryShipment` and mark it in transit. +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`. -4. On OMS cancel or reject before ship, cancel or shrink the Shopify transfer so reserved stock is released. + +No reservation sync is included in phase 1. ### 3. Inventory Adjustment Lane @@ -111,8 +111,7 @@ Use this for: - cycle count - manual variance - external POS sale when Shopify did not create the sale -- reservation release adjustments when a compensation delta is simpler than a transfer cancellation -- external reset delta after OMS computes the difference +- external reset delta for `_NA_` accumulated inventory after OMS computes the difference Workflow: From e9f70d881cebe77ff6f61089b1ab14c89c1beae2 Mon Sep 17 00:00:00 2001 From: Chinmay2107 Date: Wed, 15 Apr 2026 12:47:51 +0530 Subject: [PATCH 17/20] Use ExternalInventoryReset as Shopify reset source --- ...ent-based-inventory-sync-implementation.md | 23 +++++++++++++++---- ...ify-event-based-inventory-sync-triggers.md | 4 ++-- 2 files changed, 21 insertions(+), 6 deletions(-) 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 index d29ee784..7d718618 100644 --- a/project-ideas/shopify-integration/shopify-event-based-inventory-sync-implementation.md +++ b/project-ideas/shopify-integration/shopify-event-based-inventory-sync-implementation.md @@ -15,7 +15,7 @@ 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 +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. @@ -170,9 +170,24 @@ 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 after OMS computes the difference +- `_NA_` accumulated inventory reset delta from the created `ExternalInventoryReset` record -### 7. Logging Service +### 7. External Reset Sync Service + +Suggested role: + +- `sync#ExternalInventoryResetToShopify` + +Responsibility: + +- read the created `ExternalInventoryReset` record by `resetItemId` +- process only `_NA_` facility reset records for accumulated inventory +- use `quantityOnHandDiff` and `availableToPromiseDiff` from the reset record as the Shopify delta source +- call `inventoryAdjustQuantities` + +`reset#InventoryItem` should not be the sync source. It computes diffs and calls `create#ExternalInventoryReset`; the durable reset row is the event that should drive Shopify posting. + +### 8. Logging Service Suggested role: @@ -221,7 +236,7 @@ The `SECA` should not: | `ShipmentReceipt` create or update support service | `post-service` or entity hook wrapper | `sync#TransferReceiptToShopify` | | `co.hotwax.poorti.FulfillmentServices.ship#Shipment` | `post-service` | `sync#StoreFulfillmentToShopify` | | `co.hotwax.cycleCount.InventoryCountServices.create#PhysicalInventory` | `post-service` | `sync#InventoryAdjustmentToShopify` | -| `reset#InventoryItem` or `create#ExternalInventoryReset` for `_NA_` facility | `post-service` | `sync#InventoryAdjustmentToShopify` | +| `create#ExternalInventoryReset` for `_NA_` facility | `post-service` | `sync#ExternalInventoryResetToShopify` | ## Failure Handling 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 index 7913cd5b..a080a7b3 100644 --- a/project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md +++ b/project-ideas/shopify-integration/shopify-event-based-inventory-sync-triggers.md @@ -77,7 +77,7 @@ sequenceDiagram | 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 | `reset#InventoryItem` or `create#ExternalInventoryReset` completion for `_NA_` facility | `sync#InventoryAdjustmentToShopify` | `inventoryAdjustQuantities` using computed delta only | This is for accumulated inventory pushed through `_NA_`; store-level POS inventory should still be event-driven by shipment, receipt, and correction events | +| `_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 @@ -111,7 +111,7 @@ Use this for: - cycle count - manual variance - external POS sale when Shopify did not create the sale -- external reset delta for `_NA_` accumulated inventory after OMS computes the difference +- external reset delta for `_NA_` accumulated inventory from the created `ExternalInventoryReset` record Workflow: From 434f9f38b5342fdb84f16e2619c5f72e4a85537b Mon Sep 17 00:00:00 2001 From: Chinmay2107 Date: Sun, 3 May 2026 20:29:50 +0530 Subject: [PATCH 18/20] Update Shopify event-based inventory sync implementation --- ...ent-based-inventory-sync-implementation.md | 214 ++++++------------ 1 file changed, 68 insertions(+), 146 deletions(-) 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 index 7d718618..0f2d0a4c 100644 --- a/project-ideas/shopify-integration/shopify-event-based-inventory-sync-implementation.md +++ b/project-ideas/shopify-integration/shopify-event-based-inventory-sync-implementation.md @@ -2,7 +2,7 @@ ## Purpose -This document describes a phase 1 implementation for Shopify inventory sync with direct `SECA` action and no dedicated sync history entities. +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. @@ -38,112 +38,56 @@ classDiagram +call lane sync service +log failures } - class SyncRouterService { - +resolve shop context - +validate facility mapping - +validate product mapping - +route to lane helper - } class TransferSyncService { - +sync shipment - +sync receipt + +post#ShopifyTransferShipment + +receive#ShopifyTransferShipment } class FulfillmentSyncService { - +sync store fulfillment + +post#ShopifyFulfillment } class AdjustmentSyncService { - +sync inventory adjustment - } - class ShopifyHelperService { - +resolve shop - +resolve locations - +resolve inventory item ids - +execute graphql + +post#ShopifyPhysicalInventoryVariance + +post#ShopifyManualPhysicalInventoryVariance + +post#ShopifyExternalInventoryReset + +post#ShopifyInventoryAdjustments } - SecaTrigger --> SyncRouterService - SyncRouterService --> TransferSyncService - SyncRouterService --> FulfillmentSyncService - SyncRouterService --> AdjustmentSyncService - TransferSyncService --> ShopifyHelperService - FulfillmentSyncService --> ShopifyHelperService - AdjustmentSyncService --> ShopifyHelperService + SecaTrigger --> TransferSyncService + SecaTrigger --> FulfillmentSyncService + SecaTrigger --> AdjustmentSyncService ``` ## Service Roles -### 1. Setting Guard Service - -Suggested role: - -- `is#ShopifyInventorySyncEnabled` - -Responsibility: - -- read `ProductStoreSetting` -- return true only when the store is explicitly enabled for Shopify inventory sync -- stop all downstream sync when disabled - -This service should be called first by every `SECA` path. - -### 2. Sync Router Service - -Suggested role: +### 1. Transfer Sync Services -- `route#ShopifyInventorySync` - -Responsibility: - -- receive the business key from `SECA` -- determine the event lane -- resolve `productStoreId` -- call the setting guard -- route to the correct lane-specific sync service - -This keeps the `SECA` thin and reusable. - -### 3. Shopify Context Resolver - -Suggested role: - -- `resolve#ShopifyInventorySyncContext` - -Responsibility: +Implemented roles: -- resolve `shopId` -- resolve Shopify Admin config -- resolve OMS POS/store facility to Shopify location -- resolve OMS product to Shopify inventory item -- fail fast if required mapping is missing - -This service should be shared by all lanes. - -### 4. Transfer Sync Services - -Suggested roles: - -- `sync#TransferShipmentToShopify` -- `sync#TransferReceiptToShopify` +- `co.hotwax.sob.transfer.ShopifyTransferOrderServices.post#ShopifyTransferShipment` +- `co.hotwax.sob.transfer.ShopifyTransferOrderServices.receive#ShopifyTransferShipment` Responsibilities: -`sync#TransferShipmentToShopify` +`post#ShopifyTransferShipment` - find the shipped OMS transfer shipment -- create the prerequisite Shopify transfer if needed -- create `InventoryShipment` -- set tracking when available -- mark shipment in transit +- 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` -`sync#TransferReceiptToShopify` -- group receipt quantities by `shipmentId + datetimeReceived + facilityId` -- map to the correct Shopify shipment items -- call `inventoryShipmentReceive` +`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 -### 5. Store Fulfillment Sync Service +### 2. Store Fulfillment Sync Service -Suggested role: +Implemented role: -- `sync#StoreFulfillmentToShopify` +- `co.hotwax.sob.fulfillment.FulfillmentFeedServices.post#ShopifyFulfillment` Responsibility: @@ -154,11 +98,14 @@ Responsibility: This is the correct store-shipment equivalent for Shopify. -### 6. Inventory Adjustment Sync Service +### 3. Inventory Adjustment Sync Services -Suggested role: +Implemented roles: -- `sync#InventoryAdjustmentToShopify` +- `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: @@ -172,54 +119,17 @@ This service should be reused for: - external POS sale where Shopify did not create the sale - `_NA_` accumulated inventory reset delta from the created `ExternalInventoryReset` record -### 7. External Reset Sync Service - -Suggested role: - -- `sync#ExternalInventoryResetToShopify` - -Responsibility: - -- read the created `ExternalInventoryReset` record by `resetItemId` -- process only `_NA_` facility reset records for accumulated inventory -- use `quantityOnHandDiff` and `availableToPromiseDiff` from the reset record as the Shopify delta source -- call `inventoryAdjustQuantities` - -`reset#InventoryItem` should not be the sync source. It computes diffs and calls `create#ExternalInventoryReset`; the durable reset row is the event that should drive Shopify posting. - -### 8. Logging Service +Manual variance is intentionally filtered: -Suggested role: - -- `log#ShopifyInventorySyncFailure` - -Responsibility: - -- write one structured application log entry -- keep logging format stable so support can search by business key - -Suggested log fields: - -- event type -- source service -- orderId -- orderItemSeqId -- shipmentId -- receiptId -- physicalInventoryId -- productStoreId -- facilityId -- shopId -- Shopify location id -- payload summary -- Shopify error text +- `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 router service +2. call the lane sync service 3. log failure without disturbing committed OMS work The `SECA` should not: @@ -230,13 +140,14 @@ The `SECA` should not: ## Suggested SECA Layout -| OMS service | SECA timing | Routed sync service | +| OMS service | SECA timing | Sync service | | --- | --- | --- | -| `co.hotwax.poorti.TransferOrderFulfillmentServices.ship#TransferOrderShipment` | `post-service` | `sync#TransferShipmentToShopify` | -| `ShipmentReceipt` create or update support service | `post-service` or entity hook wrapper | `sync#TransferReceiptToShopify` | -| `co.hotwax.poorti.FulfillmentServices.ship#Shipment` | `post-service` | `sync#StoreFulfillmentToShopify` | -| `co.hotwax.cycleCount.InventoryCountServices.create#PhysicalInventory` | `post-service` | `sync#InventoryAdjustmentToShopify` | -| `create#ExternalInventoryReset` for `_NA_` facility | `post-service` | `sync#ExternalInventoryResetToShopify` | +| `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 @@ -248,6 +159,14 @@ Phase 1 failure handling is intentionally simple: - 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 @@ -258,20 +177,20 @@ If failures become frequent, the next enhancement should be a small retry or out ## Service Interaction Example -Example: store-origin TO shipment +Example: `TO_Receive_Only` warehouse-to-store receipt -1. OMS ships the transfer shipment. -2. `SECA` fires after `ship#TransferOrderShipment`. -3. Router checks `SHOPIFY_INV_SYNC` for the product store. -4. Transfer shipment sync service resolves Shopify context. -5. Service ensures the transfer exists. -6. Service creates Shopify shipment. -7. Service marks shipment in transit. -8. On failure, log the error with `orderId`, `shipmentId`, `facilityId`, and `shopId`. +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 -- keep all Shopify GraphQL calls in helper services, not inside `SECA` - fail fast on missing location or product mapping - never hard reset inventory from these event paths - use adjustment mutations only for adjustment-style events @@ -279,6 +198,9 @@ Example: store-origin TO shipment - 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 ## Operational Note From c60bbc26d19d080fcaaa03c014337d26d5af6c33 Mon Sep 17 00:00:00 2001 From: Chinmay2107 Date: Sun, 3 May 2026 21:25:58 +0530 Subject: [PATCH 19/20] Clarify Shopify online inventory boundary --- ...ent-based-inventory-sync-implementation.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) 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 index 0f2d0a4c..894c6ee5 100644 --- a/project-ideas/shopify-integration/shopify-event-based-inventory-sync-implementation.md +++ b/project-ideas/shopify-integration/shopify-event-based-inventory-sync-implementation.md @@ -19,6 +19,31 @@ This implementation covers these lanes: 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 @@ -202,6 +227,21 @@ Example: `TO_Receive_Only` warehouse-to-store receipt - 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 + ## Operational Note This approach is the right starting point for a small implementation. From c779d72221f9fbc9ab0b42c31100b21931607242 Mon Sep 17 00:00:00 2001 From: Chinmay2107 Date: Mon, 4 May 2026 00:16:04 +0530 Subject: [PATCH 20/20] Add Shopify resources and idempotency note --- ...ent-based-inventory-sync-implementation.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) 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 index 894c6ee5..b7149d7d 100644 --- a/project-ideas/shopify-integration/shopify-event-based-inventory-sync-implementation.md +++ b/project-ideas/shopify-integration/shopify-event-based-inventory-sync-implementation.md @@ -242,6 +242,39 @@ Official references: - 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.