@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (476) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +11 -0
  3. package/dist/cjs/clients/fluent-client.js +13 -6
  4. package/dist/cjs/utils/pagination-helpers.js +38 -2
  5. package/dist/cjs/versori/fluent-versori-client.js +11 -5
  6. package/dist/esm/clients/fluent-client.js +13 -6
  7. package/dist/esm/utils/pagination-helpers.js +38 -2
  8. package/dist/esm/versori/fluent-versori-client.js +11 -5
  9. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  10. package/dist/tsconfig.tsbuildinfo +1 -1
  11. package/dist/tsconfig.types.tsbuildinfo +1 -1
  12. package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
  13. package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
  14. package/docs/00-START-HERE/cli-documentation-index.md +202 -202
  15. package/docs/00-START-HERE/cli-quick-reference.md +252 -252
  16. package/docs/00-START-HERE/decision-tree.md +552 -552
  17. package/docs/00-START-HERE/getting-started.md +1070 -1070
  18. package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
  19. package/docs/00-START-HERE/readme.md +237 -237
  20. package/docs/00-START-HERE/retailerid-configuration.md +404 -404
  21. package/docs/00-START-HERE/sdk-philosophy.md +794 -794
  22. package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
  23. package/docs/01-TEMPLATES/faq.md +686 -686
  24. package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
  25. package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
  26. package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
  27. package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
  28. package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
  29. package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
  30. package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
  31. package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
  32. package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
  33. package/docs/01-TEMPLATES/readme.md +957 -957
  34. package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
  35. package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
  36. package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
  37. package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
  38. package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
  39. package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
  40. package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
  41. package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
  42. package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
  43. package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
  44. package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
  45. package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
  46. package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
  47. package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
  48. package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
  49. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
  50. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
  51. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
  52. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
  53. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
  54. package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
  55. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
  56. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
  57. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
  58. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
  59. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
  60. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
  61. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
  62. package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
  63. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
  64. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
  65. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
  66. package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
  67. package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
  68. package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
  69. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
  70. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
  71. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
  72. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
  73. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
  74. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
  75. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
  76. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
  77. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
  78. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
  79. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
  80. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
  81. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
  82. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
  83. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
  84. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
  85. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
  86. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
  87. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
  88. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
  89. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
  90. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
  91. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
  92. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
  93. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
  94. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
  95. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
  96. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
  97. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
  98. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
  99. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
  100. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
  101. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
  102. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
  103. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
  104. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
  105. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
  106. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
  107. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
  108. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
  109. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
  110. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
  111. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
  112. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
  113. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
  114. package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
  115. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
  116. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
  117. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
  118. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
  119. package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
  120. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
  121. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
  122. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
  123. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
  124. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
  125. package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
  126. package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
  127. package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
  128. package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
  129. package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
  130. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -520
  131. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
  132. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
  133. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
  134. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
  135. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
  136. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
  137. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
  138. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
  139. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
  140. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
  141. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
  142. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
  143. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
  144. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
  145. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
  146. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
  147. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
  148. package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
  149. package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
  150. package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
  151. package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
  152. package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
  153. package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
  154. package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
  155. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
  156. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
  157. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
  158. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
  159. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
  160. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
  161. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
  162. package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
  163. package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
  164. package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
  165. package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
  166. package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
  167. package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
  168. package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
  169. package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
  170. package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
  171. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
  172. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
  173. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
  174. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
  175. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
  176. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
  177. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
  178. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
  179. package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
  180. package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
  181. package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
  182. package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
  183. package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
  184. package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
  185. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
  186. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
  187. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
  188. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
  189. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
  190. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
  191. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
  192. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
  193. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
  194. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
  195. package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
  196. package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
  197. package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
  198. package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
  199. package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
  200. package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
  201. package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
  202. package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
  203. package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
  204. package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
  205. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
  206. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
  207. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
  208. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
  209. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
  210. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
  211. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
  212. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
  213. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
  214. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
  215. package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
  216. package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
  217. package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
  218. package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
  219. package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
  220. package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
  221. package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
  222. package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
  223. package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
  224. package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
  225. package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
  226. package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
  227. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
  228. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
  229. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
  230. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
  231. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
  232. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
  233. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
  234. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
  235. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
  236. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
  237. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
  238. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
  239. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
  240. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
  241. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
  242. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
  243. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
  244. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
  245. package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
  246. package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
  247. package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
  248. package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
  249. package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
  250. package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
  251. package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
  252. package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
  253. package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
  254. package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
  255. package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
  256. package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
  257. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
  258. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
  259. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
  260. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
  261. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
  262. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
  263. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
  264. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
  265. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
  266. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
  267. package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
  268. package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
  269. package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
  270. package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
  271. package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
  272. package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
  273. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
  274. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
  275. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
  276. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
  277. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
  278. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
  279. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
  280. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
  281. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
  282. package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
  283. package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
  284. package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
  285. package/docs/02-CORE-GUIDES/readme.md +194 -194
  286. package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
  287. package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
  288. package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
  289. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
  290. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
  291. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
  292. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
  293. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
  294. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
  295. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
  296. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
  297. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
  298. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
  299. package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
  300. package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
  301. package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
  302. package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
  303. package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
  304. package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
  305. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
  306. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
  307. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
  308. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
  309. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
  310. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
  311. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
  312. package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
  313. package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
  314. package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
  315. package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
  316. package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
  317. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
  318. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
  319. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
  320. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
  321. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
  322. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
  323. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
  324. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
  325. package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
  326. package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
  327. package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
  328. package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
  329. package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
  330. package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
  331. package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
  332. package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
  333. package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
  334. package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
  335. package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
  336. package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
  337. package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
  338. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
  339. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
  340. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
  341. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
  342. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
  343. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
  344. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
  345. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
  346. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
  347. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
  348. package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
  349. package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
  350. package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
  351. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
  352. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
  353. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
  354. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
  355. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
  356. package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
  357. package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
  358. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
  359. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
  360. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
  361. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
  362. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
  363. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
  364. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
  365. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
  366. package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
  367. package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
  368. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
  369. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
  370. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
  371. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
  372. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
  373. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
  374. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
  375. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
  376. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
  377. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
  378. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
  379. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
  380. package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
  381. package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
  382. package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
  383. package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
  384. package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
  385. package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
  386. package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
  387. package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
  388. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
  389. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
  390. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
  391. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
  392. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
  393. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
  394. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
  395. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
  396. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
  397. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
  398. package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
  399. package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
  400. package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
  401. package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
  402. package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
  403. package/docs/03-PATTERN-GUIDES/readme.md +159 -159
  404. package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
  405. package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
  406. package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
  407. package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
  408. package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
  409. package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
  410. package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
  411. package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
  412. package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
  413. package/docs/04-REFERENCE/architecture/readme.md +279 -279
  414. package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
  415. package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
  416. package/docs/04-REFERENCE/platforms/readme.md +135 -135
  417. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
  418. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
  419. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
  420. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
  421. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
  422. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
  423. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
  424. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
  425. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
  426. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
  427. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
  428. package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
  429. package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
  430. package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
  431. package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
  432. package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
  433. package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
  434. package/docs/04-REFERENCE/readme.md +148 -148
  435. package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
  436. package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
  437. package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
  438. package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
  439. package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
  440. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
  441. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
  442. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
  443. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
  444. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
  445. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
  446. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
  447. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
  448. package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
  449. package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
  450. package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
  451. package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
  452. package/docs/04-REFERENCE/schema/readme.md +141 -141
  453. package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
  454. package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
  455. package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
  456. package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
  457. package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
  458. package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
  459. package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
  460. package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
  461. package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
  462. package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
  463. package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
  464. package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
  465. package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
  466. package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
  467. package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
  468. package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
  469. package/docs/04-REFERENCE/testing/readme.md +86 -86
  470. package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
  471. package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
  472. package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
  473. package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
  474. package/docs/template-loading-matrix.md +242 -242
  475. package/package.json +5 -3
  476. package/docs/02-CORE-GUIDES/api-reference/cli-profile-integration.md +0 -377
@@ -1,1882 +1,1882 @@
1
- ---
2
- template_id: tpl-ingest-multi-channel-inventory-sync
3
- canonical_filename: template-ingestion-multi-channel-inventory-sync.md
4
- sdk_version: latest
5
- runtime: versori
6
- direction: ingestion
7
- source: multi-channel-rest-s3
8
- destination: fluent-batch-api
9
- entity: inventory
10
- format: json-csv
11
- logging: versori
12
- status: stable
13
- ---
14
-
15
- # Template: Ingestion - Multi-Channel Inventory Sync (Scheduled)
16
-
17
- ## STEP 1: Understand This Template
18
-
19
- **What This Template Does:**
20
-
21
- - Scheduled Versori workflow for multi-channel inventory aggregation and sync
22
- - Fetches inventory from multiple sources in parallel (REST API, S3 CSV, Fluent GraphQL)
23
- - Calculates channel-specific ATP (Available To Promise) with configurable buffers and caps
24
- - Performs delta detection using VersoriKVAdapter to sync only changed inventory records
25
- - Handles partial channel failures gracefully with Promise.allSettled
26
- - Rate-limits external REST API calls with exponential backoff retry logic
27
- - Sends consolidated inventory updates to Fluent Commerce Batch API with chunking
28
- - Tracks job lifecycle with JobTracker for operational monitoring
29
- - Supports scheduled (cron) and ad-hoc (webhook) triggers
30
-
31
- **Key SDK Components:**
32
-
33
- - `createClient()` - Universal client factory (auto-detects Versori context)
34
- - `S3DataSource` - S3 file operations with retry logic
35
- - `CSVParserService` - CSV parsing with validation
36
- - `UniversalMapper` - Field transformation with SDK resolvers
37
- - `StateService` + `VersoriKVAdapter` - Delta detection state management
38
- - `JobTracker` - Job lifecycle tracking - `FluentClient.createJob()` - Create Batch API job
39
- - `FluentClient.sendBatch()` - Send inventory chunks (fire-and-forget)
40
- - `FluentClient.graphql()` - Query current Fluent inventory state with auto-pagination
41
- - Native Versori `log` - Use `log` from context
42
- **Entity Type:**
43
-
44
- - **InventoryQuantity** - Fluent entity for inventory positions and quantities
45
- - **EntityType: 'INVENTORY'** - Used in Batch API `sendBatch()` call
46
- - **Batch API Method** - Uses `createJob()` and `sendBatch()` (not Event API)
47
-
48
- **Critical Patterns:**
49
-
50
- - **Multi-source aggregation**: Use `Promise.allSettled()` for parallel channel fetching
51
- - **Graceful degradation**: Continue processing even if one channel fails
52
- - **Delta detection**: StateService tracks previous state to detect changes
53
- - **Progress Logging**: Enhanced logging with context (sample SKUs, locations)
54
- - **JobTracker Progress Updates**: Periodic progress updates during batch processing
55
- - **ATP calculation**: `ATP = (onHand - reserved) - buffer` with oversell protection
56
- - **Delta detection**: Use `StateService` + `VersoriKVAdapter` to track previous ATP values
57
- - **Rate limiting**: Enforce minimum interval between channel API requests
58
- - **Safe configuration**: External JSON mapping file (not inline TypeScript)
59
- - **BPP Configuration**: Use `'skip'` (delta detection already filters changes)
60
- - **Fire-and-forget batches**: Batch submission is asynchronous (no polling needed)
61
-
62
- **When to Use This Template:**
63
-
64
- - ✅ Multiple inventory sources (3+ channels) requiring aggregation
65
- - ✅ Channel-specific ATP calculations with buffers and caps
66
- - ✅ Delta detection needed (only sync changed records)
67
- - ✅ Partial channel failures shouldn't block entire sync
68
- - ✅ External APIs require rate limiting and retry logic
69
- - ✅ Scheduled batch processing (every 15 minutes, hourly, etc.)
70
-
71
- **When NOT to Use:**
72
-
73
- - ❌ Single inventory source (use simpler single-source templates)
74
- - ❌ Real-time sync required (this is designed for scheduled batch processing)
75
- - ❌ No ATP calculations needed (use direct field mapping)
76
- - ❌ Don't need delta detection (full snapshots every time)
77
- - ❌ Products, Locations, Customers (use Event API templates)
78
-
79
- ---
80
-
81
- ## STEP 2: AI Prompt
82
-
83
- **Copy this prompt to generate the complete implementation:**
84
-
85
- ```
86
- Create a Versori scheduled workflow for multi-channel inventory aggregation to Fluent Commerce Batch API.
87
-
88
- REQUIREMENTS:
89
- 1. Runtime: Versori Platform (scheduled workflow)
90
- 2. Sources: Multiple channels (REST API, S3 CSV, Fluent GraphQL)
91
- 3. Destination: Fluent Commerce Batch API (InventoryQuantity entity)
92
- 4. Entity: InventoryQuantity (EntityType: 'INVENTORY')
93
-
94
- KEY FEATURES:
95
- 1. **Multi-channel fetching** (parallel):
96
- - **Channel A**: REST API with rate limiting (120 RPM) and retry logic
97
- - **Channel B**: S3 CSV file with CSVParserService
98
- - **Fluent GraphQL**: Current inventory state via auto-pagination
99
- 2. **ATP (Available To Promise) calculation**:
100
- - Formula: `ATP = (onHand - reserved) - buffer`
101
- - Channel-specific buffers (configurable per channel)
102
- - Aggregate ATP across channels with deduplication by SKU + location
103
- - Support oversell protection (ensure ATP ≥ 0)
104
- 3. **Delta detection**:
105
- - Use `StateService` + `VersoriKVAdapter` to track previous ATP values
106
- - Only send changed records to Batch API
107
- - Store state with 7-day TTL in Versori KV
108
- 4. **Graceful degradation**:
109
- - Use `Promise.allSettled()` for channel fetching
110
- - Continue processing even if one channel fails
111
- - Log channel failures but don't block entire sync
112
- 5. **Batch API**:
113
- - Entity: `InventoryQuantity`
114
- - EntityType: `'INVENTORY'`
115
- - Action: `'UPSERT'`
116
- - BPP: `'skip'` (delta detection already filters)
117
- - Batch size: 500 records per chunk
118
- - Poll batches until complete with exponential backoff
119
- 6. **Job tracking**:
120
- - Use `JobTracker` with Versori KV storage
121
- - Track status transitions: `fetching_channels` → `aggregating` → `delta_detection` → `creating_batch_job` → `sending_batches` → `polling_batches` → `updating_delta_state`
122
- 7. **Modular architecture**:
123
- - Separate service files (ATP calculator, channel connectors, batch dispatcher)
124
- - External JSON mapping config
125
- - Clean separation of concerns
126
-
127
- CRITICAL REQUIREMENTS:
128
- 1. Modular architecture: Separate service files (ATP calculator, channel connectors, batch dispatcher)
129
- 2. External JSON mapping config: Use `with { type: 'json' }` import syntax
130
- 3. Native logging: Use log from context (LoggingService removed - use native log)
131
- 4. Graceful degradation: Promise.allSettled for parallel channel fetching
132
- 5. Delta detection: Track previous ATP values in Versori KV
133
- 6. BPP: Set to 'skip' (delta already filters changes)
134
- 7. Rate limiting: Enforce minimum interval between Channel A requests
135
- 8. Job tracking: Use JobTracker for lifecycle management
136
-
137
- SDK METHODS TO USE:
138
- - createClient(ctx) - Pass entire Versori context, auto-detects platform
139
- - new S3DataSource(config, log) - S3 file operations
140
- - new CSVParserService() - Parse CSV files
141
- - await client.graphql({ query, variables, pagination }) - Fluent GraphQL extraction with auto-pagination
142
- - new VersoriKVAdapter(openKv(':project:')) - Versori KV storage adapter
143
- - new StateService(kvAdapter) - Delta state management (takes KV adapter, not logger)
144
- - new JobTracker(kv, log) - Job lifecycle tracking
145
- - await client.createJob({ name, retailerId, meta: { preprocessing: 'skip' } }) - Create Batch API job
146
- - await client.sendBatch(jobId, { action, entityType, source, event, entities }) - Send batch chunk (fire-and-forget)
147
-
148
- FORBIDDEN PATTERNS:
149
- - ❌ LoggingService (removed - use native log on Versori)
150
- - ❌ Don't use monolithic index.ts (extract services into separate files)
151
- - ❌ Don't use inline mapping config (use external JSON file)
152
- - ❌ Don't fail entire sync if one channel fails (use Promise.allSettled)
153
- - ❌ Don't send all records (use delta detection to filter unchanged)
154
- - ❌ Don't use BPP with deltas (set preprocessing: 'skip')
155
- - ❌ Don't forget rate limiting for external APIs
156
- - ❌ Don't forget to call dispose() on data sources in finally block
157
-
158
- GENERATE:
159
- 1. package.json with dependencies
160
- 2. index.ts (workflow entry point with scheduled/adhoc/status triggers)
161
- 3. src/workflows/multi-channel-sync.workflow.ts (main orchestration logic)
162
- 4. src/services/atp-calculator.service.ts (ATP calculation and aggregation)
163
- 5. src/services/channel-a-connector.service.ts (REST API with rate limiting)
164
- 6. src/services/channel-b-connector.service.ts (S3 CSV fetching)
165
- 7. src/services/batch-processor.service.ts (Batch API submission)
166
- 8. src/services/batch-logger.service.ts (SFTP log file writing)
167
- 9. src/types/multi-channel.types.ts (TypeScript interfaces)
168
- 10. config/multi-channel.mapping.json (mapping configuration - external JSON file)
169
-
170
- NOTE: Use external JSON files for mapping configuration (not TypeScript .config files)
171
-
172
- Ensure all code is production-ready with proper error handling, graceful degradation, and rate limiting. Use modular architecture with separate service files for each concern.
173
- ```
174
-
175
- ---
176
-
177
- ## What You'll Build
178
-
179
- ### Versori Workflows Structure
180
-
181
- **Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
182
-
183
- **Trigger Types:**
184
- - **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
185
- - **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
186
- - **`workflow()`** → Durable workflows (advanced, rarely used)
187
-
188
- **Execution Steps (chained to triggers):**
189
- - **`http()`** → External API calls (chained from schedule/webhook)
190
- - **`fn()`** → Internal processing (chained from schedule/webhook)
191
-
192
- ### Recommended Project Structure
193
-
194
- ```
195
- inventory-batch-sync/
196
- ├── index.ts # Entry point - exports all workflows
197
- └── src/
198
- ├── workflows/
199
- │ ├── scheduled/
200
- │ │ └── daily-inventory-sync.ts # Scheduled: Daily inventory sync
201
- │ │
202
- │ └── webhook/
203
- │ ├── adhoc-inventory-sync.ts # Webhook: Manual trigger
204
- │ └── job-status-check.ts # Webhook: Status query
205
-
206
- ├── services/
207
- │ └── inventory-sync.service.ts # Shared orchestration logic (reusable)
208
-
209
- └── types/
210
- └── inventory.types.ts # Shared type definitions
211
- ```
212
-
213
- **Benefits:**
214
- - ✅ Clear trigger separation (`scheduled/` vs `webhook/`)
215
- - ✅ Descriptive file names (easy to browse and understand)
216
- - ✅ Scalable (add new workflows without cluttering)
217
- - ✅ Reusable code in `services/` (DRY principle)
218
- - ✅ Easy to modify individual workflows without affecting others
219
-
220
- ---
221
-
222
- ## Workflow Files
223
-
224
- ### 1. Scheduled Workflows (`src/workflows/scheduled/`)
225
-
226
- All time-based triggers that run automatically on cron schedules.
227
-
228
- #### `src/workflows/scheduled/daily-inventory-sync.ts`
229
-
230
- **Purpose**: Automatic Daily inventory sync
231
- **Trigger**: Cron schedule (`0 2 * * *`)
232
- **Exposed as Endpoint**: ❌ NO - Runs automatically
233
-
234
- ```typescript
235
- import { schedule, http } from '@versori/run';
236
- import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
237
- import { runIngestion } from '../../services/inventory-sync.service.ts';
238
-
239
- /**
240
- * Scheduled Workflow: Daily Inventory Sync
241
- *
242
- * Runs automatically daily at 2 AM UTC
243
- * NOT exposed as HTTP endpoint - Versori executes on schedule
244
- *
245
- * Uses shared service: inventory-sync.service.ts
246
- */
247
- export const daily_inventory_sync = schedule(
248
- 'inventory-batch-scheduled',
249
- '0 2 * * *' // Daily at 2 AM UTC
250
- ).then(
251
- http('run-inventory-batch', { connection: 'fluent_commerce' }, async ctx => {
252
- const { log, openKv } = ctx;
253
- const jobId = `inventory-batch-${Date.now()}`;
254
- const tracker = new JobTracker(openKv(':project:'), log);
255
-
256
- await tracker.createJob(jobId, { triggeredBy: 'schedule', stage: 'initialization' });
257
- await tracker.updateJob(jobId, { status: 'processing' });
258
-
259
- try {
260
- // Reuse shared orchestration logic
261
- const result = await runIngestion(ctx, jobId, tracker);
262
- await tracker.markCompleted(jobId, result);
263
- return { success: true, jobId, ...result };
264
- } catch (e: any) {
265
- await tracker.markFailed(jobId, e);
266
- return { success: false, jobId, error: e?.message };
267
- }
268
- })
269
- );
270
- ```
271
-
272
- ---
273
-
274
- ### 2. Webhook Workflows (`src/workflows/webhook/`)
275
-
276
- All HTTP-based triggers that create webhook endpoints.
277
-
278
- #### `src/workflows/webhook/adhoc-inventory-sync.ts`
279
-
280
- **Purpose**: Manual inventory sync trigger (on-demand)
281
- **Trigger**: HTTP POST
282
- **Endpoint**: `POST https://{workspace}.versori.run/inventory-batch-adhoc`
283
- **Use Cases**: Testing, priority processing, ad-hoc runs
284
-
285
- ```typescript
286
- import { webhook, http } from '@versori/run';
287
- import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
288
- import { runIngestion } from '../../services/inventory-sync.service.ts';
289
-
290
- /**
291
- * Webhook: Manual Inventory Sync Trigger
292
- *
293
- * Endpoint: POST https://{workspace}.versori.run/inventory-batch-adhoc
294
- * Request body (optional): { filePattern: "urgent_*.xml", maxFiles: 5 }
295
- *
296
- * Pattern: webhook().then(http()) - needs Fluent API access
297
- * Uses shared service: inventory-sync.service.ts
298
- */
299
- export const adhoc_inventory_sync = webhook('inventory-batch-adhoc', {
300
- response: { mode: 'sync' },
301
- connection: 'inventory-batch-adhoc', // Versori validates API key
302
- }).then(
303
- http('run-inventory-batch-adhoc', { connection: 'fluent_commerce' }, async ctx => {
304
- const { log, openKv, data } = ctx;
305
- const jobId = `inventory-batch-adhoc-${Date.now()}`;
306
- const tracker = new JobTracker(openKv(':project:'), log);
307
-
308
- await tracker.createJob(jobId, {
309
- triggeredBy: 'manual',
310
- stage: 'initialization',
311
- options: data // Optional: filePattern, maxFiles, etc.
312
- });
313
- await tracker.updateJob(jobId, { status: 'processing' });
314
-
315
- try {
316
- // Same orchestration logic as scheduled workflow
317
- const result = await runIngestion(ctx, jobId, tracker);
318
- await tracker.markCompleted(jobId, result);
319
- return { success: true, jobId, ...result };
320
- } catch (e: any) {
321
- await tracker.markFailed(jobId, e);
322
- return { success: false, jobId, error: e?.message };
323
- }
324
- })
325
- );
326
- ```
327
-
328
- #### `src/workflows/webhook/job-status-check.ts`
329
-
330
- **Purpose**: Query job status
331
- **Trigger**: HTTP POST
332
- **Endpoint**: `POST https://{workspace}.versori.run/inventory-batch-job-status`
333
- **Request body**: `{ jobId: "inventory-batch-1234567890" }`
334
-
335
- ```typescript
336
- import { webhook, fn } from '@versori/run';
337
- import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
338
-
339
- /**
340
- * Webhook: Job Status Check
341
- *
342
- * Endpoint: POST https://{workspace}.versori.run/inventory-batch-job-status
343
- * Request body: { jobId: "inventory-batch-1234567890" }
344
- *
345
- * Pattern: webhook().then(fn()) - no external API needed, only KV storage
346
- * Lightweight: Only queries KV store, no Fluent API calls
347
- */
348
- export const jobStatusCheck = webhook('inventory-batch-job-status', {
349
- response: { mode: 'sync' },
350
- connection: 'inventory-batch-job-status',
351
- }).then(
352
- fn('status', async ctx => {
353
- const { data, log, openKv } = ctx;
354
- const jobId = data?.jobId as string;
355
-
356
- if (!jobId) {
357
- return { success: false, error: 'jobId required' };
358
- }
359
-
360
- const tracker = new JobTracker(openKv(':project:'), log);
361
- const status = await tracker.getJob(jobId);
362
-
363
- return status
364
- ? { success: true, jobId, ...status }
365
- : { success: false, error: 'Job not found', jobId };
366
- })
367
- );
368
- ```
369
-
370
- ---
371
-
372
- ### 3. Entry Point (`index.ts`)
373
-
374
- **Purpose**: Register all workflows with Versori platform
375
-
376
- ```typescript
377
- /**
378
- * Entry Point - Registers all workflows with Versori platform
379
- *
380
- * Versori automatically discovers and registers exported workflows
381
- *
382
- * File Structure:
383
- * - src/workflows/scheduled/ → Time-based triggers (cron)
384
- * - src/workflows/webhook/ → HTTP-based triggers (webhooks)
385
- */
386
-
387
- // Import scheduled workflows
388
- import { daily_inventory_sync } from './src/workflows/scheduled/daily-inventory-sync';
389
-
390
- // Import webhook workflows
391
- import { adhoc_inventory_sync } from './src/workflows/webhook/adhoc-inventory-sync';
392
- import { jobStatusCheck } from './src/workflows/webhook/job-status-check';
393
-
394
- // Register all workflows
395
- export {
396
- // Scheduled (time-based triggers)
397
- daily_inventory_sync,
398
-
399
- // Webhooks (HTTP-based triggers)
400
- adhoc_inventory_sync,
401
- jobStatusCheck,
402
- };
403
- ```
404
-
405
- **What Gets Exposed:**
406
-
407
- - ✅ `adhoc_inventory_sync` → `https://{workspace}.versori.run/inventory-batch-adhoc`
408
- - ✅ `jobStatusCheck` → `https://{workspace}.versori.run/inventory-batch-job-status`
409
- - ❌ `daily_inventory_sync` → NOT exposed (runs automatically on cron)
410
-
411
- ---
412
-
413
- ### Adding New Workflows
414
-
415
- **To add a scheduled workflow:**
416
- 1. Create file in `src/workflows/scheduled/` with descriptive name (e.g., `hourly-delta-sync.ts`)
417
- 2. Export the workflow from the file
418
- 3. Import and re-export in `index.ts`
419
-
420
- **To add a webhook workflow:**
421
- 1. Create file in `src/workflows/webhook/` with descriptive name (e.g., `error-notification.ts`)
422
- 2. Export the workflow from the file
423
- 3. Import and re-export in `index.ts`
424
-
425
- **Example - Adding hourly delta sync:**
426
-
427
- ```typescript
428
- // src/workflows/scheduled/hourly-delta-sync.ts
429
- export const hourlyDeltaSync = schedule(
430
- 'inventory-delta-hourly',
431
- '0 * * * *' // Every hour
432
- ).then(
433
- http('run-delta-sync', { connection: 'fluent_commerce' }, async ctx => {
434
- // Delta sync logic (skip BPP)
435
- const result = await runIngestion(ctx, jobId, tracker, { bppEnabled: false });
436
- return result;
437
- })
438
- );
439
-
440
- // index.ts (add to imports and exports)
441
- import { hourlyDeltaSync } from './src/workflows/scheduled/hourly-delta-sync';
442
- export { daily_inventory_sync, hourlyDeltaSync, ... };
443
- ```
444
-
445
- ---
446
- ## Complete Modular Implementation
447
-
448
- ### File: `package.json`
449
-
450
- ```json
451
- {
452
- "name": "multi-channel-inventory-sync",
453
- "version": "1.0.0",
454
- "description": "Multi-Channel Inventory Aggregation to Fluent Commerce Batch API",
455
- "type": "module",
456
- "versori": {
457
- "workflows": "./index.ts"
458
- },
459
- "scripts": {
460
- "lint": "eslint . --ext .ts",
461
- "typecheck": "tsc --noEmit"
462
- },
463
- "dependencies": {
464
- "@fluentcommerce/fc-connect-sdk": "^0.1.39",
465
- "@versori/run": "latest"
466
- },
467
- "devDependencies": {
468
- "@types/node": "^20.0.0",
469
- "typescript": "^5.0.0"
470
- },
471
- "engines": {
472
- "node": ">=18.0.0"
473
- }
474
- }
475
- ```
476
-
477
- ---
478
-
479
- ### File: `index.ts`
480
-
481
- ```typescript
482
- import { schedule, webhook, http, fn } from '@versori/run';
483
- import { processMultiChannelSync } from './src/workflows/multi-channel-sync.workflow';
484
-
485
- /**
486
- * Scheduled workflow: Multi-channel inventory sync every 15 minutes
487
- *
488
- * Processing: Parallel channel fetching with graceful degradation
489
- * BPP: Disabled (preprocessing: 'skip') - delta detection already filters
490
- * State Management: VersoriKVAdapter + JobTracker prevent duplicates
491
- */
492
- export const scheduledMultiChannelSync = schedule(
493
- 'multi-channel-sync',
494
- '*/15 * * * *' // Every 15 minutes
495
- ).then(
496
- http('run-sync', { connection: 'fluent_commerce' }, async ctx => {
497
- // ctx contains: fetch, connections, log, activation, openKv
498
- // Pass entire context to workflow
499
- return await processMultiChannelSync(ctx);
500
- })
501
- );
502
-
503
- /**
504
- * Manual trigger endpoint for testing and ad-hoc runs
505
- */
506
- export const adhocMultiChannelSync = webhook('multi-channel-sync-adhoc', {
507
- response: { mode: 'sync' },
508
- connection: 'multi-channel-sync-adhoc',
509
- }).then(
510
- http('run-sync-adhoc', { connection: 'fluent_commerce' }, async ctx => {
511
- return await processMultiChannelSync(ctx);
512
- })
513
- );
514
-
515
- /**
516
- * Job status check endpoint
517
- */
518
- export const multiChannelSyncJobStatus = webhook('multi-channel-sync-job-status', {
519
- response: { mode: 'sync' },
520
- connection: 'multi-channel-sync-job-status',
521
- }).then(
522
- fn('status', async ctx => {
523
- const { data, log, openKv } = ctx;
524
- const jobId = data?.jobId as string;
525
- if (!jobId) return { success: false, error: 'jobId required' };
526
- const { JobTracker } = await import('@fluentcommerce/fc-connect-sdk');
527
- const tracker = new JobTracker(openKv(':project:'), log);
528
- const status = await tracker.getJob(jobId);
529
- return status
530
- ? { success: true, jobId, ...status }
531
- : { success: false, error: 'Job not found', jobId };
532
- })
533
- );
534
- ```
535
-
536
- ---
537
-
538
- ### File: `src/types/multi-channel.types.ts`
539
-
540
- ```typescript
541
- /**
542
- * Type definitions for multi-channel inventory sync
543
- */
544
- export interface ChannelInventoryRecord {
545
- sku: string;
546
- location: string;
547
- channel: string;
548
- onHand: number;
549
- reserved: number;
550
- buffer: number;
551
- lastUpdated?: string;
552
- }
553
-
554
- export interface AggregatedInventory {
555
- sku: string;
556
- location: string;
557
- totalOnHand: number;
558
- totalReserved: number;
559
- atp: number;
560
- channels: {
561
- [channel: string]: {
562
- allocated: number;
563
- buffer: number;
564
- max?: number;
565
- };
566
- };
567
- }
568
-
569
- export interface SyncState {
570
- [sku: string]: {
571
- [location: string]: number; // ATP value
572
- };
573
- }
574
-
575
- export interface SyncStats {
576
- totalRecords: number;
577
- channelARecords: number;
578
- channelBRecords: number;
579
- fluentRecords: number;
580
- aggregatedSkus: number;
581
- changedRecords: number;
582
- batchesSent: number;
583
- successCount: number;
584
- errorCount: number;
585
- duration: number;
586
- }
587
-
588
- export interface BatchDetail {
589
- batchId: string;
590
- recordCount: number;
591
- timestamp: string;
592
- status: 'SENT' | 'FAILED';
593
- error?: string;
594
- }
595
-
596
- export interface BatchResult {
597
- totalSent: number;
598
- batchCount: number;
599
- batches: BatchDetail[];
600
- errors: Array<{ batchId: string; error: string }>;
601
- }
602
- ```
603
-
604
- ---
605
-
606
- ### File: `src/config/multi-channel.mapping.json`
607
-
608
- ```json
609
- {
610
- "name": "multi-channel-inventory",
611
- "version": "1.0.0",
612
- "description": "Normalize channel payloads to aggregation schema",
613
- "fields": {
614
- "locationRef": {
615
- "source": "locationRef",
616
- "required": true,
617
- "resolver": "sdk.trim",
618
- "comment": "Location reference"
619
- },
620
- "skuRef": {
621
- "source": "skuRef",
622
- "required": true,
623
- "resolver": "sdk.trim",
624
- "comment": "SKU reference"
625
- },
626
- "onHand": {
627
- "source": "onHand",
628
- "resolver": "sdk.number",
629
- "comment": "On-hand quantity"
630
- },
631
- "reserved": {
632
- "source": "reserved",
633
- "resolver": "sdk.number",
634
- "comment": "Reserved quantity"
635
- },
636
- "buffer": {
637
- "source": "buffer",
638
- "resolver": "sdk.number",
639
- "comment": "Safety buffer"
640
- },
641
- "channel": {
642
- "source": "channel",
643
- "defaultValue": "UNKNOWN",
644
- "comment": "Channel identifier"
645
- }
646
- }
647
- }
648
- ```
649
-
650
- > **✅ PRODUCTION STANDARD:** Use external JSON files for mapping configuration (not TypeScript objects)
651
-
652
- ---
653
-
654
- ### File: `src/services/atp-calculator.service.ts`
655
-
656
- ```typescript
657
- import type { ChannelInventoryRecord, AggregatedInventory } from '../types/multi-channel.types';
658
-
659
- /**
660
- * Service for calculating ATP (Available To Promise) across channels
661
- */
662
- export class ATPCalculatorService {
663
- private readonly oversellProtection: boolean;
664
-
665
- constructor(
666
- oversellProtection = true
667
- ) {
668
- this.oversellProtection = oversellProtection;
669
- }
670
-
671
- /**
672
- * Calculate base ATP for a single record
673
- * Formula: ATP = (onHand - reserved) - buffer
674
- */
675
- calculateBaseATP(onHand: number, reserved: number, buffer: number): number {
676
- const available = Math.max(0, onHand - reserved);
677
- const atp = available - buffer;
678
- return this.oversellProtection ? Math.max(0, atp) : atp;
679
- }
680
-
681
- /**
682
- * Aggregate inventory across channels
683
- * Deduplicates by SKU + location and calculates consolidated ATP
684
- */
685
- aggregateChannelInventory(records: ChannelInventoryRecord[]): Map<string, AggregatedInventory> {
686
- const aggregated = new Map<string, AggregatedInventory>();
687
-
688
- for (const record of records) {
689
- const key = `${record.sku}:${record.location}`;
690
- const existing = aggregated.get(key);
691
-
692
- if (existing) {
693
- // Aggregate across channels
694
- existing.totalOnHand += record.onHand;
695
- existing.totalReserved += record.reserved;
696
- existing.channels[record.channel] = {
697
- allocated: record.onHand - record.reserved - record.buffer,
698
- buffer: record.buffer,
699
- };
700
- } else {
701
- // First record for this SKU+location
702
- aggregated.set(key, {
703
- sku: record.sku,
704
- location: record.location,
705
- totalOnHand: record.onHand,
706
- totalReserved: record.reserved,
707
- atp: 0,
708
- channels: {
709
- [record.channel]: {
710
- allocated: record.onHand - record.reserved - record.buffer,
711
- buffer: record.buffer,
712
- },
713
- },
714
- });
715
- }
716
- }
717
-
718
- // Calculate final ATP for each aggregated record
719
- for (const [, agg] of aggregated) {
720
- const totalBuffer = Math.max(...Object.values(agg.channels).map(c => c.buffer));
721
- agg.atp = this.calculateBaseATP(agg.totalOnHand, agg.totalReserved, totalBuffer);
722
- }
723
-
724
-
725
- return aggregated;
726
- }
727
- }
728
- ```
729
-
730
- ---
731
-
732
- ### File: `src/services/channel-a-connector.service.ts`
733
-
734
- ```typescript
735
-
736
- /**
737
- * Service for fetching inventory from Channel A REST API
738
- * Includes rate limiting and exponential backoff retry logic
739
- */
740
- export class ChannelAConnectorService {
741
- private readonly url: string;
742
- private readonly apiKey: string;
743
- private readonly rateLimitRpm: number;
744
- private readonly logger; // ✅ Versori native log - TypeScript infers type
745
- private lastRequest = 0;
746
-
747
- constructor(url: string, apiKey: string, rateLimitRpm: number, logger) { // ✅ Versori native log - TypeScript infers type
748
- this.url = url;
749
- this.apiKey = apiKey;
750
- this.rateLimitRpm = rateLimitRpm;
751
- this.logger = logger;
752
- }
753
-
754
- /**
755
- * Fetch inventory from Channel A with rate limiting
756
- */
757
- async fetchInventory(): Promise<any[]> {
758
- await this.enforceRateLimit();
759
-
760
- const response = await this.fetchWithRetry(this.url, {
761
- method: 'GET',
762
- headers: {
763
- 'Content-Type': 'application/json',
764
- Authorization: `Bearer ${this.apiKey}`,
765
- },
766
- });
767
-
768
- if (!response.ok) {
769
- throw new Error(`Channel A API error: ${response.status} ${response.statusText}`);
770
- }
771
-
772
- const data = await response.json();
773
- return data.inventory || [];
774
- }
775
-
776
- /**
777
- * Enforce rate limit (minimum interval between requests)
778
- */
779
- private async enforceRateLimit(): Promise<void> {
780
- const minInterval = 60000 / this.rateLimitRpm;
781
- const now = Date.now();
782
- const elapsed = now - this.lastRequest;
783
-
784
- if (elapsed < minInterval) {
785
- const wait = minInterval - elapsed;
786
- await new Promise(resolve => setTimeout(resolve, wait));
787
- }
788
-
789
- this.lastRequest = Date.now();
790
- }
791
-
792
- /**
793
- * Fetch with exponential backoff retry
794
- */
795
- private async fetchWithRetry(
796
- url: string,
797
- options: RequestInit,
798
- maxRetries = 3
799
- ): Promise<Response> {
800
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
801
- try {
802
- const response = await fetch(url, options);
803
-
804
- if (response.status === 429 || response.status >= 500) {
805
- if (attempt < maxRetries) {
806
- const backoff = Math.pow(2, attempt) * 1000;
807
- `[ChannelA] Error ${response.status}, retrying in ${backoff}ms (attempt ${attempt}/${maxRetries})`
808
- );
809
- await new Promise(resolve => setTimeout(resolve, backoff));
810
- continue;
811
- }
812
- }
813
-
814
- return response;
815
- } catch (error) {
816
- if (attempt === maxRetries) throw error;
817
- const backoff = Math.pow(2, attempt) * 1000;
818
- `[ChannelA] Fetch error, retrying in ${backoff}ms (attempt ${attempt}/${maxRetries}):`,
819
- error
820
- );
821
- await new Promise(resolve => setTimeout(resolve, backoff));
822
- }
823
- }
824
-
825
- throw new Error('Channel A: Max retries exceeded');
826
- }
827
- }
828
- ```
829
-
830
- ---
831
-
832
- ### File: `src/services/channel-b-connector.service.ts`
833
-
834
- ```typescript
835
- import { S3DataSource, CSVParserService } from '@fluentcommerce/fc-connect-sdk';
836
-
837
- /**
838
- * Service for fetching inventory from Channel B S3 CSV
839
- */
840
- export class ChannelBConnectorService {
841
- private readonly s3: S3DataSource;
842
- private readonly csv: CSVParserService;
843
- private readonly bucket: string;
844
- private readonly key: string;
845
- private readonly logger; // ✅ Versori native log - TypeScript infers type
846
-
847
- constructor(s3Config: any, bucket: string, key: string, logger) { // ✅ Versori native log - TypeScript infers type
848
- this.s3 = new S3DataSource(s3Config, logger);
849
- this.csv = new CSVParserService();
850
- this.bucket = bucket;
851
- this.key = key;
852
- }
853
-
854
- /**
855
- * Fetch inventory from S3 CSV file
856
- */
857
- async fetchInventory(): Promise<any[]> {
858
- try {
859
- const csvContent = (await this.s3.downloadFile(`${this.bucket}/${this.key}`, {
860
- encoding: 'utf8',
861
- })) as string;
862
-
863
- const records = await this.csv.parse(csvContent, {
864
- columns: true,
865
- skip_empty_lines: true,
866
- trim: true,
867
- });
868
-
869
- return records || [];
870
- } catch (error) {
871
- throw error;
872
- }
873
- // Note: S3DataSource doesn't require explicit disposal (unlike SFTP)
874
- }
875
- }
876
- ```
877
-
878
- ---
879
-
880
- ### File: `src/services/batch-processor.service.ts`
881
-
882
- ```typescript
883
- import type { FluentClient } from '@fluentcommerce/fc-connect-sdk';
884
- import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
885
- import { BatchResult, BatchDetail } from '../types/multi-channel.types';
886
-
887
- /**
888
- * Service for sending records to Fluent Batch API
889
- *
890
- * ✅ PRODUCTION ENHANCEMENT: Optional logger for detailed progress tracking
891
- */
892
- export class BatchProcessorService {
893
- constructor(
894
- private client: FluentClient,
895
- private jobTracker: JobTracker,
896
- private log?: any // ✅ Optional logger for progress tracking
897
- ) {}
898
-
899
- /**
900
- * Send inventory records to Batch API with chunking
901
- */
902
- async sendInventoryBatches(
903
- jobId: string,
904
- records: any[],
905
- batchSize: number = 500
906
- ): Promise<BatchResult> {
907
- const result: BatchResult = {
908
- totalSent: 0,
909
- batchCount: 0,
910
- batches: [],
911
- errors: [],
912
- };
913
-
914
- // Chunk records into batches
915
- const chunks: any[][] = [];
916
- for (let i = 0; i < records.length; i += batchSize) {
917
- chunks.push(records.slice(i, i + batchSize));
918
- }
919
-
920
- const totalBatches = chunks.length;
921
-
922
- // ✅ PRODUCTION ENHANCEMENT: Log batch sending start
923
- if (this.log) {
924
- this.log.info('📤 Starting batch sending', {
925
- jobId,
926
- totalRecords: records.length,
927
- batchSize,
928
- totalBatches,
929
- processingMode: 'sequential (one at a time)',
930
- });
931
- }
932
-
933
- // Send each batch
934
- for (let i = 0; i < chunks.length; i++) {
935
- const batchNumber = i + 1;
936
-
937
- // ✅ PRODUCTION ENHANCEMENT: Log progress every 10 batches
938
- if (this.log && batchNumber % 10 === 0) {
939
- this.log.info(`📤 Sending batch ${batchNumber}/${totalBatches}`, {
940
- jobId,
941
- batchNumber,
942
- totalBatches,
943
- recordsInBatch: chunks[i].length,
944
- totalSentSoFar: result.totalSent,
945
- progress: `${((batchNumber / totalBatches) * 100).toFixed(1)}%`,
946
- });
947
- }
948
-
949
- try {
950
- const batch = await this.client.sendBatch(jobId, {
951
- action: 'UPSERT',
952
- entityType: 'INVENTORY',
953
- source: 'MULTI_CHANNEL',
954
- event: 'MULTI_CHANNEL_SYNC',
955
- entities: chunks[i],
956
- });
957
-
958
- result.totalSent += chunks[i].length;
959
- result.batchCount++;
960
- result.batches.push({
961
- batchId: batch.id,
962
- recordCount: chunks[i].length,
963
- timestamp: new Date().toISOString(),
964
- status: 'SENT',
965
- });
966
-
967
- // ✅ No logging here - workflow handles it
968
-
969
- // Update job tracker
970
- await this.jobTracker.updateJob(jobId, {
971
- details: {
972
- batchesSent: result.batchCount,
973
- recordsProcessed: result.totalSent,
974
- },
975
- });
976
- } catch (error: any) {
977
- result.errors.push({
978
- batchId: `batch-${batchNumber}`,
979
- error: error.message,
980
- });
981
- result.batches.push({
982
- batchId: `batch-${batchNumber}`,
983
- recordCount: chunks[i].length,
984
- timestamp: new Date().toISOString(),
985
- status: 'FAILED',
986
- error: error.message,
987
- });
988
- }
989
- }
990
-
991
- // ✅ PRODUCTION ENHANCEMENT: Log completion
992
- if (this.log) {
993
- this.log.info('✅ Sequential batch sending completed', {
994
- jobId,
995
- totalBatches,
996
- batchesSent: result.batchCount,
997
- batchesFailed: result.errors.length,
998
- totalRecordsSent: result.totalSent,
999
- });
1000
- }
1001
-
1002
- return result;
1003
- }
1004
- }
1005
- ```
1006
-
1007
- ---
1008
-
1009
- ### File: `src/services/batch-logger.service.ts`
1010
-
1011
- ```typescript
1012
- import { Buffer } from 'node:buffer';
1013
- import { S3DataSource } from '@fluentcommerce/fc-connect-sdk';
1014
-
1015
- /**
1016
- * Service for writing batch processing logs to S3
1017
- */
1018
- export class BatchLoggerService {
1019
- constructor(private s3: S3DataSource) {
1020
- // ✅ No logger - workflow handles logging with Versori native log
1021
- }
1022
-
1023
- /**
1024
- * Write batch processing log to S3
1025
- */
1026
- async writeBatchLog(
1027
- logData: any,
1028
- bucket: string,
1029
- keyPrefix: string,
1030
- format: 'json' | 'text' = 'json'
1031
- ): Promise<void> {
1032
- try {
1033
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1034
- const logFileName = `${keyPrefix}${timestamp}.${format === 'json' ? 'json' : 'log'}`;
1035
- const logKey = `${bucket}/${logFileName}`;
1036
-
1037
- const logContent = this.formatLogContent(logData, format);
1038
-
1039
- await this.s3.uploadFile(logKey, Buffer.from(logContent, 'utf8'), {
1040
- encoding: 'utf8',
1041
- contentType: format === 'json' ? 'application/json' : 'text/plain',
1042
- });
1043
-
1044
- // ✅ No logging here - workflow handles it
1045
- } catch (error: any) {
1046
- // ✅ No logging here - workflow handles it
1047
- // Don't throw - logging failure shouldn't stop workflow
1048
- }
1049
- }
1050
-
1051
- private formatLogContent(logData: any, format: 'json' | 'text'): string {
1052
- if (format === 'json') {
1053
- return JSON.stringify(logData, null, 2);
1054
- }
1055
-
1056
- return `Multi-Channel Sync Log
1057
- ======================
1058
- Timestamp: ${logData.timestamp}
1059
- Job ID: ${logData.jobId}
1060
-
1061
- Channel Summary:
1062
- Channel A: ${logData.channels.channelA || 0} records
1063
- Channel B: ${logData.channels.channelB || 0} records
1064
- Fluent: ${logData.channels.fluent || 0} records
1065
-
1066
- Aggregation:
1067
- Total Records: ${logData.aggregated}
1068
- Changed Records: ${logData.changed}
1069
- Unchanged: ${logData.unchanged}
1070
-
1071
- Batches:
1072
- ${logData.batches
1073
- .map(
1074
- (b: any, i: number) =>
1075
- ` [${i + 1}] ${b.batchId} | ${b.recordCount} records | ${b.status}${b.error ? ` | Error: ${b.error}` : ''}`
1076
- )
1077
- .join('\n')}
1078
-
1079
- Summary:
1080
- Total Batches: ${logData.summary.totalBatches}
1081
- Successful: ${logData.summary.success}
1082
- Failed: ${logData.summary.failed}
1083
- Duration: ${logData.summary.duration}ms
1084
-
1085
- Status: ${logData.status}
1086
- `;
1087
- }
1088
- }
1089
- ```
1090
-
1091
- ---
1092
-
1093
- ### File: `src/workflows/multi-channel-sync.workflow.ts`
1094
-
1095
- ```typescript
1096
- import { Buffer } from 'node:buffer';
1097
- import {
1098
- createClient,
1099
- StateService,
1100
- VersoriKVAdapter,
1101
- JobTracker,
1102
- } from '@fluentcommerce/fc-connect-sdk';
1103
- import type { ChannelInventoryRecord, SyncStats } from '../types/multi-channel.types';
1104
- import { ATPCalculatorService } from '../services/atp-calculator.service';
1105
- import { ChannelAConnectorService } from '../services/channel-a-connector.service';
1106
- import { ChannelBConnectorService } from '../services/channel-b-connector.service';
1107
- import { BatchProcessorService } from '../services/batch-processor.service';
1108
- import { BatchLoggerService } from '../services/batch-logger.service';
1109
-
1110
- const INVENTORY_QUERY = `
1111
- query GetInventory($retailerId: ID!, $first: Int!, $after: String) {
1112
- inventoryPositions(retailerId: $retailerId, first: $first, after: $after) {
1113
- edges {
1114
- node {
1115
- id
1116
- ref
1117
- productRef
1118
- locationRef
1119
- onHand
1120
- reservedQuantity
1121
- status
1122
- type
1123
- }
1124
- cursor
1125
- }
1126
- pageInfo {
1127
- hasNextPage
1128
- endCursor
1129
- }
1130
- }
1131
- }
1132
- `;
1133
-
1134
- /**
1135
- * Fetch current inventory state from Fluent GraphQL
1136
- */
1137
- async function fetchFluentInventory(
1138
- client: any,
1139
- retailerId: string,
1140
- pageSize: number,
1141
- maxRecords: number,
1142
- log: any
1143
- ): Promise<any[]> {
1144
- try {
1145
- const result = await client.graphql({
1146
- query: INVENTORY_QUERY,
1147
- variables: { retailerId, first: pageSize },
1148
- pagination: { maxRecords },
1149
- });
1150
-
1151
- const edges = result.data?.inventoryPositions?.edges || [];
1152
- const records = edges.map((e: any) => ({
1153
- productRef: e.node.productRef,
1154
- locationRef: e.node.locationRef,
1155
- onHand: Number(e.node.onHand || 0),
1156
- reserved: Number(e.node.reservedQuantity || 0),
1157
- }));
1158
-
1159
- log.info(`[Fluent] Fetched ${records.length} records from GraphQL`);
1160
- return records;
1161
- } catch (error) {
1162
- // ✅ Enhanced error logging: Extract all error details for visibility
1163
- const errorDetails = {
1164
- message: error instanceof Error ? error.message : String(error),
1165
- stack: error instanceof Error ? error.stack : undefined,
1166
- errorType: error instanceof Error ? error.constructor.name : 'Error',
1167
- };
1168
- log.error('[Fluent] GraphQL fetch error:', errorDetails);
1169
- throw error;
1170
- }
1171
- }
1172
-
1173
- /**
1174
- * Main multi-channel sync workflow orchestrator
1175
- * Coordinates channel fetching, aggregation, delta detection, and batch submission
1176
- *
1177
- * @param ctx - Versori context object containing fetch, connections, log, activation, openKv
1178
- */
1179
- export async function processMultiChannelSync(ctx: any) {
1180
- const { log, openKv, activation } = ctx;
1181
- const startTime = Date.now();
1182
-
1183
- log.info('[MultiChannelSync] Starting sync workflow');
1184
-
1185
- try {
1186
- // ========================================
1187
- // CLIENT INITIALIZATION
1188
- // ========================================
1189
- const client = await createClient(ctx);
1190
-
1191
- // ========================================
1192
- // CONFIGURATION
1193
- // ========================================
1194
- const config = {
1195
- retailerId: activation?.getVariable('retailerId'),
1196
- jobName: activation?.getVariable('jobName') || 'Multi-Channel Inventory Sync',
1197
- batchSize: parseInt(activation?.getVariable('batchSize') || '500', 10),
1198
- maxRecords: parseInt(activation?.getVariable('maxRecords') || '50000', 10),
1199
- defaultBuffer: parseInt(activation?.getVariable('defaultBuffer') || '5', 10),
1200
- oversellProtection: activation?.getVariable('oversellProtection') !== 'false',
1201
- useBpp: activation?.getVariable('useBpp') || 'skip',
1202
- enableDelta: activation?.getVariable('enableDelta') !== 'false',
1203
- deltaStateKey: activation?.getVariable('deltaStateKey') || 'sync:delta:state',
1204
- channelAEnabled: activation?.getVariable('channelAEnabled') === 'true',
1205
- channelAUrl: activation?.getVariable('channelAUrl'),
1206
- channelAKey: activation?.getVariable('channelAKey'),
1207
- channelABuffer: parseInt(activation?.getVariable('channelABuffer') || '5', 10),
1208
- channelARateLimitRpm: parseInt(activation?.getVariable('channelARateLimitRpm') || '120', 10),
1209
- channelBEnabled: activation?.getVariable('channelBEnabled') === 'true',
1210
- channelBBucket: activation?.getVariable('channelBBucket'),
1211
- channelBKey: activation?.getVariable('channelBKey'),
1212
- fluentEnabled: activation?.getVariable('fluentEnabled') === 'true',
1213
- fluentPageSize: parseInt(activation?.getVariable('fluentPageSize') || '200', 10),
1214
- };
1215
-
1216
- // ========================================
1217
- // SERVICE INITIALIZATION
1218
- // ========================================
1219
- const kv = new VersoriKVAdapter(openKv(':project:'));
1220
- const jobTracker = new JobTracker(openKv(':project:'), log);
1221
- const stateService = new StateService(log);
1222
-
1223
- const atpCalc = new ATPCalculatorService(config.oversellProtection);
1224
- // ✅ PRODUCTION ENHANCEMENT: Pass log to BatchProcessorService for detailed progress tracking
1225
- const batchProcessor = new BatchProcessorService(client, jobTracker, log);
1226
-
1227
- const jobId = `multi-channel-sync-${Date.now()}`;
1228
- await jobTracker.createJob(jobId, {
1229
- triggeredBy: 'schedule',
1230
- stage: 'initialization',
1231
- details: { config },
1232
- });
1233
-
1234
- const stats: SyncStats = {
1235
- totalRecords: 0,
1236
- channelARecords: 0,
1237
- channelBRecords: 0,
1238
- fluentRecords: 0,
1239
- aggregatedSkus: 0,
1240
- changedRecords: 0,
1241
- batchesSent: 0,
1242
- successCount: 0,
1243
- errorCount: 0,
1244
- duration: 0,
1245
- };
1246
-
1247
- await jobTracker.updateJob(jobId, { status: 'fetching_channels' });
1248
-
1249
- // ========================================
1250
- // CHANNEL FETCHING (PARALLEL WITH GRACEFUL DEGRADATION)
1251
- // ========================================
1252
- const allRecords: ChannelInventoryRecord[] = [];
1253
-
1254
- // Fetch Channel A (REST API)
1255
- if (config.channelAEnabled && config.channelAUrl && config.channelAKey) {
1256
- log.info('[MultiChannelSync] Fetching from Channel A...');
1257
- const channelA = new ChannelAConnectorService(
1258
- config.channelAUrl,
1259
- config.channelAKey,
1260
- config.channelARateLimitRpm,
1261
- log
1262
- );
1263
-
1264
- try {
1265
- const records = await channelA.fetchInventory();
1266
- const mapped = records.map(r => ({
1267
- sku: r.product_id,
1268
- location: r.warehouse_code,
1269
- channel: 'A',
1270
- onHand: r.quantity_available,
1271
- reserved: r.quantity_reserved,
1272
- buffer: config.channelABuffer,
1273
- lastUpdated: r.updated_at,
1274
- }));
1275
-
1276
- allRecords.push(...mapped);
1277
- stats.channelARecords = mapped.length;
1278
- log.info(`[MultiChannelSync] Channel A: ${mapped.length} records`);
1279
- } catch (error) {
1280
- // ✅ Enhanced error logging: Extract all error details for visibility
1281
- const errorDetails = {
1282
- message: error instanceof Error ? error.message : String(error),
1283
- stack: error instanceof Error ? error.stack : undefined,
1284
- errorType: error instanceof Error ? error.constructor.name : 'Error',
1285
- };
1286
- log.error('[MultiChannelSync] Channel A fetch failed (continuing):', errorDetails);
1287
- stats.errorCount++;
1288
- }
1289
- }
1290
-
1291
- // Fetch Channel B (S3 CSV)
1292
- if (config.channelBEnabled && config.channelBBucket && config.channelBKey) {
1293
- log.info('[MultiChannelSync] Fetching from Channel B...');
1294
- const s3Config = {
1295
- type: 'S3_CSV',
1296
- connectionId: 'channel-b-s3',
1297
- name: 'Channel B S3',
1298
- s3Config: {
1299
- bucket: config.channelBBucket,
1300
- region: activation?.getVariable('awsRegion') || 'us-east-1',
1301
- accessKeyId: activation?.getVariable('awsAccessKeyId'),
1302
- secretAccessKey: activation?.getVariable('awsSecretAccessKey'),
1303
- },
1304
- };
1305
-
1306
- const channelB = new ChannelBConnectorService(
1307
- s3Config,
1308
- config.channelBBucket,
1309
- config.channelBKey,
1310
- log
1311
- );
1312
-
1313
- try {
1314
- const records = await channelB.fetchInventory();
1315
- const mapped = records.map((r: any) => ({
1316
- sku: r.sku,
1317
- location: r.location,
1318
- channel: 'B',
1319
- onHand: parseInt(r.qty, 10),
1320
- reserved: parseInt(r.reserved, 10),
1321
- buffer: config.defaultBuffer,
1322
- }));
1323
-
1324
- allRecords.push(...mapped);
1325
- stats.channelBRecords = mapped.length;
1326
- log.info(`[MultiChannelSync] Channel B: ${mapped.length} records`);
1327
- } catch (error) {
1328
- // ✅ Enhanced error logging: Extract all error details for visibility
1329
- const errorDetails = {
1330
- message: error instanceof Error ? error.message : String(error),
1331
- stack: error instanceof Error ? error.stack : undefined,
1332
- errorType: error instanceof Error ? error.constructor.name : 'Error',
1333
- };
1334
- log.error('[MultiChannelSync] Channel B fetch failed (continuing):', errorDetails);
1335
- stats.errorCount++;
1336
- }
1337
- }
1338
-
1339
- // Fetch Fluent GraphQL (current state)
1340
- if (config.fluentEnabled) {
1341
- log.info('[MultiChannelSync] Fetching from Fluent GraphQL...');
1342
- try {
1343
- const records = await fetchFluentInventory(
1344
- client,
1345
- config.retailerId!,
1346
- config.fluentPageSize,
1347
- config.maxRecords,
1348
- log
1349
- );
1350
-
1351
- const mapped = records.map(r => ({
1352
- sku: r.productRef,
1353
- location: r.locationRef,
1354
- channel: 'FLUENT',
1355
- onHand: r.onHand,
1356
- reserved: r.reserved,
1357
- buffer: 0,
1358
- }));
1359
-
1360
- allRecords.push(...mapped);
1361
- stats.fluentRecords = mapped.length;
1362
- log.info(`[MultiChannelSync] Fluent: ${mapped.length} records`);
1363
- } catch (error) {
1364
- // ✅ Enhanced error logging: Extract all error details for visibility
1365
- const errorDetails = {
1366
- message: error instanceof Error ? error.message : String(error),
1367
- stack: error instanceof Error ? error.stack : undefined,
1368
- errorType: error instanceof Error ? error.constructor.name : 'Error',
1369
- };
1370
- log.error('[MultiChannelSync] Fluent fetch failed (continuing):', errorDetails);
1371
- stats.errorCount++;
1372
- }
1373
- }
1374
-
1375
- stats.totalRecords = allRecords.length;
1376
-
1377
- if (allRecords.length === 0) {
1378
- log.warn('[MultiChannelSync] No inventory records fetched from any channel');
1379
- await jobTracker.markCompleted(jobId, { message: 'No data', stats });
1380
- return { success: false, message: 'No data', stats };
1381
- }
1382
-
1383
- // ========================================
1384
- // AGGREGATION
1385
- // ========================================
1386
- await jobTracker.updateJob(jobId, { status: 'aggregating' });
1387
-
1388
- log.info('[MultiChannelSync] Aggregating inventory across channels...');
1389
- const aggregated = atpCalc.aggregateChannelInventory(allRecords);
1390
- stats.aggregatedSkus = aggregated.size;
1391
-
1392
- const finalInventory = Array.from(aggregated.values()).map(agg => ({
1393
- skuRef: agg.sku,
1394
- locationRef: agg.location,
1395
- qty: agg.atp,
1396
- type: 'AVAILABLE',
1397
- status: 'ACTIVE',
1398
- expectedOn: new Date().toISOString().split('T')[0],
1399
- }));
1400
-
1401
- log.info(`[MultiChannelSync] Aggregated ${stats.aggregatedSkus} unique SKU/location combinations`);
1402
-
1403
- // ========================================
1404
- // DELTA DETECTION
1405
- // ========================================
1406
- let recordsToSend = finalInventory;
1407
-
1408
- if (config.enableDelta) {
1409
- await jobTracker.updateJob(jobId, { status: 'delta_detection' });
1410
- log.info('[MultiChannelSync] Checking for changes (delta detection)...');
1411
-
1412
- const prevState = ((await stateService.getState(config.deltaStateKey)) as any) || {};
1413
- const changedRecords = [];
1414
-
1415
- for (const record of finalInventory) {
1416
- const prevQty = prevState[record.skuRef]?.[record.locationRef];
1417
- if (prevQty === undefined || prevQty !== record.qty) {
1418
- changedRecords.push(record);
1419
- }
1420
- }
1421
-
1422
- recordsToSend = changedRecords;
1423
- stats.changedRecords = changedRecords.length;
1424
- log.info(
1425
- `[MultiChannelSync] Delta detection: ${changedRecords.length} changed records (${finalInventory.length} total)`
1426
- );
1427
- } else {
1428
- stats.changedRecords = finalInventory.length;
1429
- }
1430
-
1431
- if (recordsToSend.length === 0) {
1432
- log.info('[MultiChannelSync] No changes detected, skipping batch send');
1433
- stats.duration = Date.now() - startTime;
1434
- await jobTracker.markCompleted(jobId, { message: 'No changes', stats });
1435
- return { success: true, message: 'No changes', stats };
1436
- }
1437
-
1438
- // ========================================
1439
- // BATCH API SUBMISSION
1440
- // ========================================
1441
- await jobTracker.updateJob(jobId, { status: 'creating_batch_job' });
1442
-
1443
- log.info('[MultiChannelSync] Creating Batch API job...');
1444
- const job = await client.createJob({
1445
- name: config.jobName,
1446
- retailerId: config.retailerId!,
1447
- meta: {
1448
- preprocessing: config.useBpp,
1449
- },
1450
- });
1451
-
1452
- log.info(`[MultiChannelSync] Job created: ${job.id}`);
1453
-
1454
- await jobTracker.updateJob(jobId, { status: 'sending_batches' });
1455
-
1456
- // ? Enhanced: Extract context for progress logging
1457
- const uniqueLocations = [...new Set(recordsToSend.map((r: any) => r.locationRef))];
1458
- const sampleSKUs = recordsToSend.slice(0, 5).map((r: any) => r.skuRef);
1459
- const estimatedBatches = Math.ceil(recordsToSend.length / config.batchSize);
1460
-
1461
- // ? Enhanced: Start logging with context
1462
- log.info(`[BatchProcessor] Sending batches for multi-channel sync`, {
1463
- totalRecords: recordsToSend.length,
1464
- estimatedBatches,
1465
- batchSize: config.batchSize,
1466
- locations: uniqueLocations.join(', '),
1467
- sampleSKUs: sampleSKUs.join(', '),
1468
- jobId: job.id
1469
- });
1470
-
1471
- const batchResults = await batchProcessor.sendInventoryBatches(
1472
- job.id,
1473
- recordsToSend,
1474
- config.batchSize
1475
- );
1476
-
1477
- // ✅ Logging handled in workflow with Versori native log
1478
- log.info(`[BatchProcessor] Sent ${batchResults.batchCount} batches`, {
1479
- jobId: job.id,
1480
- totalRecords: batchResults.totalSent,
1481
- successfulBatches: batchResults.batches.filter(b => b.status === 'SENT').length,
1482
- failedBatches: batchResults.batches.filter(b => b.status === 'FAILED').length,
1483
- });
1484
-
1485
- // ? Enhanced: Completion logging with summary
1486
- log.info(`[BatchProcessor] Batch submission completed for multi-channel sync`, {
1487
- totalBatches: batchResults.batchCount,
1488
- totalRecords: batchResults.totalSent,
1489
- successfulBatches: batchResults.batches.filter(b => b.status === 'SENT').length,
1490
- failedBatches: batchResults.errors.length,
1491
- jobId: job.id
1492
- });
1493
-
1494
- if (batchResults.errors.length > 0) {
1495
- log.warn(`[BatchProcessor] ${batchResults.errors.length} batches failed`, {
1496
- jobId: job.id,
1497
- errors: batchResults.errors.slice(0, 5), // Log first 5 errors
1498
- });
1499
- }
1500
-
1501
- stats.batchesSent = batchResults.batchCount;
1502
- stats.successCount = batchResults.batches.filter(b => b.status === 'SENT').length;
1503
- stats.errorCount = batchResults.errors.length;
1504
-
1505
- // ========================================
1506
- // DELTA STATE UPDATE
1507
- // ========================================
1508
- if (config.enableDelta) {
1509
- await jobTracker.updateJob(jobId, { status: 'updating_delta_state' });
1510
- log.info('[MultiChannelSync] Updating delta state...');
1511
-
1512
- const newState: any = {};
1513
- for (const record of finalInventory) {
1514
- if (!newState[record.skuRef]) {
1515
- newState[record.skuRef] = {};
1516
- }
1517
- newState[record.skuRef][record.locationRef] = record.qty;
1518
- }
1519
-
1520
- await stateService.setState(config.deltaStateKey, newState, {
1521
- ttlSeconds: 7 * 24 * 60 * 60, // 7 days
1522
- });
1523
-
1524
- log.info('[MultiChannelSync] Delta state updated');
1525
- }
1526
-
1527
- stats.duration = Date.now() - startTime;
1528
-
1529
- log.info('[MultiChannelSync] Sync completed', { stats });
1530
-
1531
- await jobTracker.markCompleted(jobId, {
1532
- stats,
1533
- jobId: job.id,
1534
- });
1535
-
1536
- return {
1537
- success: stats.errorCount === 0,
1538
- jobId: job.id,
1539
- stats,
1540
- };
1541
- } catch (error: any) {
1542
- // ✅ Enhanced error logging: Extract all error details for visibility
1543
- const errorDetails = {
1544
- message: error?.message || 'Unknown error',
1545
- stack: error?.stack,
1546
- fileName: error?.fileName,
1547
- lineNumber: error?.lineNumber,
1548
- originalError: error?.context?.originalError?.message,
1549
- errorType: error?.name || 'Error',
1550
- };
1551
- log.error('[MultiChannelSync] Fatal error:', errorDetails);
1552
- return { success: false, error: error.message, duration: Date.now() - startTime };
1553
- }
1554
- }
1555
- ```
1556
-
1557
- ---
1558
-
1559
- ## Versori Activation Variables
1560
-
1561
- ```bash
1562
- # Required Variables
1563
- retailerId=your-retailer-id
1564
-
1565
- # Sync Configuration
1566
- jobName=Multi-Channel Inventory Sync
1567
- batchSize=500
1568
- maxRecords=50000
1569
- defaultBuffer=5
1570
- oversellProtection=true
1571
- useBpp=skip
1572
-
1573
- # Delta Detection
1574
- enableDelta=true
1575
- deltaStateKey=sync:delta:state
1576
-
1577
- # Channel A (REST API)
1578
- channelAEnabled=true
1579
- channelAUrl=https://api.channel-a.example.com/inventory
1580
- channelAKey=your-api-key
1581
- channelABuffer=5
1582
- channelARateLimitRpm=120
1583
-
1584
- # Channel B (S3 CSV)
1585
- channelBEnabled=true
1586
- channelBBucket=channel-b-inventory
1587
- channelBKey=inventory/current.csv
1588
-
1589
- # AWS Credentials (for Channel B S3)
1590
- awsRegion=us-east-1
1591
- awsAccessKeyId=AKIAXXXXXXXXXXXX
1592
- awsSecretAccessKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
1593
-
1594
- # Fluent GraphQL (current state comparison)
1595
- fluentEnabled=true
1596
- fluentPageSize=200
1597
- ```
1598
-
1599
- ---
1600
-
1601
- ## Sample Channel Data
1602
-
1603
- ### Channel A (REST API Response)
1604
-
1605
- ```json
1606
- {
1607
- "inventory": [
1608
- {
1609
- "product_id": "SKU-12345",
1610
- "warehouse_code": "LOC001",
1611
- "quantity_available": 100,
1612
- "quantity_reserved": 20,
1613
- "updated_at": "2025-01-25T10:00:00Z"
1614
- }
1615
- ]
1616
- }
1617
- ```
1618
-
1619
- ### Channel B (S3 CSV)
1620
-
1621
- ```csv
1622
- sku,location,qty,reserved
1623
- SKU-12345,LOC001,95,15
1624
- SKU-67890,LOC002,75,5
1625
- SKU-11111,LOC001,200,0
1626
- ```
1627
-
1628
- ### ATP Calculation Example
1629
-
1630
- ```typescript
1631
- // Channel A: SKU-12345 at LOC001
1632
- onHand = 100
1633
- reserved = 20
1634
- buffer = 5
1635
- ATP = (100 - 20) - 5 = 75
1636
-
1637
- // Channel B: SKU-12345 at LOC001
1638
- onHand = 95
1639
- reserved = 15
1640
- buffer = 5
1641
- ATP = (95 - 15) - 5 = 75
1642
-
1643
- // Aggregated: SKU-12345 at LOC001
1644
- totalOnHand = 100 + 95 = 195
1645
- totalReserved = 20 + 15 = 35
1646
- maxBuffer = max(5, 5) = 5
1647
- finalATP = (195 - 35) - 5 = 155
1648
- ```
1649
-
1650
- ---
1651
-
1652
- ## Deployment
1653
-
1654
- ```bash
1655
- # Install dependencies
1656
- npm install
1657
-
1658
- # Validate configuration
1659
- npm run lint
1660
-
1661
- # Deploy to Versori
1662
- versori deploy
1663
-
1664
- # View logs
1665
- versori logs multi-channel-inventory-sync
1666
-
1667
- # Trigger manual sync
1668
- versori run adhocMultiChannelSync
1669
- ```
1670
-
1671
- ---
1672
-
1673
- ## Testing
1674
-
1675
- ### Test Scheduled Sync
1676
-
1677
- Upload test CSV files to S3/SFTP for each channel and wait for the scheduled run.
1678
-
1679
- **Check logs:**
1680
-
1681
- ```
1682
- [STEP 1/8] Initializing job tracking
1683
- [STEP 2/8] Initializing Fluent Commerce client and data sources
1684
- [STEP 3/8] Discovering files across channels
1685
- [CHANNEL 1/3] Processing channel: CHANNEL_A
1686
- [FILE 1/1] Processing file: channel-a-inventory_20250124.csv
1687
- [STEP 4/8] Downloading and parsing: channel-a-inventory_20250124.csv
1688
- [STEP 5/8] Transforming 5000 inventory records from channel-a-inventory_20250124.csv
1689
- [STEP 6/8] Creating batch job and sending 5 batches to Fluent Commerce
1690
- [STEP 7/8] Archiving file: channel-a-inventory_20250124.csv
1691
- [STEP 8/8] Completing job and calculating totals
1692
- ```
1693
-
1694
- ### Test Ad hoc Sync
1695
-
1696
- ```bash
1697
- # Sync all channels
1698
- curl -X POST https://api.versori.com/webhooks/multi-channel-sync-adhoc \
1699
- -H "Content-Type: application/json" \
1700
- -d '{}'
1701
-
1702
- # Sync specific channel
1703
- curl -X POST https://api.versori.com/webhooks/multi-channel-sync-adhoc \
1704
- -H "Content-Type: application/json" \
1705
- -d '{
1706
- "channelId": "CHANNEL_A"
1707
- }'
1708
-
1709
- # Sync with specific pattern
1710
- curl -X POST https://api.versori.com/webhooks/multi-channel-sync-adhoc \
1711
- -H "Content-Type: application/json" \
1712
- -d '{
1713
- "filePattern": "urgent_*.csv",
1714
- "channelId": "CHANNEL_B"
1715
- }'
1716
- ```
1717
-
1718
- ### Test Job Status Query
1719
-
1720
- ```bash
1721
- curl -X POST https://api.versori.com/webhooks/multi-channel-sync-job-status \
1722
- -H "Content-Type: application/json" \
1723
- -d '{
1724
- "jobId": "ADHOC_MULTI_20251024_183045_abc123"
1725
- }'
1726
- ```
1727
-
1728
- ### Verify Batch Jobs in Fluent
1729
-
1730
- After processing, check the Batch job status for each channel in Fluent Commerce:
1731
-
1732
- ```bash
1733
- # Query job status via GraphQL
1734
- curl -X POST https://your-fluent-instance.com/graphql \
1735
- -H "Authorization: Bearer YOUR_TOKEN" \
1736
- -H "Content-Type: application/json" \
1737
- -d '{
1738
- "query": "query { job(id: \"job-123456\") { id status recordCount processedCount } }"
1739
- }'
1740
- ```
1741
-
1742
- ---
1743
-
1744
- ## Monitoring
1745
-
1746
- ### Success Response
1747
-
1748
- ```json
1749
- {
1750
- "success": true,
1751
- "channelsProcessed": 3,
1752
- "channelsFailed": 0,
1753
- "filesProcessed": 3,
1754
- "filesSkipped": 0,
1755
- "filesFailed": 0,
1756
- "results": [
1757
- {
1758
- "channel": "CHANNEL_A",
1759
- "file": "channel-a-inventory_2025-01-22.csv",
1760
- "success": true,
1761
- "recordCount": 5000,
1762
- "batchCount": 5,
1763
- "jobId": "job-123456",
1764
- "duration": 12345
1765
- },
1766
- {
1767
- "channel": "CHANNEL_B",
1768
- "file": "channel-b-inventory_2025-01-22.csv",
1769
- "success": true,
1770
- "recordCount": 3000,
1771
- "batchCount": 3,
1772
- "jobId": "job-123457",
1773
- "duration": 9876
1774
- },
1775
- {
1776
- "channel": "CHANNEL_C",
1777
- "file": "channel-c-inventory_2025-01-22.csv",
1778
- "success": true,
1779
- "recordCount": 2000,
1780
- "batchCount": 2,
1781
- "jobId": "job-123458",
1782
- "duration": 8765
1783
- }
1784
- ],
1785
- "duration": 13456
1786
- }
1787
- ```
1788
-
1789
- ### Partial Success Response
1790
-
1791
- ```json
1792
- {
1793
- "success": true,
1794
- "channelsProcessed": 2,
1795
- "channelsFailed": 1,
1796
- "filesProcessed": 2,
1797
- "filesSkipped": 0,
1798
- "filesFailed": 1,
1799
- "results": [
1800
- {
1801
- "channel": "CHANNEL_A",
1802
- "file": "channel-a-inventory_2025-01-22.csv",
1803
- "success": true,
1804
- "recordCount": 5000,
1805
- "batchCount": 5,
1806
- "jobId": "job-123456",
1807
- "duration": 12345
1808
- },
1809
- {
1810
- "channel": "CHANNEL_B",
1811
- "file": "channel-b-inventory_2025-01-22.csv",
1812
- "success": false,
1813
- "error": "CSV parse error: Invalid structure"
1814
- }
1815
- ],
1816
- "duration": 13456
1817
- }
1818
- ```
1819
-
1820
- ### Error Response
1821
-
1822
- ```json
1823
- {
1824
- "success": false,
1825
- "channelsProcessed": 0,
1826
- "channelsFailed": 3,
1827
- "filesProcessed": 0,
1828
- "filesFailed": 3,
1829
- "results": [
1830
- {
1831
- "channel": "CHANNEL_A",
1832
- "file": "channel-a-inventory_2025-01-22.csv",
1833
- "success": false,
1834
- "error": "Data source connection failed"
1835
- }
1836
- ],
1837
- "duration": 876
1838
- }
1839
- ```
1840
-
1841
- ### Monitoring Metrics
1842
-
1843
- Monitor these metrics via Versori logs filtered by `jobId` or `stage`:
1844
-
1845
- - **Channels Processed** - Total channels successfully processed
1846
- - **Files Processed** - Total files successfully processed across all channels
1847
- - **Batch Jobs Created** - Total Batch jobs created in Fluent Commerce (one per channel)
1848
- - **Processing Duration** - Time taken for complete multi-channel sync
1849
- - **Channel Failures** - Channels that failed (check individual channel errors)
1850
-
1851
- Use the status webhook for dashboards and automated monitoring.
1852
-
1853
- ---
1854
-
1855
- - 🎯 **TRUE modular architecture** - Separate service files with clear responsibilities
1856
- - 🎯 **Graceful degradation** - Use `Promise.allSettled()` for partial channel failures
1857
- - 🎯 **Delta detection** - Only sync changed records (reduces API load by 90%+)
1858
- - 🎯 **External JSON mapping** - Use `with { type: 'json' }` import syntax
1859
- - 🎯 **ATP calculation** - `ATP = (onHand - reserved) - buffer` with oversell protection
1860
- - 🎯 **Rate limiting** - Enforce minimum interval between Channel A requests
1861
- - 🎯 **Skip BPP** - Set `preprocessing: 'skip'` when using delta detection
1862
- - 🎯 **Job tracking** - Use `JobTracker` for lifecycle management
1863
- - 🎯 **Native logging** - Use `log` from context on Versori platform
1864
- - 🎯 **EntityType: INVENTORY** - Correct entity type for inventory records
1865
- - 🎯 **Error handling** - Log channel failures but don't block entire sync
1866
-
1867
- ---
1868
-
1869
- ## Related Documentation
1870
-
1871
- - **Single-source batch ingestion**: [SFTP XML Inventory Batch Template](./template-ingestion-sftp-xml-inventory-batch.md) - Simpler pattern for single data sources (GOLD STANDARD)
1872
- - [Batch API Guide](../../../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md) - Complete Batch API patterns and BPP documentation
1873
- - [State Management](../../../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md) - VersoriKVAdapter and StateService usage
1874
- - [Job Tracker](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md) - Job lifecycle tracking
1875
- - [Universal Mapping](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - Field transformation guide
1876
- - [Error Handling Patterns](../../../../../03-PATTERN-GUIDES/error-handling/error-handling-readme.md) - Retry logic and exponential backoff
1877
- - [File Operations](../../../../../03-PATTERN-GUIDES/file-operations/file-operations-readme.md) - KV state management patterns
1878
- - [GraphQL Extraction](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - Auto-pagination for Fluent inventory queries
1879
-
1880
- ---
1881
-
1882
- [→ Back to Versori Scheduled Workflows](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) | [Versori Platform Guide →](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)
1
+ ---
2
+ template_id: tpl-ingest-multi-channel-inventory-sync
3
+ canonical_filename: template-ingestion-multi-channel-inventory-sync.md
4
+ sdk_version: latest
5
+ runtime: versori
6
+ direction: ingestion
7
+ source: multi-channel-rest-s3
8
+ destination: fluent-batch-api
9
+ entity: inventory
10
+ format: json-csv
11
+ logging: versori
12
+ status: stable
13
+ ---
14
+
15
+ # Template: Ingestion - Multi-Channel Inventory Sync (Scheduled)
16
+
17
+ ## STEP 1: Understand This Template
18
+
19
+ **What This Template Does:**
20
+
21
+ - Scheduled Versori workflow for multi-channel inventory aggregation and sync
22
+ - Fetches inventory from multiple sources in parallel (REST API, S3 CSV, Fluent GraphQL)
23
+ - Calculates channel-specific ATP (Available To Promise) with configurable buffers and caps
24
+ - Performs delta detection using VersoriKVAdapter to sync only changed inventory records
25
+ - Handles partial channel failures gracefully with Promise.allSettled
26
+ - Rate-limits external REST API calls with exponential backoff retry logic
27
+ - Sends consolidated inventory updates to Fluent Commerce Batch API with chunking
28
+ - Tracks job lifecycle with JobTracker for operational monitoring
29
+ - Supports scheduled (cron) and ad-hoc (webhook) triggers
30
+
31
+ **Key SDK Components:**
32
+
33
+ - `createClient()` - Universal client factory (auto-detects Versori context)
34
+ - `S3DataSource` - S3 file operations with retry logic
35
+ - `CSVParserService` - CSV parsing with validation
36
+ - `UniversalMapper` - Field transformation with SDK resolvers
37
+ - `StateService` + `VersoriKVAdapter` - Delta detection state management
38
+ - `JobTracker` - Job lifecycle tracking - `FluentClient.createJob()` - Create Batch API job
39
+ - `FluentClient.sendBatch()` - Send inventory chunks (fire-and-forget)
40
+ - `FluentClient.graphql()` - Query current Fluent inventory state with auto-pagination
41
+ - Native Versori `log` - Use `log` from context
42
+ **Entity Type:**
43
+
44
+ - **InventoryQuantity** - Fluent entity for inventory positions and quantities
45
+ - **EntityType: 'INVENTORY'** - Used in Batch API `sendBatch()` call
46
+ - **Batch API Method** - Uses `createJob()` and `sendBatch()` (not Event API)
47
+
48
+ **Critical Patterns:**
49
+
50
+ - **Multi-source aggregation**: Use `Promise.allSettled()` for parallel channel fetching
51
+ - **Graceful degradation**: Continue processing even if one channel fails
52
+ - **Delta detection**: StateService tracks previous state to detect changes
53
+ - **Progress Logging**: Enhanced logging with context (sample SKUs, locations)
54
+ - **JobTracker Progress Updates**: Periodic progress updates during batch processing
55
+ - **ATP calculation**: `ATP = (onHand - reserved) - buffer` with oversell protection
56
+ - **Delta detection**: Use `StateService` + `VersoriKVAdapter` to track previous ATP values
57
+ - **Rate limiting**: Enforce minimum interval between channel API requests
58
+ - **Safe configuration**: External JSON mapping file (not inline TypeScript)
59
+ - **BPP Configuration**: Use `'skip'` (delta detection already filters changes)
60
+ - **Fire-and-forget batches**: Batch submission is asynchronous (no polling needed)
61
+
62
+ **When to Use This Template:**
63
+
64
+ - ✅ Multiple inventory sources (3+ channels) requiring aggregation
65
+ - ✅ Channel-specific ATP calculations with buffers and caps
66
+ - ✅ Delta detection needed (only sync changed records)
67
+ - ✅ Partial channel failures shouldn't block entire sync
68
+ - ✅ External APIs require rate limiting and retry logic
69
+ - ✅ Scheduled batch processing (every 15 minutes, hourly, etc.)
70
+
71
+ **When NOT to Use:**
72
+
73
+ - ❌ Single inventory source (use simpler single-source templates)
74
+ - ❌ Real-time sync required (this is designed for scheduled batch processing)
75
+ - ❌ No ATP calculations needed (use direct field mapping)
76
+ - ❌ Don't need delta detection (full snapshots every time)
77
+ - ❌ Products, Locations, Customers (use Event API templates)
78
+
79
+ ---
80
+
81
+ ## STEP 2: AI Prompt
82
+
83
+ **Copy this prompt to generate the complete implementation:**
84
+
85
+ ```
86
+ Create a Versori scheduled workflow for multi-channel inventory aggregation to Fluent Commerce Batch API.
87
+
88
+ REQUIREMENTS:
89
+ 1. Runtime: Versori Platform (scheduled workflow)
90
+ 2. Sources: Multiple channels (REST API, S3 CSV, Fluent GraphQL)
91
+ 3. Destination: Fluent Commerce Batch API (InventoryQuantity entity)
92
+ 4. Entity: InventoryQuantity (EntityType: 'INVENTORY')
93
+
94
+ KEY FEATURES:
95
+ 1. **Multi-channel fetching** (parallel):
96
+ - **Channel A**: REST API with rate limiting (120 RPM) and retry logic
97
+ - **Channel B**: S3 CSV file with CSVParserService
98
+ - **Fluent GraphQL**: Current inventory state via auto-pagination
99
+ 2. **ATP (Available To Promise) calculation**:
100
+ - Formula: `ATP = (onHand - reserved) - buffer`
101
+ - Channel-specific buffers (configurable per channel)
102
+ - Aggregate ATP across channels with deduplication by SKU + location
103
+ - Support oversell protection (ensure ATP ≥ 0)
104
+ 3. **Delta detection**:
105
+ - Use `StateService` + `VersoriKVAdapter` to track previous ATP values
106
+ - Only send changed records to Batch API
107
+ - Store state with 7-day TTL in Versori KV
108
+ 4. **Graceful degradation**:
109
+ - Use `Promise.allSettled()` for channel fetching
110
+ - Continue processing even if one channel fails
111
+ - Log channel failures but don't block entire sync
112
+ 5. **Batch API**:
113
+ - Entity: `InventoryQuantity`
114
+ - EntityType: `'INVENTORY'`
115
+ - Action: `'UPSERT'`
116
+ - BPP: `'skip'` (delta detection already filters)
117
+ - Batch size: 500 records per chunk
118
+ - Poll batches until complete with exponential backoff
119
+ 6. **Job tracking**:
120
+ - Use `JobTracker` with Versori KV storage
121
+ - Track status transitions: `fetching_channels` → `aggregating` → `delta_detection` → `creating_batch_job` → `sending_batches` → `polling_batches` → `updating_delta_state`
122
+ 7. **Modular architecture**:
123
+ - Separate service files (ATP calculator, channel connectors, batch dispatcher)
124
+ - External JSON mapping config
125
+ - Clean separation of concerns
126
+
127
+ CRITICAL REQUIREMENTS:
128
+ 1. Modular architecture: Separate service files (ATP calculator, channel connectors, batch dispatcher)
129
+ 2. External JSON mapping config: Use `with { type: 'json' }` import syntax
130
+ 3. Native logging: Use log from context (LoggingService removed - use native log)
131
+ 4. Graceful degradation: Promise.allSettled for parallel channel fetching
132
+ 5. Delta detection: Track previous ATP values in Versori KV
133
+ 6. BPP: Set to 'skip' (delta already filters changes)
134
+ 7. Rate limiting: Enforce minimum interval between Channel A requests
135
+ 8. Job tracking: Use JobTracker for lifecycle management
136
+
137
+ SDK METHODS TO USE:
138
+ - createClient(ctx) - Pass entire Versori context, auto-detects platform
139
+ - new S3DataSource(config, log) - S3 file operations
140
+ - new CSVParserService() - Parse CSV files
141
+ - await client.graphql({ query, variables, pagination }) - Fluent GraphQL extraction with auto-pagination
142
+ - new VersoriKVAdapter(openKv(':project:')) - Versori KV storage adapter
143
+ - new StateService(kvAdapter) - Delta state management (takes KV adapter, not logger)
144
+ - new JobTracker(kv, log) - Job lifecycle tracking
145
+ - await client.createJob({ name, retailerId, meta: { preprocessing: 'skip' } }) - Create Batch API job
146
+ - await client.sendBatch(jobId, { action, entityType, source, event, entities }) - Send batch chunk (fire-and-forget)
147
+
148
+ FORBIDDEN PATTERNS:
149
+ - ❌ LoggingService (removed - use native log on Versori)
150
+ - ❌ Don't use monolithic index.ts (extract services into separate files)
151
+ - ❌ Don't use inline mapping config (use external JSON file)
152
+ - ❌ Don't fail entire sync if one channel fails (use Promise.allSettled)
153
+ - ❌ Don't send all records (use delta detection to filter unchanged)
154
+ - ❌ Don't use BPP with deltas (set preprocessing: 'skip')
155
+ - ❌ Don't forget rate limiting for external APIs
156
+ - ❌ Don't forget to call dispose() on data sources in finally block
157
+
158
+ GENERATE:
159
+ 1. package.json with dependencies
160
+ 2. index.ts (workflow entry point with scheduled/adhoc/status triggers)
161
+ 3. src/workflows/multi-channel-sync.workflow.ts (main orchestration logic)
162
+ 4. src/services/atp-calculator.service.ts (ATP calculation and aggregation)
163
+ 5. src/services/channel-a-connector.service.ts (REST API with rate limiting)
164
+ 6. src/services/channel-b-connector.service.ts (S3 CSV fetching)
165
+ 7. src/services/batch-processor.service.ts (Batch API submission)
166
+ 8. src/services/batch-logger.service.ts (SFTP log file writing)
167
+ 9. src/types/multi-channel.types.ts (TypeScript interfaces)
168
+ 10. config/multi-channel.mapping.json (mapping configuration - external JSON file)
169
+
170
+ NOTE: Use external JSON files for mapping configuration (not TypeScript .config files)
171
+
172
+ Ensure all code is production-ready with proper error handling, graceful degradation, and rate limiting. Use modular architecture with separate service files for each concern.
173
+ ```
174
+
175
+ ---
176
+
177
+ ## What You'll Build
178
+
179
+ ### Versori Workflows Structure
180
+
181
+ **Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
182
+
183
+ **Trigger Types:**
184
+ - **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
185
+ - **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
186
+ - **`workflow()`** → Durable workflows (advanced, rarely used)
187
+
188
+ **Execution Steps (chained to triggers):**
189
+ - **`http()`** → External API calls (chained from schedule/webhook)
190
+ - **`fn()`** → Internal processing (chained from schedule/webhook)
191
+
192
+ ### Recommended Project Structure
193
+
194
+ ```
195
+ inventory-batch-sync/
196
+ ├── index.ts # Entry point - exports all workflows
197
+ └── src/
198
+ ├── workflows/
199
+ │ ├── scheduled/
200
+ │ │ └── daily-inventory-sync.ts # Scheduled: Daily inventory sync
201
+ │ │
202
+ │ └── webhook/
203
+ │ ├── adhoc-inventory-sync.ts # Webhook: Manual trigger
204
+ │ └── job-status-check.ts # Webhook: Status query
205
+
206
+ ├── services/
207
+ │ └── inventory-sync.service.ts # Shared orchestration logic (reusable)
208
+
209
+ └── types/
210
+ └── inventory.types.ts # Shared type definitions
211
+ ```
212
+
213
+ **Benefits:**
214
+ - ✅ Clear trigger separation (`scheduled/` vs `webhook/`)
215
+ - ✅ Descriptive file names (easy to browse and understand)
216
+ - ✅ Scalable (add new workflows without cluttering)
217
+ - ✅ Reusable code in `services/` (DRY principle)
218
+ - ✅ Easy to modify individual workflows without affecting others
219
+
220
+ ---
221
+
222
+ ## Workflow Files
223
+
224
+ ### 1. Scheduled Workflows (`src/workflows/scheduled/`)
225
+
226
+ All time-based triggers that run automatically on cron schedules.
227
+
228
+ #### `src/workflows/scheduled/daily-inventory-sync.ts`
229
+
230
+ **Purpose**: Automatic Daily inventory sync
231
+ **Trigger**: Cron schedule (`0 2 * * *`)
232
+ **Exposed as Endpoint**: ❌ NO - Runs automatically
233
+
234
+ ```typescript
235
+ import { schedule, http } from '@versori/run';
236
+ import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
237
+ import { runIngestion } from '../../services/inventory-sync.service.ts';
238
+
239
+ /**
240
+ * Scheduled Workflow: Daily Inventory Sync
241
+ *
242
+ * Runs automatically daily at 2 AM UTC
243
+ * NOT exposed as HTTP endpoint - Versori executes on schedule
244
+ *
245
+ * Uses shared service: inventory-sync.service.ts
246
+ */
247
+ export const daily_inventory_sync = schedule(
248
+ 'inventory-batch-scheduled',
249
+ '0 2 * * *' // Daily at 2 AM UTC
250
+ ).then(
251
+ http('run-inventory-batch', { connection: 'fluent_commerce' }, async ctx => {
252
+ const { log, openKv } = ctx;
253
+ const jobId = `inventory-batch-${Date.now()}`;
254
+ const tracker = new JobTracker(openKv(':project:'), log);
255
+
256
+ await tracker.createJob(jobId, { triggeredBy: 'schedule', stage: 'initialization' });
257
+ await tracker.updateJob(jobId, { status: 'processing' });
258
+
259
+ try {
260
+ // Reuse shared orchestration logic
261
+ const result = await runIngestion(ctx, jobId, tracker);
262
+ await tracker.markCompleted(jobId, result);
263
+ return { success: true, jobId, ...result };
264
+ } catch (e: any) {
265
+ await tracker.markFailed(jobId, e);
266
+ return { success: false, jobId, error: e?.message };
267
+ }
268
+ })
269
+ );
270
+ ```
271
+
272
+ ---
273
+
274
+ ### 2. Webhook Workflows (`src/workflows/webhook/`)
275
+
276
+ All HTTP-based triggers that create webhook endpoints.
277
+
278
+ #### `src/workflows/webhook/adhoc-inventory-sync.ts`
279
+
280
+ **Purpose**: Manual inventory sync trigger (on-demand)
281
+ **Trigger**: HTTP POST
282
+ **Endpoint**: `POST https://{workspace}.versori.run/inventory-batch-adhoc`
283
+ **Use Cases**: Testing, priority processing, ad-hoc runs
284
+
285
+ ```typescript
286
+ import { webhook, http } from '@versori/run';
287
+ import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
288
+ import { runIngestion } from '../../services/inventory-sync.service.ts';
289
+
290
+ /**
291
+ * Webhook: Manual Inventory Sync Trigger
292
+ *
293
+ * Endpoint: POST https://{workspace}.versori.run/inventory-batch-adhoc
294
+ * Request body (optional): { filePattern: "urgent_*.xml", maxFiles: 5 }
295
+ *
296
+ * Pattern: webhook().then(http()) - needs Fluent API access
297
+ * Uses shared service: inventory-sync.service.ts
298
+ */
299
+ export const adhoc_inventory_sync = webhook('inventory-batch-adhoc', {
300
+ response: { mode: 'sync' },
301
+ connection: 'inventory-batch-adhoc', // Versori validates API key
302
+ }).then(
303
+ http('run-inventory-batch-adhoc', { connection: 'fluent_commerce' }, async ctx => {
304
+ const { log, openKv, data } = ctx;
305
+ const jobId = `inventory-batch-adhoc-${Date.now()}`;
306
+ const tracker = new JobTracker(openKv(':project:'), log);
307
+
308
+ await tracker.createJob(jobId, {
309
+ triggeredBy: 'manual',
310
+ stage: 'initialization',
311
+ options: data // Optional: filePattern, maxFiles, etc.
312
+ });
313
+ await tracker.updateJob(jobId, { status: 'processing' });
314
+
315
+ try {
316
+ // Same orchestration logic as scheduled workflow
317
+ const result = await runIngestion(ctx, jobId, tracker);
318
+ await tracker.markCompleted(jobId, result);
319
+ return { success: true, jobId, ...result };
320
+ } catch (e: any) {
321
+ await tracker.markFailed(jobId, e);
322
+ return { success: false, jobId, error: e?.message };
323
+ }
324
+ })
325
+ );
326
+ ```
327
+
328
+ #### `src/workflows/webhook/job-status-check.ts`
329
+
330
+ **Purpose**: Query job status
331
+ **Trigger**: HTTP POST
332
+ **Endpoint**: `POST https://{workspace}.versori.run/inventory-batch-job-status`
333
+ **Request body**: `{ jobId: "inventory-batch-1234567890" }`
334
+
335
+ ```typescript
336
+ import { webhook, fn } from '@versori/run';
337
+ import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
338
+
339
+ /**
340
+ * Webhook: Job Status Check
341
+ *
342
+ * Endpoint: POST https://{workspace}.versori.run/inventory-batch-job-status
343
+ * Request body: { jobId: "inventory-batch-1234567890" }
344
+ *
345
+ * Pattern: webhook().then(fn()) - no external API needed, only KV storage
346
+ * Lightweight: Only queries KV store, no Fluent API calls
347
+ */
348
+ export const jobStatusCheck = webhook('inventory-batch-job-status', {
349
+ response: { mode: 'sync' },
350
+ connection: 'inventory-batch-job-status',
351
+ }).then(
352
+ fn('status', async ctx => {
353
+ const { data, log, openKv } = ctx;
354
+ const jobId = data?.jobId as string;
355
+
356
+ if (!jobId) {
357
+ return { success: false, error: 'jobId required' };
358
+ }
359
+
360
+ const tracker = new JobTracker(openKv(':project:'), log);
361
+ const status = await tracker.getJob(jobId);
362
+
363
+ return status
364
+ ? { success: true, jobId, ...status }
365
+ : { success: false, error: 'Job not found', jobId };
366
+ })
367
+ );
368
+ ```
369
+
370
+ ---
371
+
372
+ ### 3. Entry Point (`index.ts`)
373
+
374
+ **Purpose**: Register all workflows with Versori platform
375
+
376
+ ```typescript
377
+ /**
378
+ * Entry Point - Registers all workflows with Versori platform
379
+ *
380
+ * Versori automatically discovers and registers exported workflows
381
+ *
382
+ * File Structure:
383
+ * - src/workflows/scheduled/ → Time-based triggers (cron)
384
+ * - src/workflows/webhook/ → HTTP-based triggers (webhooks)
385
+ */
386
+
387
+ // Import scheduled workflows
388
+ import { daily_inventory_sync } from './src/workflows/scheduled/daily-inventory-sync';
389
+
390
+ // Import webhook workflows
391
+ import { adhoc_inventory_sync } from './src/workflows/webhook/adhoc-inventory-sync';
392
+ import { jobStatusCheck } from './src/workflows/webhook/job-status-check';
393
+
394
+ // Register all workflows
395
+ export {
396
+ // Scheduled (time-based triggers)
397
+ daily_inventory_sync,
398
+
399
+ // Webhooks (HTTP-based triggers)
400
+ adhoc_inventory_sync,
401
+ jobStatusCheck,
402
+ };
403
+ ```
404
+
405
+ **What Gets Exposed:**
406
+
407
+ - ✅ `adhoc_inventory_sync` → `https://{workspace}.versori.run/inventory-batch-adhoc`
408
+ - ✅ `jobStatusCheck` → `https://{workspace}.versori.run/inventory-batch-job-status`
409
+ - ❌ `daily_inventory_sync` → NOT exposed (runs automatically on cron)
410
+
411
+ ---
412
+
413
+ ### Adding New Workflows
414
+
415
+ **To add a scheduled workflow:**
416
+ 1. Create file in `src/workflows/scheduled/` with descriptive name (e.g., `hourly-delta-sync.ts`)
417
+ 2. Export the workflow from the file
418
+ 3. Import and re-export in `index.ts`
419
+
420
+ **To add a webhook workflow:**
421
+ 1. Create file in `src/workflows/webhook/` with descriptive name (e.g., `error-notification.ts`)
422
+ 2. Export the workflow from the file
423
+ 3. Import and re-export in `index.ts`
424
+
425
+ **Example - Adding hourly delta sync:**
426
+
427
+ ```typescript
428
+ // src/workflows/scheduled/hourly-delta-sync.ts
429
+ export const hourlyDeltaSync = schedule(
430
+ 'inventory-delta-hourly',
431
+ '0 * * * *' // Every hour
432
+ ).then(
433
+ http('run-delta-sync', { connection: 'fluent_commerce' }, async ctx => {
434
+ // Delta sync logic (skip BPP)
435
+ const result = await runIngestion(ctx, jobId, tracker, { bppEnabled: false });
436
+ return result;
437
+ })
438
+ );
439
+
440
+ // index.ts (add to imports and exports)
441
+ import { hourlyDeltaSync } from './src/workflows/scheduled/hourly-delta-sync';
442
+ export { daily_inventory_sync, hourlyDeltaSync, ... };
443
+ ```
444
+
445
+ ---
446
+ ## Complete Modular Implementation
447
+
448
+ ### File: `package.json`
449
+
450
+ ```json
451
+ {
452
+ "name": "multi-channel-inventory-sync",
453
+ "version": "1.0.0",
454
+ "description": "Multi-Channel Inventory Aggregation to Fluent Commerce Batch API",
455
+ "type": "module",
456
+ "versori": {
457
+ "workflows": "./index.ts"
458
+ },
459
+ "scripts": {
460
+ "lint": "eslint . --ext .ts",
461
+ "typecheck": "tsc --noEmit"
462
+ },
463
+ "dependencies": {
464
+ "@fluentcommerce/fc-connect-sdk": "^0.1.39",
465
+ "@versori/run": "latest"
466
+ },
467
+ "devDependencies": {
468
+ "@types/node": "^20.0.0",
469
+ "typescript": "^5.0.0"
470
+ },
471
+ "engines": {
472
+ "node": ">=18.0.0"
473
+ }
474
+ }
475
+ ```
476
+
477
+ ---
478
+
479
+ ### File: `index.ts`
480
+
481
+ ```typescript
482
+ import { schedule, webhook, http, fn } from '@versori/run';
483
+ import { processMultiChannelSync } from './src/workflows/multi-channel-sync.workflow';
484
+
485
+ /**
486
+ * Scheduled workflow: Multi-channel inventory sync every 15 minutes
487
+ *
488
+ * Processing: Parallel channel fetching with graceful degradation
489
+ * BPP: Disabled (preprocessing: 'skip') - delta detection already filters
490
+ * State Management: VersoriKVAdapter + JobTracker prevent duplicates
491
+ */
492
+ export const scheduledMultiChannelSync = schedule(
493
+ 'multi-channel-sync',
494
+ '*/15 * * * *' // Every 15 minutes
495
+ ).then(
496
+ http('run-sync', { connection: 'fluent_commerce' }, async ctx => {
497
+ // ctx contains: fetch, connections, log, activation, openKv
498
+ // Pass entire context to workflow
499
+ return await processMultiChannelSync(ctx);
500
+ })
501
+ );
502
+
503
+ /**
504
+ * Manual trigger endpoint for testing and ad-hoc runs
505
+ */
506
+ export const adhocMultiChannelSync = webhook('multi-channel-sync-adhoc', {
507
+ response: { mode: 'sync' },
508
+ connection: 'multi-channel-sync-adhoc',
509
+ }).then(
510
+ http('run-sync-adhoc', { connection: 'fluent_commerce' }, async ctx => {
511
+ return await processMultiChannelSync(ctx);
512
+ })
513
+ );
514
+
515
+ /**
516
+ * Job status check endpoint
517
+ */
518
+ export const multiChannelSyncJobStatus = webhook('multi-channel-sync-job-status', {
519
+ response: { mode: 'sync' },
520
+ connection: 'multi-channel-sync-job-status',
521
+ }).then(
522
+ fn('status', async ctx => {
523
+ const { data, log, openKv } = ctx;
524
+ const jobId = data?.jobId as string;
525
+ if (!jobId) return { success: false, error: 'jobId required' };
526
+ const { JobTracker } = await import('@fluentcommerce/fc-connect-sdk');
527
+ const tracker = new JobTracker(openKv(':project:'), log);
528
+ const status = await tracker.getJob(jobId);
529
+ return status
530
+ ? { success: true, jobId, ...status }
531
+ : { success: false, error: 'Job not found', jobId };
532
+ })
533
+ );
534
+ ```
535
+
536
+ ---
537
+
538
+ ### File: `src/types/multi-channel.types.ts`
539
+
540
+ ```typescript
541
+ /**
542
+ * Type definitions for multi-channel inventory sync
543
+ */
544
+ export interface ChannelInventoryRecord {
545
+ sku: string;
546
+ location: string;
547
+ channel: string;
548
+ onHand: number;
549
+ reserved: number;
550
+ buffer: number;
551
+ lastUpdated?: string;
552
+ }
553
+
554
+ export interface AggregatedInventory {
555
+ sku: string;
556
+ location: string;
557
+ totalOnHand: number;
558
+ totalReserved: number;
559
+ atp: number;
560
+ channels: {
561
+ [channel: string]: {
562
+ allocated: number;
563
+ buffer: number;
564
+ max?: number;
565
+ };
566
+ };
567
+ }
568
+
569
+ export interface SyncState {
570
+ [sku: string]: {
571
+ [location: string]: number; // ATP value
572
+ };
573
+ }
574
+
575
+ export interface SyncStats {
576
+ totalRecords: number;
577
+ channelARecords: number;
578
+ channelBRecords: number;
579
+ fluentRecords: number;
580
+ aggregatedSkus: number;
581
+ changedRecords: number;
582
+ batchesSent: number;
583
+ successCount: number;
584
+ errorCount: number;
585
+ duration: number;
586
+ }
587
+
588
+ export interface BatchDetail {
589
+ batchId: string;
590
+ recordCount: number;
591
+ timestamp: string;
592
+ status: 'SENT' | 'FAILED';
593
+ error?: string;
594
+ }
595
+
596
+ export interface BatchResult {
597
+ totalSent: number;
598
+ batchCount: number;
599
+ batches: BatchDetail[];
600
+ errors: Array<{ batchId: string; error: string }>;
601
+ }
602
+ ```
603
+
604
+ ---
605
+
606
+ ### File: `src/config/multi-channel.mapping.json`
607
+
608
+ ```json
609
+ {
610
+ "name": "multi-channel-inventory",
611
+ "version": "1.0.0",
612
+ "description": "Normalize channel payloads to aggregation schema",
613
+ "fields": {
614
+ "locationRef": {
615
+ "source": "locationRef",
616
+ "required": true,
617
+ "resolver": "sdk.trim",
618
+ "comment": "Location reference"
619
+ },
620
+ "skuRef": {
621
+ "source": "skuRef",
622
+ "required": true,
623
+ "resolver": "sdk.trim",
624
+ "comment": "SKU reference"
625
+ },
626
+ "onHand": {
627
+ "source": "onHand",
628
+ "resolver": "sdk.number",
629
+ "comment": "On-hand quantity"
630
+ },
631
+ "reserved": {
632
+ "source": "reserved",
633
+ "resolver": "sdk.number",
634
+ "comment": "Reserved quantity"
635
+ },
636
+ "buffer": {
637
+ "source": "buffer",
638
+ "resolver": "sdk.number",
639
+ "comment": "Safety buffer"
640
+ },
641
+ "channel": {
642
+ "source": "channel",
643
+ "defaultValue": "UNKNOWN",
644
+ "comment": "Channel identifier"
645
+ }
646
+ }
647
+ }
648
+ ```
649
+
650
+ > **✅ PRODUCTION STANDARD:** Use external JSON files for mapping configuration (not TypeScript objects)
651
+
652
+ ---
653
+
654
+ ### File: `src/services/atp-calculator.service.ts`
655
+
656
+ ```typescript
657
+ import type { ChannelInventoryRecord, AggregatedInventory } from '../types/multi-channel.types';
658
+
659
+ /**
660
+ * Service for calculating ATP (Available To Promise) across channels
661
+ */
662
+ export class ATPCalculatorService {
663
+ private readonly oversellProtection: boolean;
664
+
665
+ constructor(
666
+ oversellProtection = true
667
+ ) {
668
+ this.oversellProtection = oversellProtection;
669
+ }
670
+
671
+ /**
672
+ * Calculate base ATP for a single record
673
+ * Formula: ATP = (onHand - reserved) - buffer
674
+ */
675
+ calculateBaseATP(onHand: number, reserved: number, buffer: number): number {
676
+ const available = Math.max(0, onHand - reserved);
677
+ const atp = available - buffer;
678
+ return this.oversellProtection ? Math.max(0, atp) : atp;
679
+ }
680
+
681
+ /**
682
+ * Aggregate inventory across channels
683
+ * Deduplicates by SKU + location and calculates consolidated ATP
684
+ */
685
+ aggregateChannelInventory(records: ChannelInventoryRecord[]): Map<string, AggregatedInventory> {
686
+ const aggregated = new Map<string, AggregatedInventory>();
687
+
688
+ for (const record of records) {
689
+ const key = `${record.sku}:${record.location}`;
690
+ const existing = aggregated.get(key);
691
+
692
+ if (existing) {
693
+ // Aggregate across channels
694
+ existing.totalOnHand += record.onHand;
695
+ existing.totalReserved += record.reserved;
696
+ existing.channels[record.channel] = {
697
+ allocated: record.onHand - record.reserved - record.buffer,
698
+ buffer: record.buffer,
699
+ };
700
+ } else {
701
+ // First record for this SKU+location
702
+ aggregated.set(key, {
703
+ sku: record.sku,
704
+ location: record.location,
705
+ totalOnHand: record.onHand,
706
+ totalReserved: record.reserved,
707
+ atp: 0,
708
+ channels: {
709
+ [record.channel]: {
710
+ allocated: record.onHand - record.reserved - record.buffer,
711
+ buffer: record.buffer,
712
+ },
713
+ },
714
+ });
715
+ }
716
+ }
717
+
718
+ // Calculate final ATP for each aggregated record
719
+ for (const [, agg] of aggregated) {
720
+ const totalBuffer = Math.max(...Object.values(agg.channels).map(c => c.buffer));
721
+ agg.atp = this.calculateBaseATP(agg.totalOnHand, agg.totalReserved, totalBuffer);
722
+ }
723
+
724
+
725
+ return aggregated;
726
+ }
727
+ }
728
+ ```
729
+
730
+ ---
731
+
732
+ ### File: `src/services/channel-a-connector.service.ts`
733
+
734
+ ```typescript
735
+
736
+ /**
737
+ * Service for fetching inventory from Channel A REST API
738
+ * Includes rate limiting and exponential backoff retry logic
739
+ */
740
+ export class ChannelAConnectorService {
741
+ private readonly url: string;
742
+ private readonly apiKey: string;
743
+ private readonly rateLimitRpm: number;
744
+ private readonly logger; // ✅ Versori native log - TypeScript infers type
745
+ private lastRequest = 0;
746
+
747
+ constructor(url: string, apiKey: string, rateLimitRpm: number, logger) { // ✅ Versori native log - TypeScript infers type
748
+ this.url = url;
749
+ this.apiKey = apiKey;
750
+ this.rateLimitRpm = rateLimitRpm;
751
+ this.logger = logger;
752
+ }
753
+
754
+ /**
755
+ * Fetch inventory from Channel A with rate limiting
756
+ */
757
+ async fetchInventory(): Promise<any[]> {
758
+ await this.enforceRateLimit();
759
+
760
+ const response = await this.fetchWithRetry(this.url, {
761
+ method: 'GET',
762
+ headers: {
763
+ 'Content-Type': 'application/json',
764
+ Authorization: `Bearer ${this.apiKey}`,
765
+ },
766
+ });
767
+
768
+ if (!response.ok) {
769
+ throw new Error(`Channel A API error: ${response.status} ${response.statusText}`);
770
+ }
771
+
772
+ const data = await response.json();
773
+ return data.inventory || [];
774
+ }
775
+
776
+ /**
777
+ * Enforce rate limit (minimum interval between requests)
778
+ */
779
+ private async enforceRateLimit(): Promise<void> {
780
+ const minInterval = 60000 / this.rateLimitRpm;
781
+ const now = Date.now();
782
+ const elapsed = now - this.lastRequest;
783
+
784
+ if (elapsed < minInterval) {
785
+ const wait = minInterval - elapsed;
786
+ await new Promise(resolve => setTimeout(resolve, wait));
787
+ }
788
+
789
+ this.lastRequest = Date.now();
790
+ }
791
+
792
+ /**
793
+ * Fetch with exponential backoff retry
794
+ */
795
+ private async fetchWithRetry(
796
+ url: string,
797
+ options: RequestInit,
798
+ maxRetries = 3
799
+ ): Promise<Response> {
800
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
801
+ try {
802
+ const response = await fetch(url, options);
803
+
804
+ if (response.status === 429 || response.status >= 500) {
805
+ if (attempt < maxRetries) {
806
+ const backoff = Math.pow(2, attempt) * 1000;
807
+ `[ChannelA] Error ${response.status}, retrying in ${backoff}ms (attempt ${attempt}/${maxRetries})`
808
+ );
809
+ await new Promise(resolve => setTimeout(resolve, backoff));
810
+ continue;
811
+ }
812
+ }
813
+
814
+ return response;
815
+ } catch (error) {
816
+ if (attempt === maxRetries) throw error;
817
+ const backoff = Math.pow(2, attempt) * 1000;
818
+ `[ChannelA] Fetch error, retrying in ${backoff}ms (attempt ${attempt}/${maxRetries}):`,
819
+ error
820
+ );
821
+ await new Promise(resolve => setTimeout(resolve, backoff));
822
+ }
823
+ }
824
+
825
+ throw new Error('Channel A: Max retries exceeded');
826
+ }
827
+ }
828
+ ```
829
+
830
+ ---
831
+
832
+ ### File: `src/services/channel-b-connector.service.ts`
833
+
834
+ ```typescript
835
+ import { S3DataSource, CSVParserService } from '@fluentcommerce/fc-connect-sdk';
836
+
837
+ /**
838
+ * Service for fetching inventory from Channel B S3 CSV
839
+ */
840
+ export class ChannelBConnectorService {
841
+ private readonly s3: S3DataSource;
842
+ private readonly csv: CSVParserService;
843
+ private readonly bucket: string;
844
+ private readonly key: string;
845
+ private readonly logger; // ✅ Versori native log - TypeScript infers type
846
+
847
+ constructor(s3Config: any, bucket: string, key: string, logger) { // ✅ Versori native log - TypeScript infers type
848
+ this.s3 = new S3DataSource(s3Config, logger);
849
+ this.csv = new CSVParserService();
850
+ this.bucket = bucket;
851
+ this.key = key;
852
+ }
853
+
854
+ /**
855
+ * Fetch inventory from S3 CSV file
856
+ */
857
+ async fetchInventory(): Promise<any[]> {
858
+ try {
859
+ const csvContent = (await this.s3.downloadFile(`${this.bucket}/${this.key}`, {
860
+ encoding: 'utf8',
861
+ })) as string;
862
+
863
+ const records = await this.csv.parse(csvContent, {
864
+ columns: true,
865
+ skip_empty_lines: true,
866
+ trim: true,
867
+ });
868
+
869
+ return records || [];
870
+ } catch (error) {
871
+ throw error;
872
+ }
873
+ // Note: S3DataSource doesn't require explicit disposal (unlike SFTP)
874
+ }
875
+ }
876
+ ```
877
+
878
+ ---
879
+
880
+ ### File: `src/services/batch-processor.service.ts`
881
+
882
+ ```typescript
883
+ import type { FluentClient } from '@fluentcommerce/fc-connect-sdk';
884
+ import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
885
+ import { BatchResult, BatchDetail } from '../types/multi-channel.types';
886
+
887
+ /**
888
+ * Service for sending records to Fluent Batch API
889
+ *
890
+ * ✅ PRODUCTION ENHANCEMENT: Optional logger for detailed progress tracking
891
+ */
892
+ export class BatchProcessorService {
893
+ constructor(
894
+ private client: FluentClient,
895
+ private jobTracker: JobTracker,
896
+ private log?: any // ✅ Optional logger for progress tracking
897
+ ) {}
898
+
899
+ /**
900
+ * Send inventory records to Batch API with chunking
901
+ */
902
+ async sendInventoryBatches(
903
+ jobId: string,
904
+ records: any[],
905
+ batchSize: number = 500
906
+ ): Promise<BatchResult> {
907
+ const result: BatchResult = {
908
+ totalSent: 0,
909
+ batchCount: 0,
910
+ batches: [],
911
+ errors: [],
912
+ };
913
+
914
+ // Chunk records into batches
915
+ const chunks: any[][] = [];
916
+ for (let i = 0; i < records.length; i += batchSize) {
917
+ chunks.push(records.slice(i, i + batchSize));
918
+ }
919
+
920
+ const totalBatches = chunks.length;
921
+
922
+ // ✅ PRODUCTION ENHANCEMENT: Log batch sending start
923
+ if (this.log) {
924
+ this.log.info('📤 Starting batch sending', {
925
+ jobId,
926
+ totalRecords: records.length,
927
+ batchSize,
928
+ totalBatches,
929
+ processingMode: 'sequential (one at a time)',
930
+ });
931
+ }
932
+
933
+ // Send each batch
934
+ for (let i = 0; i < chunks.length; i++) {
935
+ const batchNumber = i + 1;
936
+
937
+ // ✅ PRODUCTION ENHANCEMENT: Log progress every 10 batches
938
+ if (this.log && batchNumber % 10 === 0) {
939
+ this.log.info(`📤 Sending batch ${batchNumber}/${totalBatches}`, {
940
+ jobId,
941
+ batchNumber,
942
+ totalBatches,
943
+ recordsInBatch: chunks[i].length,
944
+ totalSentSoFar: result.totalSent,
945
+ progress: `${((batchNumber / totalBatches) * 100).toFixed(1)}%`,
946
+ });
947
+ }
948
+
949
+ try {
950
+ const batch = await this.client.sendBatch(jobId, {
951
+ action: 'UPSERT',
952
+ entityType: 'INVENTORY',
953
+ source: 'MULTI_CHANNEL',
954
+ event: 'MULTI_CHANNEL_SYNC',
955
+ entities: chunks[i],
956
+ });
957
+
958
+ result.totalSent += chunks[i].length;
959
+ result.batchCount++;
960
+ result.batches.push({
961
+ batchId: batch.id,
962
+ recordCount: chunks[i].length,
963
+ timestamp: new Date().toISOString(),
964
+ status: 'SENT',
965
+ });
966
+
967
+ // ✅ No logging here - workflow handles it
968
+
969
+ // Update job tracker
970
+ await this.jobTracker.updateJob(jobId, {
971
+ details: {
972
+ batchesSent: result.batchCount,
973
+ recordsProcessed: result.totalSent,
974
+ },
975
+ });
976
+ } catch (error: any) {
977
+ result.errors.push({
978
+ batchId: `batch-${batchNumber}`,
979
+ error: error.message,
980
+ });
981
+ result.batches.push({
982
+ batchId: `batch-${batchNumber}`,
983
+ recordCount: chunks[i].length,
984
+ timestamp: new Date().toISOString(),
985
+ status: 'FAILED',
986
+ error: error.message,
987
+ });
988
+ }
989
+ }
990
+
991
+ // ✅ PRODUCTION ENHANCEMENT: Log completion
992
+ if (this.log) {
993
+ this.log.info('✅ Sequential batch sending completed', {
994
+ jobId,
995
+ totalBatches,
996
+ batchesSent: result.batchCount,
997
+ batchesFailed: result.errors.length,
998
+ totalRecordsSent: result.totalSent,
999
+ });
1000
+ }
1001
+
1002
+ return result;
1003
+ }
1004
+ }
1005
+ ```
1006
+
1007
+ ---
1008
+
1009
+ ### File: `src/services/batch-logger.service.ts`
1010
+
1011
+ ```typescript
1012
+ import { Buffer } from 'node:buffer';
1013
+ import { S3DataSource } from '@fluentcommerce/fc-connect-sdk';
1014
+
1015
+ /**
1016
+ * Service for writing batch processing logs to S3
1017
+ */
1018
+ export class BatchLoggerService {
1019
+ constructor(private s3: S3DataSource) {
1020
+ // ✅ No logger - workflow handles logging with Versori native log
1021
+ }
1022
+
1023
+ /**
1024
+ * Write batch processing log to S3
1025
+ */
1026
+ async writeBatchLog(
1027
+ logData: any,
1028
+ bucket: string,
1029
+ keyPrefix: string,
1030
+ format: 'json' | 'text' = 'json'
1031
+ ): Promise<void> {
1032
+ try {
1033
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1034
+ const logFileName = `${keyPrefix}${timestamp}.${format === 'json' ? 'json' : 'log'}`;
1035
+ const logKey = `${bucket}/${logFileName}`;
1036
+
1037
+ const logContent = this.formatLogContent(logData, format);
1038
+
1039
+ await this.s3.uploadFile(logKey, Buffer.from(logContent, 'utf8'), {
1040
+ encoding: 'utf8',
1041
+ contentType: format === 'json' ? 'application/json' : 'text/plain',
1042
+ });
1043
+
1044
+ // ✅ No logging here - workflow handles it
1045
+ } catch (error: any) {
1046
+ // ✅ No logging here - workflow handles it
1047
+ // Don't throw - logging failure shouldn't stop workflow
1048
+ }
1049
+ }
1050
+
1051
+ private formatLogContent(logData: any, format: 'json' | 'text'): string {
1052
+ if (format === 'json') {
1053
+ return JSON.stringify(logData, null, 2);
1054
+ }
1055
+
1056
+ return `Multi-Channel Sync Log
1057
+ ======================
1058
+ Timestamp: ${logData.timestamp}
1059
+ Job ID: ${logData.jobId}
1060
+
1061
+ Channel Summary:
1062
+ Channel A: ${logData.channels.channelA || 0} records
1063
+ Channel B: ${logData.channels.channelB || 0} records
1064
+ Fluent: ${logData.channels.fluent || 0} records
1065
+
1066
+ Aggregation:
1067
+ Total Records: ${logData.aggregated}
1068
+ Changed Records: ${logData.changed}
1069
+ Unchanged: ${logData.unchanged}
1070
+
1071
+ Batches:
1072
+ ${logData.batches
1073
+ .map(
1074
+ (b: any, i: number) =>
1075
+ ` [${i + 1}] ${b.batchId} | ${b.recordCount} records | ${b.status}${b.error ? ` | Error: ${b.error}` : ''}`
1076
+ )
1077
+ .join('\n')}
1078
+
1079
+ Summary:
1080
+ Total Batches: ${logData.summary.totalBatches}
1081
+ Successful: ${logData.summary.success}
1082
+ Failed: ${logData.summary.failed}
1083
+ Duration: ${logData.summary.duration}ms
1084
+
1085
+ Status: ${logData.status}
1086
+ `;
1087
+ }
1088
+ }
1089
+ ```
1090
+
1091
+ ---
1092
+
1093
+ ### File: `src/workflows/multi-channel-sync.workflow.ts`
1094
+
1095
+ ```typescript
1096
+ import { Buffer } from 'node:buffer';
1097
+ import {
1098
+ createClient,
1099
+ StateService,
1100
+ VersoriKVAdapter,
1101
+ JobTracker,
1102
+ } from '@fluentcommerce/fc-connect-sdk';
1103
+ import type { ChannelInventoryRecord, SyncStats } from '../types/multi-channel.types';
1104
+ import { ATPCalculatorService } from '../services/atp-calculator.service';
1105
+ import { ChannelAConnectorService } from '../services/channel-a-connector.service';
1106
+ import { ChannelBConnectorService } from '../services/channel-b-connector.service';
1107
+ import { BatchProcessorService } from '../services/batch-processor.service';
1108
+ import { BatchLoggerService } from '../services/batch-logger.service';
1109
+
1110
+ const INVENTORY_QUERY = `
1111
+ query GetInventory($retailerId: ID!, $first: Int!, $after: String) {
1112
+ inventoryPositions(retailerId: $retailerId, first: $first, after: $after) {
1113
+ edges {
1114
+ node {
1115
+ id
1116
+ ref
1117
+ productRef
1118
+ locationRef
1119
+ onHand
1120
+ reservedQuantity
1121
+ status
1122
+ type
1123
+ }
1124
+ cursor
1125
+ }
1126
+ pageInfo {
1127
+ hasNextPage
1128
+ endCursor
1129
+ }
1130
+ }
1131
+ }
1132
+ `;
1133
+
1134
+ /**
1135
+ * Fetch current inventory state from Fluent GraphQL
1136
+ */
1137
+ async function fetchFluentInventory(
1138
+ client: any,
1139
+ retailerId: string,
1140
+ pageSize: number,
1141
+ maxRecords: number,
1142
+ log: any
1143
+ ): Promise<any[]> {
1144
+ try {
1145
+ const result = await client.graphql({
1146
+ query: INVENTORY_QUERY,
1147
+ variables: { retailerId, first: pageSize },
1148
+ pagination: { maxRecords },
1149
+ });
1150
+
1151
+ const edges = result.data?.inventoryPositions?.edges || [];
1152
+ const records = edges.map((e: any) => ({
1153
+ productRef: e.node.productRef,
1154
+ locationRef: e.node.locationRef,
1155
+ onHand: Number(e.node.onHand || 0),
1156
+ reserved: Number(e.node.reservedQuantity || 0),
1157
+ }));
1158
+
1159
+ log.info(`[Fluent] Fetched ${records.length} records from GraphQL`);
1160
+ return records;
1161
+ } catch (error) {
1162
+ // ✅ Enhanced error logging: Extract all error details for visibility
1163
+ const errorDetails = {
1164
+ message: error instanceof Error ? error.message : String(error),
1165
+ stack: error instanceof Error ? error.stack : undefined,
1166
+ errorType: error instanceof Error ? error.constructor.name : 'Error',
1167
+ };
1168
+ log.error('[Fluent] GraphQL fetch error:', errorDetails);
1169
+ throw error;
1170
+ }
1171
+ }
1172
+
1173
+ /**
1174
+ * Main multi-channel sync workflow orchestrator
1175
+ * Coordinates channel fetching, aggregation, delta detection, and batch submission
1176
+ *
1177
+ * @param ctx - Versori context object containing fetch, connections, log, activation, openKv
1178
+ */
1179
+ export async function processMultiChannelSync(ctx: any) {
1180
+ const { log, openKv, activation } = ctx;
1181
+ const startTime = Date.now();
1182
+
1183
+ log.info('[MultiChannelSync] Starting sync workflow');
1184
+
1185
+ try {
1186
+ // ========================================
1187
+ // CLIENT INITIALIZATION
1188
+ // ========================================
1189
+ const client = await createClient(ctx);
1190
+
1191
+ // ========================================
1192
+ // CONFIGURATION
1193
+ // ========================================
1194
+ const config = {
1195
+ retailerId: activation?.getVariable('retailerId'),
1196
+ jobName: activation?.getVariable('jobName') || 'Multi-Channel Inventory Sync',
1197
+ batchSize: parseInt(activation?.getVariable('batchSize') || '500', 10),
1198
+ maxRecords: parseInt(activation?.getVariable('maxRecords') || '50000', 10),
1199
+ defaultBuffer: parseInt(activation?.getVariable('defaultBuffer') || '5', 10),
1200
+ oversellProtection: activation?.getVariable('oversellProtection') !== 'false',
1201
+ useBpp: activation?.getVariable('useBpp') || 'skip',
1202
+ enableDelta: activation?.getVariable('enableDelta') !== 'false',
1203
+ deltaStateKey: activation?.getVariable('deltaStateKey') || 'sync:delta:state',
1204
+ channelAEnabled: activation?.getVariable('channelAEnabled') === 'true',
1205
+ channelAUrl: activation?.getVariable('channelAUrl'),
1206
+ channelAKey: activation?.getVariable('channelAKey'),
1207
+ channelABuffer: parseInt(activation?.getVariable('channelABuffer') || '5', 10),
1208
+ channelARateLimitRpm: parseInt(activation?.getVariable('channelARateLimitRpm') || '120', 10),
1209
+ channelBEnabled: activation?.getVariable('channelBEnabled') === 'true',
1210
+ channelBBucket: activation?.getVariable('channelBBucket'),
1211
+ channelBKey: activation?.getVariable('channelBKey'),
1212
+ fluentEnabled: activation?.getVariable('fluentEnabled') === 'true',
1213
+ fluentPageSize: parseInt(activation?.getVariable('fluentPageSize') || '200', 10),
1214
+ };
1215
+
1216
+ // ========================================
1217
+ // SERVICE INITIALIZATION
1218
+ // ========================================
1219
+ const kv = new VersoriKVAdapter(openKv(':project:'));
1220
+ const jobTracker = new JobTracker(openKv(':project:'), log);
1221
+ const stateService = new StateService(log);
1222
+
1223
+ const atpCalc = new ATPCalculatorService(config.oversellProtection);
1224
+ // ✅ PRODUCTION ENHANCEMENT: Pass log to BatchProcessorService for detailed progress tracking
1225
+ const batchProcessor = new BatchProcessorService(client, jobTracker, log);
1226
+
1227
+ const jobId = `multi-channel-sync-${Date.now()}`;
1228
+ await jobTracker.createJob(jobId, {
1229
+ triggeredBy: 'schedule',
1230
+ stage: 'initialization',
1231
+ details: { config },
1232
+ });
1233
+
1234
+ const stats: SyncStats = {
1235
+ totalRecords: 0,
1236
+ channelARecords: 0,
1237
+ channelBRecords: 0,
1238
+ fluentRecords: 0,
1239
+ aggregatedSkus: 0,
1240
+ changedRecords: 0,
1241
+ batchesSent: 0,
1242
+ successCount: 0,
1243
+ errorCount: 0,
1244
+ duration: 0,
1245
+ };
1246
+
1247
+ await jobTracker.updateJob(jobId, { status: 'fetching_channels' });
1248
+
1249
+ // ========================================
1250
+ // CHANNEL FETCHING (PARALLEL WITH GRACEFUL DEGRADATION)
1251
+ // ========================================
1252
+ const allRecords: ChannelInventoryRecord[] = [];
1253
+
1254
+ // Fetch Channel A (REST API)
1255
+ if (config.channelAEnabled && config.channelAUrl && config.channelAKey) {
1256
+ log.info('[MultiChannelSync] Fetching from Channel A...');
1257
+ const channelA = new ChannelAConnectorService(
1258
+ config.channelAUrl,
1259
+ config.channelAKey,
1260
+ config.channelARateLimitRpm,
1261
+ log
1262
+ );
1263
+
1264
+ try {
1265
+ const records = await channelA.fetchInventory();
1266
+ const mapped = records.map(r => ({
1267
+ sku: r.product_id,
1268
+ location: r.warehouse_code,
1269
+ channel: 'A',
1270
+ onHand: r.quantity_available,
1271
+ reserved: r.quantity_reserved,
1272
+ buffer: config.channelABuffer,
1273
+ lastUpdated: r.updated_at,
1274
+ }));
1275
+
1276
+ allRecords.push(...mapped);
1277
+ stats.channelARecords = mapped.length;
1278
+ log.info(`[MultiChannelSync] Channel A: ${mapped.length} records`);
1279
+ } catch (error) {
1280
+ // ✅ Enhanced error logging: Extract all error details for visibility
1281
+ const errorDetails = {
1282
+ message: error instanceof Error ? error.message : String(error),
1283
+ stack: error instanceof Error ? error.stack : undefined,
1284
+ errorType: error instanceof Error ? error.constructor.name : 'Error',
1285
+ };
1286
+ log.error('[MultiChannelSync] Channel A fetch failed (continuing):', errorDetails);
1287
+ stats.errorCount++;
1288
+ }
1289
+ }
1290
+
1291
+ // Fetch Channel B (S3 CSV)
1292
+ if (config.channelBEnabled && config.channelBBucket && config.channelBKey) {
1293
+ log.info('[MultiChannelSync] Fetching from Channel B...');
1294
+ const s3Config = {
1295
+ type: 'S3_CSV',
1296
+ connectionId: 'channel-b-s3',
1297
+ name: 'Channel B S3',
1298
+ s3Config: {
1299
+ bucket: config.channelBBucket,
1300
+ region: activation?.getVariable('awsRegion') || 'us-east-1',
1301
+ accessKeyId: activation?.getVariable('awsAccessKeyId'),
1302
+ secretAccessKey: activation?.getVariable('awsSecretAccessKey'),
1303
+ },
1304
+ };
1305
+
1306
+ const channelB = new ChannelBConnectorService(
1307
+ s3Config,
1308
+ config.channelBBucket,
1309
+ config.channelBKey,
1310
+ log
1311
+ );
1312
+
1313
+ try {
1314
+ const records = await channelB.fetchInventory();
1315
+ const mapped = records.map((r: any) => ({
1316
+ sku: r.sku,
1317
+ location: r.location,
1318
+ channel: 'B',
1319
+ onHand: parseInt(r.qty, 10),
1320
+ reserved: parseInt(r.reserved, 10),
1321
+ buffer: config.defaultBuffer,
1322
+ }));
1323
+
1324
+ allRecords.push(...mapped);
1325
+ stats.channelBRecords = mapped.length;
1326
+ log.info(`[MultiChannelSync] Channel B: ${mapped.length} records`);
1327
+ } catch (error) {
1328
+ // ✅ Enhanced error logging: Extract all error details for visibility
1329
+ const errorDetails = {
1330
+ message: error instanceof Error ? error.message : String(error),
1331
+ stack: error instanceof Error ? error.stack : undefined,
1332
+ errorType: error instanceof Error ? error.constructor.name : 'Error',
1333
+ };
1334
+ log.error('[MultiChannelSync] Channel B fetch failed (continuing):', errorDetails);
1335
+ stats.errorCount++;
1336
+ }
1337
+ }
1338
+
1339
+ // Fetch Fluent GraphQL (current state)
1340
+ if (config.fluentEnabled) {
1341
+ log.info('[MultiChannelSync] Fetching from Fluent GraphQL...');
1342
+ try {
1343
+ const records = await fetchFluentInventory(
1344
+ client,
1345
+ config.retailerId!,
1346
+ config.fluentPageSize,
1347
+ config.maxRecords,
1348
+ log
1349
+ );
1350
+
1351
+ const mapped = records.map(r => ({
1352
+ sku: r.productRef,
1353
+ location: r.locationRef,
1354
+ channel: 'FLUENT',
1355
+ onHand: r.onHand,
1356
+ reserved: r.reserved,
1357
+ buffer: 0,
1358
+ }));
1359
+
1360
+ allRecords.push(...mapped);
1361
+ stats.fluentRecords = mapped.length;
1362
+ log.info(`[MultiChannelSync] Fluent: ${mapped.length} records`);
1363
+ } catch (error) {
1364
+ // ✅ Enhanced error logging: Extract all error details for visibility
1365
+ const errorDetails = {
1366
+ message: error instanceof Error ? error.message : String(error),
1367
+ stack: error instanceof Error ? error.stack : undefined,
1368
+ errorType: error instanceof Error ? error.constructor.name : 'Error',
1369
+ };
1370
+ log.error('[MultiChannelSync] Fluent fetch failed (continuing):', errorDetails);
1371
+ stats.errorCount++;
1372
+ }
1373
+ }
1374
+
1375
+ stats.totalRecords = allRecords.length;
1376
+
1377
+ if (allRecords.length === 0) {
1378
+ log.warn('[MultiChannelSync] No inventory records fetched from any channel');
1379
+ await jobTracker.markCompleted(jobId, { message: 'No data', stats });
1380
+ return { success: false, message: 'No data', stats };
1381
+ }
1382
+
1383
+ // ========================================
1384
+ // AGGREGATION
1385
+ // ========================================
1386
+ await jobTracker.updateJob(jobId, { status: 'aggregating' });
1387
+
1388
+ log.info('[MultiChannelSync] Aggregating inventory across channels...');
1389
+ const aggregated = atpCalc.aggregateChannelInventory(allRecords);
1390
+ stats.aggregatedSkus = aggregated.size;
1391
+
1392
+ const finalInventory = Array.from(aggregated.values()).map(agg => ({
1393
+ skuRef: agg.sku,
1394
+ locationRef: agg.location,
1395
+ qty: agg.atp,
1396
+ type: 'AVAILABLE',
1397
+ status: 'ACTIVE',
1398
+ expectedOn: new Date().toISOString().split('T')[0],
1399
+ }));
1400
+
1401
+ log.info(`[MultiChannelSync] Aggregated ${stats.aggregatedSkus} unique SKU/location combinations`);
1402
+
1403
+ // ========================================
1404
+ // DELTA DETECTION
1405
+ // ========================================
1406
+ let recordsToSend = finalInventory;
1407
+
1408
+ if (config.enableDelta) {
1409
+ await jobTracker.updateJob(jobId, { status: 'delta_detection' });
1410
+ log.info('[MultiChannelSync] Checking for changes (delta detection)...');
1411
+
1412
+ const prevState = ((await stateService.getState(config.deltaStateKey)) as any) || {};
1413
+ const changedRecords = [];
1414
+
1415
+ for (const record of finalInventory) {
1416
+ const prevQty = prevState[record.skuRef]?.[record.locationRef];
1417
+ if (prevQty === undefined || prevQty !== record.qty) {
1418
+ changedRecords.push(record);
1419
+ }
1420
+ }
1421
+
1422
+ recordsToSend = changedRecords;
1423
+ stats.changedRecords = changedRecords.length;
1424
+ log.info(
1425
+ `[MultiChannelSync] Delta detection: ${changedRecords.length} changed records (${finalInventory.length} total)`
1426
+ );
1427
+ } else {
1428
+ stats.changedRecords = finalInventory.length;
1429
+ }
1430
+
1431
+ if (recordsToSend.length === 0) {
1432
+ log.info('[MultiChannelSync] No changes detected, skipping batch send');
1433
+ stats.duration = Date.now() - startTime;
1434
+ await jobTracker.markCompleted(jobId, { message: 'No changes', stats });
1435
+ return { success: true, message: 'No changes', stats };
1436
+ }
1437
+
1438
+ // ========================================
1439
+ // BATCH API SUBMISSION
1440
+ // ========================================
1441
+ await jobTracker.updateJob(jobId, { status: 'creating_batch_job' });
1442
+
1443
+ log.info('[MultiChannelSync] Creating Batch API job...');
1444
+ const job = await client.createJob({
1445
+ name: config.jobName,
1446
+ retailerId: config.retailerId!,
1447
+ meta: {
1448
+ preprocessing: config.useBpp,
1449
+ },
1450
+ });
1451
+
1452
+ log.info(`[MultiChannelSync] Job created: ${job.id}`);
1453
+
1454
+ await jobTracker.updateJob(jobId, { status: 'sending_batches' });
1455
+
1456
+ // ? Enhanced: Extract context for progress logging
1457
+ const uniqueLocations = [...new Set(recordsToSend.map((r: any) => r.locationRef))];
1458
+ const sampleSKUs = recordsToSend.slice(0, 5).map((r: any) => r.skuRef);
1459
+ const estimatedBatches = Math.ceil(recordsToSend.length / config.batchSize);
1460
+
1461
+ // ? Enhanced: Start logging with context
1462
+ log.info(`[BatchProcessor] Sending batches for multi-channel sync`, {
1463
+ totalRecords: recordsToSend.length,
1464
+ estimatedBatches,
1465
+ batchSize: config.batchSize,
1466
+ locations: uniqueLocations.join(', '),
1467
+ sampleSKUs: sampleSKUs.join(', '),
1468
+ jobId: job.id
1469
+ });
1470
+
1471
+ const batchResults = await batchProcessor.sendInventoryBatches(
1472
+ job.id,
1473
+ recordsToSend,
1474
+ config.batchSize
1475
+ );
1476
+
1477
+ // ✅ Logging handled in workflow with Versori native log
1478
+ log.info(`[BatchProcessor] Sent ${batchResults.batchCount} batches`, {
1479
+ jobId: job.id,
1480
+ totalRecords: batchResults.totalSent,
1481
+ successfulBatches: batchResults.batches.filter(b => b.status === 'SENT').length,
1482
+ failedBatches: batchResults.batches.filter(b => b.status === 'FAILED').length,
1483
+ });
1484
+
1485
+ // ? Enhanced: Completion logging with summary
1486
+ log.info(`[BatchProcessor] Batch submission completed for multi-channel sync`, {
1487
+ totalBatches: batchResults.batchCount,
1488
+ totalRecords: batchResults.totalSent,
1489
+ successfulBatches: batchResults.batches.filter(b => b.status === 'SENT').length,
1490
+ failedBatches: batchResults.errors.length,
1491
+ jobId: job.id
1492
+ });
1493
+
1494
+ if (batchResults.errors.length > 0) {
1495
+ log.warn(`[BatchProcessor] ${batchResults.errors.length} batches failed`, {
1496
+ jobId: job.id,
1497
+ errors: batchResults.errors.slice(0, 5), // Log first 5 errors
1498
+ });
1499
+ }
1500
+
1501
+ stats.batchesSent = batchResults.batchCount;
1502
+ stats.successCount = batchResults.batches.filter(b => b.status === 'SENT').length;
1503
+ stats.errorCount = batchResults.errors.length;
1504
+
1505
+ // ========================================
1506
+ // DELTA STATE UPDATE
1507
+ // ========================================
1508
+ if (config.enableDelta) {
1509
+ await jobTracker.updateJob(jobId, { status: 'updating_delta_state' });
1510
+ log.info('[MultiChannelSync] Updating delta state...');
1511
+
1512
+ const newState: any = {};
1513
+ for (const record of finalInventory) {
1514
+ if (!newState[record.skuRef]) {
1515
+ newState[record.skuRef] = {};
1516
+ }
1517
+ newState[record.skuRef][record.locationRef] = record.qty;
1518
+ }
1519
+
1520
+ await stateService.setState(config.deltaStateKey, newState, {
1521
+ ttlSeconds: 7 * 24 * 60 * 60, // 7 days
1522
+ });
1523
+
1524
+ log.info('[MultiChannelSync] Delta state updated');
1525
+ }
1526
+
1527
+ stats.duration = Date.now() - startTime;
1528
+
1529
+ log.info('[MultiChannelSync] Sync completed', { stats });
1530
+
1531
+ await jobTracker.markCompleted(jobId, {
1532
+ stats,
1533
+ jobId: job.id,
1534
+ });
1535
+
1536
+ return {
1537
+ success: stats.errorCount === 0,
1538
+ jobId: job.id,
1539
+ stats,
1540
+ };
1541
+ } catch (error: any) {
1542
+ // ✅ Enhanced error logging: Extract all error details for visibility
1543
+ const errorDetails = {
1544
+ message: error?.message || 'Unknown error',
1545
+ stack: error?.stack,
1546
+ fileName: error?.fileName,
1547
+ lineNumber: error?.lineNumber,
1548
+ originalError: error?.context?.originalError?.message,
1549
+ errorType: error?.name || 'Error',
1550
+ };
1551
+ log.error('[MultiChannelSync] Fatal error:', errorDetails);
1552
+ return { success: false, error: error.message, duration: Date.now() - startTime };
1553
+ }
1554
+ }
1555
+ ```
1556
+
1557
+ ---
1558
+
1559
+ ## Versori Activation Variables
1560
+
1561
+ ```bash
1562
+ # Required Variables
1563
+ retailerId=your-retailer-id
1564
+
1565
+ # Sync Configuration
1566
+ jobName=Multi-Channel Inventory Sync
1567
+ batchSize=500
1568
+ maxRecords=50000
1569
+ defaultBuffer=5
1570
+ oversellProtection=true
1571
+ useBpp=skip
1572
+
1573
+ # Delta Detection
1574
+ enableDelta=true
1575
+ deltaStateKey=sync:delta:state
1576
+
1577
+ # Channel A (REST API)
1578
+ channelAEnabled=true
1579
+ channelAUrl=https://api.channel-a.example.com/inventory
1580
+ channelAKey=your-api-key
1581
+ channelABuffer=5
1582
+ channelARateLimitRpm=120
1583
+
1584
+ # Channel B (S3 CSV)
1585
+ channelBEnabled=true
1586
+ channelBBucket=channel-b-inventory
1587
+ channelBKey=inventory/current.csv
1588
+
1589
+ # AWS Credentials (for Channel B S3)
1590
+ awsRegion=us-east-1
1591
+ awsAccessKeyId=AKIAXXXXXXXXXXXX
1592
+ awsSecretAccessKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
1593
+
1594
+ # Fluent GraphQL (current state comparison)
1595
+ fluentEnabled=true
1596
+ fluentPageSize=200
1597
+ ```
1598
+
1599
+ ---
1600
+
1601
+ ## Sample Channel Data
1602
+
1603
+ ### Channel A (REST API Response)
1604
+
1605
+ ```json
1606
+ {
1607
+ "inventory": [
1608
+ {
1609
+ "product_id": "SKU-12345",
1610
+ "warehouse_code": "LOC001",
1611
+ "quantity_available": 100,
1612
+ "quantity_reserved": 20,
1613
+ "updated_at": "2025-01-25T10:00:00Z"
1614
+ }
1615
+ ]
1616
+ }
1617
+ ```
1618
+
1619
+ ### Channel B (S3 CSV)
1620
+
1621
+ ```csv
1622
+ sku,location,qty,reserved
1623
+ SKU-12345,LOC001,95,15
1624
+ SKU-67890,LOC002,75,5
1625
+ SKU-11111,LOC001,200,0
1626
+ ```
1627
+
1628
+ ### ATP Calculation Example
1629
+
1630
+ ```typescript
1631
+ // Channel A: SKU-12345 at LOC001
1632
+ onHand = 100
1633
+ reserved = 20
1634
+ buffer = 5
1635
+ ATP = (100 - 20) - 5 = 75
1636
+
1637
+ // Channel B: SKU-12345 at LOC001
1638
+ onHand = 95
1639
+ reserved = 15
1640
+ buffer = 5
1641
+ ATP = (95 - 15) - 5 = 75
1642
+
1643
+ // Aggregated: SKU-12345 at LOC001
1644
+ totalOnHand = 100 + 95 = 195
1645
+ totalReserved = 20 + 15 = 35
1646
+ maxBuffer = max(5, 5) = 5
1647
+ finalATP = (195 - 35) - 5 = 155
1648
+ ```
1649
+
1650
+ ---
1651
+
1652
+ ## Deployment
1653
+
1654
+ ```bash
1655
+ # Install dependencies
1656
+ npm install
1657
+
1658
+ # Validate configuration
1659
+ npm run lint
1660
+
1661
+ # Deploy to Versori
1662
+ versori deploy
1663
+
1664
+ # View logs
1665
+ versori logs multi-channel-inventory-sync
1666
+
1667
+ # Trigger manual sync
1668
+ versori run adhocMultiChannelSync
1669
+ ```
1670
+
1671
+ ---
1672
+
1673
+ ## Testing
1674
+
1675
+ ### Test Scheduled Sync
1676
+
1677
+ Upload test CSV files to S3/SFTP for each channel and wait for the scheduled run.
1678
+
1679
+ **Check logs:**
1680
+
1681
+ ```
1682
+ [STEP 1/8] Initializing job tracking
1683
+ [STEP 2/8] Initializing Fluent Commerce client and data sources
1684
+ [STEP 3/8] Discovering files across channels
1685
+ [CHANNEL 1/3] Processing channel: CHANNEL_A
1686
+ [FILE 1/1] Processing file: channel-a-inventory_20250124.csv
1687
+ [STEP 4/8] Downloading and parsing: channel-a-inventory_20250124.csv
1688
+ [STEP 5/8] Transforming 5000 inventory records from channel-a-inventory_20250124.csv
1689
+ [STEP 6/8] Creating batch job and sending 5 batches to Fluent Commerce
1690
+ [STEP 7/8] Archiving file: channel-a-inventory_20250124.csv
1691
+ [STEP 8/8] Completing job and calculating totals
1692
+ ```
1693
+
1694
+ ### Test Ad hoc Sync
1695
+
1696
+ ```bash
1697
+ # Sync all channels
1698
+ curl -X POST https://api.versori.com/webhooks/multi-channel-sync-adhoc \
1699
+ -H "Content-Type: application/json" \
1700
+ -d '{}'
1701
+
1702
+ # Sync specific channel
1703
+ curl -X POST https://api.versori.com/webhooks/multi-channel-sync-adhoc \
1704
+ -H "Content-Type: application/json" \
1705
+ -d '{
1706
+ "channelId": "CHANNEL_A"
1707
+ }'
1708
+
1709
+ # Sync with specific pattern
1710
+ curl -X POST https://api.versori.com/webhooks/multi-channel-sync-adhoc \
1711
+ -H "Content-Type: application/json" \
1712
+ -d '{
1713
+ "filePattern": "urgent_*.csv",
1714
+ "channelId": "CHANNEL_B"
1715
+ }'
1716
+ ```
1717
+
1718
+ ### Test Job Status Query
1719
+
1720
+ ```bash
1721
+ curl -X POST https://api.versori.com/webhooks/multi-channel-sync-job-status \
1722
+ -H "Content-Type: application/json" \
1723
+ -d '{
1724
+ "jobId": "ADHOC_MULTI_20251024_183045_abc123"
1725
+ }'
1726
+ ```
1727
+
1728
+ ### Verify Batch Jobs in Fluent
1729
+
1730
+ After processing, check the Batch job status for each channel in Fluent Commerce:
1731
+
1732
+ ```bash
1733
+ # Query job status via GraphQL
1734
+ curl -X POST https://your-fluent-instance.com/graphql \
1735
+ -H "Authorization: Bearer YOUR_TOKEN" \
1736
+ -H "Content-Type: application/json" \
1737
+ -d '{
1738
+ "query": "query { job(id: \"job-123456\") { id status recordCount processedCount } }"
1739
+ }'
1740
+ ```
1741
+
1742
+ ---
1743
+
1744
+ ## Monitoring
1745
+
1746
+ ### Success Response
1747
+
1748
+ ```json
1749
+ {
1750
+ "success": true,
1751
+ "channelsProcessed": 3,
1752
+ "channelsFailed": 0,
1753
+ "filesProcessed": 3,
1754
+ "filesSkipped": 0,
1755
+ "filesFailed": 0,
1756
+ "results": [
1757
+ {
1758
+ "channel": "CHANNEL_A",
1759
+ "file": "channel-a-inventory_2025-01-22.csv",
1760
+ "success": true,
1761
+ "recordCount": 5000,
1762
+ "batchCount": 5,
1763
+ "jobId": "job-123456",
1764
+ "duration": 12345
1765
+ },
1766
+ {
1767
+ "channel": "CHANNEL_B",
1768
+ "file": "channel-b-inventory_2025-01-22.csv",
1769
+ "success": true,
1770
+ "recordCount": 3000,
1771
+ "batchCount": 3,
1772
+ "jobId": "job-123457",
1773
+ "duration": 9876
1774
+ },
1775
+ {
1776
+ "channel": "CHANNEL_C",
1777
+ "file": "channel-c-inventory_2025-01-22.csv",
1778
+ "success": true,
1779
+ "recordCount": 2000,
1780
+ "batchCount": 2,
1781
+ "jobId": "job-123458",
1782
+ "duration": 8765
1783
+ }
1784
+ ],
1785
+ "duration": 13456
1786
+ }
1787
+ ```
1788
+
1789
+ ### Partial Success Response
1790
+
1791
+ ```json
1792
+ {
1793
+ "success": true,
1794
+ "channelsProcessed": 2,
1795
+ "channelsFailed": 1,
1796
+ "filesProcessed": 2,
1797
+ "filesSkipped": 0,
1798
+ "filesFailed": 1,
1799
+ "results": [
1800
+ {
1801
+ "channel": "CHANNEL_A",
1802
+ "file": "channel-a-inventory_2025-01-22.csv",
1803
+ "success": true,
1804
+ "recordCount": 5000,
1805
+ "batchCount": 5,
1806
+ "jobId": "job-123456",
1807
+ "duration": 12345
1808
+ },
1809
+ {
1810
+ "channel": "CHANNEL_B",
1811
+ "file": "channel-b-inventory_2025-01-22.csv",
1812
+ "success": false,
1813
+ "error": "CSV parse error: Invalid structure"
1814
+ }
1815
+ ],
1816
+ "duration": 13456
1817
+ }
1818
+ ```
1819
+
1820
+ ### Error Response
1821
+
1822
+ ```json
1823
+ {
1824
+ "success": false,
1825
+ "channelsProcessed": 0,
1826
+ "channelsFailed": 3,
1827
+ "filesProcessed": 0,
1828
+ "filesFailed": 3,
1829
+ "results": [
1830
+ {
1831
+ "channel": "CHANNEL_A",
1832
+ "file": "channel-a-inventory_2025-01-22.csv",
1833
+ "success": false,
1834
+ "error": "Data source connection failed"
1835
+ }
1836
+ ],
1837
+ "duration": 876
1838
+ }
1839
+ ```
1840
+
1841
+ ### Monitoring Metrics
1842
+
1843
+ Monitor these metrics via Versori logs filtered by `jobId` or `stage`:
1844
+
1845
+ - **Channels Processed** - Total channels successfully processed
1846
+ - **Files Processed** - Total files successfully processed across all channels
1847
+ - **Batch Jobs Created** - Total Batch jobs created in Fluent Commerce (one per channel)
1848
+ - **Processing Duration** - Time taken for complete multi-channel sync
1849
+ - **Channel Failures** - Channels that failed (check individual channel errors)
1850
+
1851
+ Use the status webhook for dashboards and automated monitoring.
1852
+
1853
+ ---
1854
+
1855
+ - 🎯 **TRUE modular architecture** - Separate service files with clear responsibilities
1856
+ - 🎯 **Graceful degradation** - Use `Promise.allSettled()` for partial channel failures
1857
+ - 🎯 **Delta detection** - Only sync changed records (reduces API load by 90%+)
1858
+ - 🎯 **External JSON mapping** - Use `with { type: 'json' }` import syntax
1859
+ - 🎯 **ATP calculation** - `ATP = (onHand - reserved) - buffer` with oversell protection
1860
+ - 🎯 **Rate limiting** - Enforce minimum interval between Channel A requests
1861
+ - 🎯 **Skip BPP** - Set `preprocessing: 'skip'` when using delta detection
1862
+ - 🎯 **Job tracking** - Use `JobTracker` for lifecycle management
1863
+ - 🎯 **Native logging** - Use `log` from context on Versori platform
1864
+ - 🎯 **EntityType: INVENTORY** - Correct entity type for inventory records
1865
+ - 🎯 **Error handling** - Log channel failures but don't block entire sync
1866
+
1867
+ ---
1868
+
1869
+ ## Related Documentation
1870
+
1871
+ - **Single-source batch ingestion**: [SFTP XML Inventory Batch Template](./template-ingestion-sftp-xml-inventory-batch.md) - Simpler pattern for single data sources (GOLD STANDARD)
1872
+ - [Batch API Guide](../../../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md) - Complete Batch API patterns and BPP documentation
1873
+ - [State Management](../../../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md) - VersoriKVAdapter and StateService usage
1874
+ - [Job Tracker](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md) - Job lifecycle tracking
1875
+ - [Universal Mapping](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - Field transformation guide
1876
+ - [Error Handling Patterns](../../../../../03-PATTERN-GUIDES/error-handling/error-handling-readme.md) - Retry logic and exponential backoff
1877
+ - [File Operations](../../../../../03-PATTERN-GUIDES/file-operations/file-operations-readme.md) - KV state management patterns
1878
+ - [GraphQL Extraction](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - Auto-pagination for Fluent inventory queries
1879
+
1880
+ ---
1881
+
1882
+ [→ Back to Versori Scheduled Workflows](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) | [Versori Platform Guide →](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)