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