@fluentcommerce/fc-connect-sdk 0.1.53 → 0.1.55

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 (495) hide show
  1. package/CHANGELOG.md +30 -2
  2. package/README.md +39 -0
  3. package/dist/cjs/auth/index.d.ts +3 -0
  4. package/dist/cjs/auth/index.js +13 -0
  5. package/dist/cjs/auth/profile-loader.d.ts +18 -0
  6. package/dist/cjs/auth/profile-loader.js +208 -0
  7. package/dist/cjs/client-factory.d.ts +4 -0
  8. package/dist/cjs/client-factory.js +10 -0
  9. package/dist/cjs/clients/fluent-client.js +13 -6
  10. package/dist/cjs/index.d.ts +3 -1
  11. package/dist/cjs/index.js +8 -2
  12. package/dist/cjs/utils/pagination-helpers.js +38 -2
  13. package/dist/cjs/versori/fluent-versori-client.js +11 -5
  14. package/dist/esm/auth/index.d.ts +3 -0
  15. package/dist/esm/auth/index.js +2 -0
  16. package/dist/esm/auth/profile-loader.d.ts +18 -0
  17. package/dist/esm/auth/profile-loader.js +169 -0
  18. package/dist/esm/client-factory.d.ts +4 -0
  19. package/dist/esm/client-factory.js +9 -0
  20. package/dist/esm/clients/fluent-client.js +13 -6
  21. package/dist/esm/index.d.ts +3 -1
  22. package/dist/esm/index.js +2 -1
  23. package/dist/esm/utils/pagination-helpers.js +38 -2
  24. package/dist/esm/versori/fluent-versori-client.js +11 -5
  25. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/dist/tsconfig.types.tsbuildinfo +1 -1
  28. package/dist/types/auth/index.d.ts +3 -0
  29. package/dist/types/auth/profile-loader.d.ts +18 -0
  30. package/dist/types/client-factory.d.ts +4 -0
  31. package/dist/types/index.d.ts +3 -1
  32. package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
  33. package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
  34. package/docs/00-START-HERE/cli-documentation-index.md +202 -202
  35. package/docs/00-START-HERE/cli-quick-reference.md +252 -252
  36. package/docs/00-START-HERE/decision-tree.md +552 -552
  37. package/docs/00-START-HERE/getting-started.md +1070 -1070
  38. package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
  39. package/docs/00-START-HERE/readme.md +237 -237
  40. package/docs/00-START-HERE/retailerid-configuration.md +404 -404
  41. package/docs/00-START-HERE/sdk-philosophy.md +794 -794
  42. package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
  43. package/docs/01-TEMPLATES/faq.md +686 -686
  44. package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
  45. package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
  46. package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
  47. package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
  48. package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
  49. package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
  50. package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
  51. package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
  52. package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
  53. package/docs/01-TEMPLATES/readme.md +957 -957
  54. package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
  55. package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
  56. package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
  57. package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
  58. package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
  59. package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
  60. package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
  61. package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
  62. package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
  63. package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
  64. package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
  65. package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
  66. package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
  67. package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
  68. package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
  69. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
  70. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
  71. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
  72. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
  73. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
  74. package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
  75. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
  76. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
  77. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
  78. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
  79. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
  80. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
  81. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
  82. package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
  83. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
  84. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
  85. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
  86. package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
  87. package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
  88. package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
  89. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
  90. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
  91. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
  92. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
  93. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
  94. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
  95. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
  96. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
  97. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
  98. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
  99. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
  100. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
  101. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
  102. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
  103. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
  104. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
  105. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
  106. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
  107. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
  108. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
  109. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
  110. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
  111. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
  112. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
  113. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
  114. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
  115. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
  116. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
  117. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
  118. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
  119. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
  120. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
  121. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
  122. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
  123. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
  124. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
  125. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
  126. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
  127. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
  128. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
  129. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
  130. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
  131. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
  132. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
  133. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
  134. package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
  135. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
  136. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
  137. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
  138. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
  139. package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
  140. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
  141. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
  142. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
  143. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
  144. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
  145. package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
  146. package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
  147. package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
  148. package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
  149. package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
  150. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -482
  151. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
  152. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
  153. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
  154. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
  155. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
  156. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
  157. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
  158. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
  159. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
  160. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
  161. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
  162. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
  163. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
  164. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
  165. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
  166. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
  167. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
  168. package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
  169. package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
  170. package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
  171. package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
  172. package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
  173. package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
  174. package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
  175. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
  176. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
  177. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
  178. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
  179. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
  180. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
  181. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
  182. package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
  183. package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
  184. package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
  185. package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
  186. package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
  187. package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
  188. package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
  189. package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
  190. package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
  191. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
  192. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
  193. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
  194. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
  195. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
  196. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
  197. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
  198. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
  199. package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
  200. package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
  201. package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
  202. package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
  203. package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
  204. package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
  205. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
  206. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
  207. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
  208. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
  209. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
  210. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
  211. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
  212. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
  213. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
  214. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
  215. package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
  216. package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
  217. package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
  218. package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
  219. package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
  220. package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
  221. package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
  222. package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
  223. package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
  224. package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
  225. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
  226. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
  227. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
  228. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
  229. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
  230. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
  231. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
  232. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
  233. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
  234. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
  235. package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
  236. package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
  237. package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
  238. package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
  239. package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
  240. package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
  241. package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
  242. package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
  243. package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
  244. package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
  245. package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
  246. package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
  247. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
  248. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
  249. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
  250. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
  251. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
  252. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
  253. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
  254. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
  255. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
  256. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
  257. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
  258. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
  259. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
  260. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
  261. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
  262. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
  263. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
  264. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
  265. package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
  266. package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
  267. package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
  268. package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
  269. package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
  270. package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
  271. package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
  272. package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
  273. package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
  274. package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
  275. package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
  276. package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
  277. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
  278. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
  279. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
  280. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
  281. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
  282. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
  283. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
  284. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
  285. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
  286. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
  287. package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
  288. package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
  289. package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
  290. package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
  291. package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
  292. package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
  293. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
  294. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
  295. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
  296. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
  297. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
  298. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
  299. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
  300. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
  301. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
  302. package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
  303. package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
  304. package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
  305. package/docs/02-CORE-GUIDES/readme.md +194 -194
  306. package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
  307. package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
  308. package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
  309. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
  310. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
  311. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
  312. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
  313. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
  314. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
  315. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
  316. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
  317. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
  318. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
  319. package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
  320. package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
  321. package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
  322. package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
  323. package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
  324. package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
  325. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
  326. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
  327. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
  328. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
  329. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
  330. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
  331. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
  332. package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
  333. package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
  334. package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
  335. package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
  336. package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
  337. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
  338. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
  339. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
  340. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
  341. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
  342. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
  343. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
  344. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
  345. package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
  346. package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
  347. package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
  348. package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
  349. package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
  350. package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
  351. package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
  352. package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
  353. package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
  354. package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
  355. package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
  356. package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
  357. package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
  358. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
  359. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
  360. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
  361. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
  362. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
  363. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
  364. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
  365. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
  366. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
  367. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
  368. package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
  369. package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
  370. package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
  371. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
  372. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
  373. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
  374. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
  375. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
  376. package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
  377. package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
  378. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
  379. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
  380. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
  381. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
  382. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
  383. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
  384. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
  385. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
  386. package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
  387. package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
  388. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
  389. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
  390. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
  391. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
  392. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
  393. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
  394. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
  395. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
  396. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
  397. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
  398. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
  399. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
  400. package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
  401. package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
  402. package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
  403. package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
  404. package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
  405. package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
  406. package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
  407. package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
  408. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
  409. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
  410. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
  411. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
  412. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
  413. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
  414. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
  415. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
  416. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
  417. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
  418. package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
  419. package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
  420. package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
  421. package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
  422. package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
  423. package/docs/03-PATTERN-GUIDES/readme.md +159 -159
  424. package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
  425. package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
  426. package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
  427. package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
  428. package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
  429. package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
  430. package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
  431. package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
  432. package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
  433. package/docs/04-REFERENCE/architecture/readme.md +279 -279
  434. package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
  435. package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
  436. package/docs/04-REFERENCE/platforms/readme.md +135 -135
  437. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
  438. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
  439. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
  440. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
  441. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
  442. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
  443. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
  444. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
  445. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
  446. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
  447. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
  448. package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
  449. package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
  450. package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
  451. package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
  452. package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
  453. package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
  454. package/docs/04-REFERENCE/readme.md +148 -148
  455. package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
  456. package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
  457. package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
  458. package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
  459. package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
  460. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
  461. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
  462. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
  463. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
  464. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
  465. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
  466. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
  467. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
  468. package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
  469. package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
  470. package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
  471. package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
  472. package/docs/04-REFERENCE/schema/readme.md +141 -141
  473. package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
  474. package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
  475. package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
  476. package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
  477. package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
  478. package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
  479. package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
  480. package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
  481. package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
  482. package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
  483. package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
  484. package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
  485. package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
  486. package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
  487. package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
  488. package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
  489. package/docs/04-REFERENCE/testing/readme.md +86 -86
  490. package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
  491. package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
  492. package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
  493. package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
  494. package/docs/template-loading-matrix.md +242 -242
  495. package/package.json +5 -3
@@ -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)