@fluentcommerce/fc-connect-sdk 0.1.53 → 0.1.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (495) hide show
  1. package/CHANGELOG.md +30 -2
  2. package/README.md +39 -0
  3. package/dist/cjs/auth/index.d.ts +3 -0
  4. package/dist/cjs/auth/index.js +13 -0
  5. package/dist/cjs/auth/profile-loader.d.ts +18 -0
  6. package/dist/cjs/auth/profile-loader.js +208 -0
  7. package/dist/cjs/client-factory.d.ts +4 -0
  8. package/dist/cjs/client-factory.js +10 -0
  9. package/dist/cjs/clients/fluent-client.js +13 -6
  10. package/dist/cjs/index.d.ts +3 -1
  11. package/dist/cjs/index.js +8 -2
  12. package/dist/cjs/utils/pagination-helpers.js +38 -2
  13. package/dist/cjs/versori/fluent-versori-client.js +11 -5
  14. package/dist/esm/auth/index.d.ts +3 -0
  15. package/dist/esm/auth/index.js +2 -0
  16. package/dist/esm/auth/profile-loader.d.ts +18 -0
  17. package/dist/esm/auth/profile-loader.js +169 -0
  18. package/dist/esm/client-factory.d.ts +4 -0
  19. package/dist/esm/client-factory.js +9 -0
  20. package/dist/esm/clients/fluent-client.js +13 -6
  21. package/dist/esm/index.d.ts +3 -1
  22. package/dist/esm/index.js +2 -1
  23. package/dist/esm/utils/pagination-helpers.js +38 -2
  24. package/dist/esm/versori/fluent-versori-client.js +11 -5
  25. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/dist/tsconfig.types.tsbuildinfo +1 -1
  28. package/dist/types/auth/index.d.ts +3 -0
  29. package/dist/types/auth/profile-loader.d.ts +18 -0
  30. package/dist/types/client-factory.d.ts +4 -0
  31. package/dist/types/index.d.ts +3 -1
  32. package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
  33. package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
  34. package/docs/00-START-HERE/cli-documentation-index.md +202 -202
  35. package/docs/00-START-HERE/cli-quick-reference.md +252 -252
  36. package/docs/00-START-HERE/decision-tree.md +552 -552
  37. package/docs/00-START-HERE/getting-started.md +1070 -1070
  38. package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
  39. package/docs/00-START-HERE/readme.md +237 -237
  40. package/docs/00-START-HERE/retailerid-configuration.md +404 -404
  41. package/docs/00-START-HERE/sdk-philosophy.md +794 -794
  42. package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
  43. package/docs/01-TEMPLATES/faq.md +686 -686
  44. package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
  45. package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
  46. package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
  47. package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
  48. package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
  49. package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
  50. package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
  51. package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
  52. package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
  53. package/docs/01-TEMPLATES/readme.md +957 -957
  54. package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
  55. package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
  56. package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
  57. package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
  58. package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
  59. package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
  60. package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
  61. package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
  62. package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
  63. package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
  64. package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
  65. package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
  66. package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
  67. package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
  68. package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
  69. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
  70. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
  71. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
  72. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
  73. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
  74. package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
  75. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
  76. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
  77. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
  78. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
  79. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
  80. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
  81. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
  82. package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
  83. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
  84. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
  85. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
  86. package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
  87. package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
  88. package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
  89. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
  90. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
  91. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
  92. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
  93. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
  94. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
  95. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
  96. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
  97. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
  98. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
  99. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
  100. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
  101. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
  102. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
  103. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
  104. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
  105. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
  106. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
  107. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
  108. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
  109. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
  110. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
  111. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
  112. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
  113. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
  114. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
  115. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
  116. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
  117. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
  118. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
  119. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
  120. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
  121. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
  122. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
  123. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
  124. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
  125. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
  126. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
  127. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
  128. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
  129. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
  130. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
  131. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
  132. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
  133. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
  134. package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
  135. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
  136. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
  137. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
  138. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
  139. package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
  140. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
  141. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
  142. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
  143. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
  144. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
  145. package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
  146. package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
  147. package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
  148. package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
  149. package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
  150. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -482
  151. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
  152. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
  153. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
  154. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
  155. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
  156. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
  157. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
  158. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
  159. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
  160. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
  161. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
  162. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
  163. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
  164. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
  165. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
  166. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
  167. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
  168. package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
  169. package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
  170. package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
  171. package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
  172. package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
  173. package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
  174. package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
  175. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
  176. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
  177. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
  178. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
  179. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
  180. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
  181. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
  182. package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
  183. package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
  184. package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
  185. package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
  186. package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
  187. package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
  188. package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
  189. package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
  190. package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
  191. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
  192. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
  193. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
  194. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
  195. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
  196. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
  197. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
  198. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
  199. package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
  200. package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
  201. package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
  202. package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
  203. package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
  204. package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
  205. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
  206. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
  207. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
  208. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
  209. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
  210. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
  211. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
  212. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
  213. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
  214. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
  215. package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
  216. package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
  217. package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
  218. package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
  219. package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
  220. package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
  221. package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
  222. package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
  223. package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
  224. package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
  225. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
  226. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
  227. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
  228. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
  229. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
  230. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
  231. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
  232. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
  233. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
  234. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
  235. package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
  236. package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
  237. package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
  238. package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
  239. package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
  240. package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
  241. package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
  242. package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
  243. package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
  244. package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
  245. package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
  246. package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
  247. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
  248. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
  249. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
  250. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
  251. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
  252. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
  253. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
  254. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
  255. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
  256. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
  257. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
  258. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
  259. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
  260. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
  261. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
  262. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
  263. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
  264. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
  265. package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
  266. package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
  267. package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
  268. package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
  269. package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
  270. package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
  271. package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
  272. package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
  273. package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
  274. package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
  275. package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
  276. package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
  277. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
  278. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
  279. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
  280. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
  281. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
  282. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
  283. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
  284. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
  285. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
  286. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
  287. package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
  288. package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
  289. package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
  290. package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
  291. package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
  292. package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
  293. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
  294. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
  295. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
  296. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
  297. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
  298. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
  299. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
  300. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
  301. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
  302. package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
  303. package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
  304. package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
  305. package/docs/02-CORE-GUIDES/readme.md +194 -194
  306. package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
  307. package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
  308. package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
  309. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
  310. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
  311. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
  312. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
  313. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
  314. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
  315. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
  316. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
  317. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
  318. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
  319. package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
  320. package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
  321. package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
  322. package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
  323. package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
  324. package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
  325. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
  326. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
  327. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
  328. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
  329. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
  330. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
  331. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
  332. package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
  333. package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
  334. package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
  335. package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
  336. package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
  337. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
  338. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
  339. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
  340. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
  341. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
  342. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
  343. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
  344. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
  345. package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
  346. package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
  347. package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
  348. package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
  349. package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
  350. package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
  351. package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
  352. package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
  353. package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
  354. package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
  355. package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
  356. package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
  357. package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
  358. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
  359. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
  360. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
  361. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
  362. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
  363. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
  364. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
  365. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
  366. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
  367. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
  368. package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
  369. package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
  370. package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
  371. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
  372. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
  373. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
  374. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
  375. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
  376. package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
  377. package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
  378. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
  379. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
  380. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
  381. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
  382. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
  383. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
  384. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
  385. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
  386. package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
  387. package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
  388. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
  389. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
  390. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
  391. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
  392. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
  393. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
  394. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
  395. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
  396. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
  397. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
  398. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
  399. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
  400. package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
  401. package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
  402. package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
  403. package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
  404. package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
  405. package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
  406. package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
  407. package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
  408. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
  409. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
  410. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
  411. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
  412. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
  413. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
  414. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
  415. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
  416. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
  417. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
  418. package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
  419. package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
  420. package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
  421. package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
  422. package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
  423. package/docs/03-PATTERN-GUIDES/readme.md +159 -159
  424. package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
  425. package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
  426. package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
  427. package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
  428. package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
  429. package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
  430. package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
  431. package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
  432. package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
  433. package/docs/04-REFERENCE/architecture/readme.md +279 -279
  434. package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
  435. package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
  436. package/docs/04-REFERENCE/platforms/readme.md +135 -135
  437. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
  438. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
  439. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
  440. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
  441. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
  442. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
  443. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
  444. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
  445. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
  446. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
  447. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
  448. package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
  449. package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
  450. package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
  451. package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
  452. package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
  453. package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
  454. package/docs/04-REFERENCE/readme.md +148 -148
  455. package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
  456. package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
  457. package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
  458. package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
  459. package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
  460. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
  461. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
  462. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
  463. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
  464. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
  465. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
  466. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
  467. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
  468. package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
  469. package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
  470. package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
  471. package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
  472. package/docs/04-REFERENCE/schema/readme.md +141 -141
  473. package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
  474. package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
  475. package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
  476. package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
  477. package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
  478. package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
  479. package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
  480. package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
  481. package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
  482. package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
  483. package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
  484. package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
  485. package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
  486. package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
  487. package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
  488. package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
  489. package/docs/04-REFERENCE/testing/readme.md +86 -86
  490. package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
  491. package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
  492. package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
  493. package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
  494. package/docs/template-loading-matrix.md +242 -242
  495. package/package.json +5 -3
@@ -1,1906 +1,1906 @@
1
- ---
2
- template_id: tpl-webhook-asn-purchase-order
3
- canonical_filename: template-webhook-asn-purchase-order.md
4
- version: 2.0.0
5
- sdk_version: ^0.1.39
6
- runtime: versori
7
- direction: ingestion
8
- source: webhook-xml-json-asn
9
- destination: fluent-graphql
10
- entity: inventory-receipt
11
- format: xml-json
12
- logging: versori
13
- status: stable
14
- features:
15
- - webhook-signature-validation
16
- - batched-events
17
- - attribute-transformation
18
- - memory-management
19
- - enhanced-logging
20
- ---
21
-
22
- # Template: Webhook - ASN & Purchase Order Processing
23
-
24
- **Template Version:** 2.0.0
25
- **SDK Version:** @fluentcommerce/fc-connect-sdk@^0.1.39
26
- **Last Updated:** 2025-01-24
27
- **Deployment Target:** Versori Platform
28
-
29
- **🆕 Version 2.0.0 Enhancements:**
30
- - ✅ **Webhook Signature Validation** - Secure webhook verification with HMAC-SHA256
31
- - ✅ **Batched Events** - Process events in optimized batches to reduce API calls
32
- - ✅ **Attribute Transformation** - Handle nested arrays and complex data structures
33
- - ✅ **Memory Management** - Clear large arrays after processing batches
34
- - ✅ **Enhanced Logging** - Track batch processing and event submission with emoji indicators
35
-
36
- **FC Connect SDK Use Case Guide**
37
-
38
- > **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
39
- > **Version**: Use latest - `npm install @fluentcommerce/fc-connect-sdk@latest`
40
-
41
- **Context**: Process Advanced Ship Notices (ASN) from 3PL warehouse and create expected inventory receipts in Fluent Commerce
42
-
43
- **Complexity**: Medium-High
44
-
45
- **Runtime**: Versori Platform
46
-
47
- **Estimated Lines**: ~750 lines (modular structure)
48
-
49
- ---
50
-
51
- ## STEP 1: Understand This Template
52
-
53
- **What This Template Does:**
54
-
55
- - HTTP webhook endpoint receiving ASN data from 3PL/WMS
56
- - XML/JSON parsing for ASN payload (EDI 856 compatible structure)
57
- - GraphQL mutation to create expected inventory receipts
58
- - Container/pallet tracking support
59
- - Cross-dock scenario handling (direct fulfillment without storage)
60
- - Expected date calculation (carrier transit time)
61
- - Custom resolvers for address normalization and SKU validation
62
- - Audit trail and notification system
63
- - Error handling for duplicate ASN prevention
64
- - **Sync + Fire-and-Forget Pattern**: Fast webhook response, background processing
65
-
66
- **Key SDK Components:**
67
-
68
- - `createClient()` - Universal client factory (auto-detects Versori context)
69
- - `GraphQLMutationMapper` - ASN → GraphQL mutation mapping
70
- - `XMLParserService` / `JSONParserService` - ASN parsing (EDI 856 format)
71
- - Native Versori `log` - Use `log` from context
72
-
73
- **Entity Type:**
74
-
75
- - **InventoryReceipt** - Fluent entity for expected inventory
76
- - **GraphQL Mutation** - Uses `createInventoryReceipt` mutation
77
-
78
- **Critical Patterns:**
79
-
80
- - **Sync + Fire-and-Forget**: Webhook validates quickly, returns immediately, processes ASN in background
81
- - **External JSON Config**: Mapping configuration in separate JSON file (`config/asn-mapping.json`)
82
- - **Modular Architecture**: Separate services, workflows, config, types folders
83
- - **Background Processing**: Long-running operations (GraphQL mutations, validation) happen asynchronously
84
- - **Error Handling**: Comprehensive error handling with duplicate detection
85
-
86
- **When to Use This Template:**
87
-
88
- - ✅ ASN processing from 3PL/WMS systems
89
- - ✅ EDI 856 format (XML or JSON)
90
- - ✅ Need fast webhook response (don't wait for receipt creation)
91
- - ✅ Container/pallet tracking
92
- - ✅ Cross-dock scenarios
93
-
94
- **When NOT to Use:**
95
-
96
- - ❌ Bulk ASN processing (use Batch API or scheduled workflows)
97
- - ❌ Real-time inventory updates (use Event API)
98
- - ❌ Need synchronous receipt creation (wait for result before responding)
99
-
100
- ---
101
-
102
- ## STEP 2: Implementation Prompt for Claude Code
103
-
104
- **Copy this prompt and send to Claude Code to generate the complete implementation:**
105
-
106
- ```
107
- Create a Versori webhook workflow for ASN (Advanced Ship Notice) processing to Fluent Commerce.
108
-
109
- REQUIREMENTS:
110
- 1. Runtime: Versori Platform (HTTP webhook)
111
- 2. Source: ASN data via HTTP POST webhook (XML or JSON, EDI 856 format)
112
- 3. Destination: Fluent Commerce GraphQL API (createInventoryReceipt mutation)
113
- 4. Format: XML or JSON (EDI 856 compatible)
114
- 5. Entity: InventoryReceipt (GraphQL mutation)
115
-
116
- KEY FEATURES:
117
- - Sync + fire-and-forget pattern (fast webhook response, background processing)
118
- - External JSON mapping configuration (config/asn-mapping.json)
119
- - Modular architecture (workflows/, services/, config/, types/)
120
- - XML/JSON parsing with EDI 856 structure support
121
- - GraphQLMutationMapper for ASN → GraphQL transformation
122
- - Custom resolvers for address normalization and SKU validation
123
- - Duplicate ASN detection and prevention
124
- - Audit trail (save input/output files)
125
-
126
- CRITICAL REQUIREMENTS:
127
- 1. Webhook Mode: response: { mode: 'sync' } (fast response)
128
- 2. Background Processing: Fire-and-forget pattern (no await on long operations)
129
- 3. Mapping Config: External JSON file (config/asn-mapping.json)
130
- 4. Modular Structure: Separate services/, config/, types/ folders
131
- 5. Native Logging: Use log from context (no LoggingService)
132
- 6. Error Handling: Duplicate detection, comprehensive error responses
133
-
134
- SDK METHODS TO USE:
135
- - createClient({ ...ctx, log }) - Pass full Versori context
136
- - new GraphQLMutationMapper(mappingConfig, log, { fluentClient: client }) - Initialize mapper
137
- - mapper.mapWithNodes(asnData, customResolvers, context) - Map ASN with custom resolvers (REQUIRED for custom resolvers)
138
- - mapWithNodes() now returns query automatically (no need for buildMutation())
139
- - client.graphql({ query, variables }) - Execute GraphQL mutation
140
-
141
- FORBIDDEN PATTERNS:
142
- - ❌ Inline mapping config (use external JSON)
143
- - ❌ await on background processing (use fire-and-forget)
144
- - ❌ LoggingService (use native log from context)
145
- - ❌ All code in one file (use modular structure)
146
- - ❌ async mode webhook (use sync + fire-and-forget)
147
- ```
148
-
149
- ---
150
-
151
- ## STEP 3: Detailed Flow Documentation
152
-
153
- ### Complete Processing Flow
154
-
155
- ```
156
- ┌─────────────────────────────────────────────────────────────┐
157
- │ 1. WEBHOOK RECEIVED │
158
- │ POST https://{workspace}.versori.run/process-asn │
159
- │ Content-Type: application/xml or application/json │
160
- │ Body: <ShipNotice>...</ShipNotice> or { ... } │
161
- └────────────────────┬────────────────────────────────────────┘
162
-
163
-
164
- ┌─────────────────────────────────────────────────────────────┐
165
- │ 2. QUICK VALIDATION (Synchronous, ~10-50ms) │
166
- │ - Check fluent_commerce connection exists │
167
- │ - Validate ASN payload present │
168
- │ - Return HTTP 200 OK immediately │
169
- └────────────────────┬────────────────────────────────────────┘
170
-
171
-
172
- ┌─────────────────────────────────────────────────────────────┐
173
- │ 3. BACKGROUND PROCESSING (Fire-and-Forget) │
174
- │ ┌─────────────────────────────────────────────────────┐ │
175
- │ │ 3a. Initialize Fluent Client │ │
176
- │ │ - createClient({ ...ctx, log }) │ │
177
- │ └─────────────────────────────────────────────────────┘ │
178
- │ ┌─────────────────────────────────────────────────────┐ │
179
- │ │ 3b. Parse ASN (XML or JSON) │ │
180
- │ │ - XMLParserService or JSONParserService │ │
181
- │ │ - Extract ASN identifier │ │
182
- │ └─────────────────────────────────────────────────────┘ │
183
- │ ┌─────────────────────────────────────────────────────┐ │
184
- │ │ 3c. Check for Duplicates │ │
185
- │ │ - Query Fluent for existing receipt │ │
186
- │ │ - Return early if duplicate │ │
187
- │ └─────────────────────────────────────────────────────┘ │
188
- │ ┌─────────────────────────────────────────────────────┐ │
189
- │ │ 3d. Map ASN to GraphQL Variables │ │
190
- │ │ - Load mapping config from JSON │ │
191
- │ │ - GraphQLMutationMapper.map() │ │
192
- │ │ - Apply custom resolvers │ │
193
- │ │ - Returns { success, data, query, context } │ │
194
- │ └─────────────────────────────────────────────────────┘ │
195
- │ ┌─────────────────────────────────────────────────────┐ │
196
- │ │ 3e. Execute GraphQL Mutation │ │
197
- │ │ - client.graphql({ query: result.query, │ │
198
- │ │ variables: result.variables })│ │
199
- │ └─────────────────────────────────────────────────────┘ │
200
- │ ┌─────────────────────────────────────────────────────┐ │
201
- │ │ 3g. Save Audit Trail (Optional) │ │
202
- │ │ - asn-input.json │ │
203
- │ │ - mapped-variables.json │ │
204
- │ │ - graphql-response.json │ │
205
- │ └─────────────────────────────────────────────────────┘ │
206
- └─────────────────────────────────────────────────────────────┘
207
- ```
208
-
209
- ### Response Timing
210
-
211
- | Stage | Timing | Blocking |
212
- |-------|--------|----------|
213
- | **Webhook Validation** | ~10-50ms | ✅ Yes (blocks response) |
214
- | **Background Processing** | ~1000-3000ms | ❌ No (fire-and-forget) |
215
- | **Total Response Time** | ~10-50ms | ✅ Fast response |
216
-
217
- **Key Benefit**: Webhook caller receives immediate acknowledgment (~50ms) while ASN processing happens in background (~2-3s).
218
-
219
- ---
220
-
221
- ## STEP 4: Production Modular Structure
222
-
223
- > **✅ This section shows the COMPLETE production-ready modular structure.**
224
- > All files are shown with proper imports/exports and folder organization.
225
-
226
- ### Complete Project Structure
227
-
228
- ```
229
- asn-purchase-order-processing/
230
- ├── package.json # Dependencies and Versori config
231
- ├── index.ts # Entry point - exports all workflows
232
- └── src/
233
- ├── workflows/
234
- │ └── webhook/
235
- │ └── asn-receipt.ts # Webhook: Receive ASN notifications
236
-
237
- ├── services/
238
- │ └── asn-processing.service.ts # Shared orchestration logic (reusable)
239
-
240
- ├── resolvers/
241
- │ └── asn-resolvers.ts # Custom resolvers for transformations
242
-
243
- ├── config/
244
- │ └── asn-mapping.json # Mapping configuration (external JSON)
245
-
246
- └── types/
247
- └── asn.types.ts # TypeScript interfaces
248
- ```
249
-
250
- **Why This Structure?**
251
-
252
- - ✅ **Clear separation**: Webhook handlers vs business logic
253
- - ✅ **Reusable services**: ASN processing logic can be reused
254
- - ✅ **External config**: Mapping changes don't require code changes
255
- - ✅ **Custom resolvers**: Separate file for complex transformations
256
- - ✅ **Type safety**: TypeScript interfaces for better IDE support
257
- - ✅ **Scalable**: Easy to add new ASN formats or processing steps
258
-
259
- ---
260
-
261
- ## SDK Methods Used
262
-
263
- ```typescript
264
- // Core SDK imports
265
- import {
266
- createClient,
267
- GraphQLMutationMapper,
268
- XMLParserService,
269
- JSONParserService,
270
- } from '@fluentcommerce/fc-connect-sdk';
271
-
272
- // Versori imports
273
- import { webhook } from '@versori/run';
274
- import { Buffer } from 'node:buffer'; // Required for Versori runtime
275
-
276
- // Key methods
277
- const logger = { info, warn, error, debug }; // Map Versori log to SDK Logger interface
278
- const client = await createClient({ ...ctx, log: logger }); // Auto-detects Versori context
279
- new GraphQLMutationMapper(config, logger, { fluentClient: client }); // Map ASN to GraphQL (uses logger adapter)
280
- new XMLParserService(); // Parse ASN XML (EDI 856)
281
- mapper.mapWithNodes(asnData, resolvers, context); // Apply mapping with custom logic
282
- // mapWithNodes() now returns query automatically - use result.query
283
- client.graphql({ query, variables }); // Execute mutation
284
- ```
285
-
286
- ---
287
-
288
- ## Versori Workflows Structure
289
-
290
- **Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
291
-
292
- **Trigger Types:**
293
- - **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
294
- - **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
295
- - **`http()`** → External API calls (chained from webhook/schedule)
296
- - **`fn()`** → Internal processing (chained from webhook/schedule)
297
-
298
- ### Recommended Project Structure
299
-
300
- ```
301
- asn-purchase-order-processing/
302
- ├── index.ts # Entry point - exports all workflows
303
- └── src/
304
- ├── workflows/
305
- │ └── webhook/
306
- │ └── asn-receipt.ts # Webhook: Receive ASN notifications
307
-
308
- ├── services/
309
- │ └── asn-processing.service.ts # Shared orchestration logic (reusable)
310
-
311
- └── config/
312
- └── asn-mapping.json # Mapping configuration
313
- ```
314
-
315
- **Benefits:**
316
- - ✅ Clear trigger separation (`webhook/` vs `scheduled/`)
317
- - ✅ Descriptive file names (easy to browse and understand)
318
- - ✅ Scalable (add new workflows without cluttering)
319
- - ✅ Reusable code in `services/` (DRY principle)
320
- - ✅ Easy to modify individual workflows without affecting others
321
-
322
- ---
323
-
324
- ## Complete Working Code
325
-
326
- ### 1. Entry Point: `index.ts`
327
-
328
- ```typescript
329
- /**
330
- * Entry point - Export all workflows for Versori platform
331
- *
332
- * This file exports all workflows to be registered with Versori.
333
- * Each workflow is defined in its own file for better organization.
334
- *
335
- * MEMORY INTERPRETER PATTERN:
336
- * Versori's interpreter reads this file and registers all exported workflows.
337
- * Do NOT use dynamic imports or conditional exports.
338
- */
339
-
340
- // Webhook workflows
341
- export { processAsnWebhook } from './workflows/webhook/asn-receipt';
342
- export { manualAsnTest } from './workflows/webhook/asn-receipt';
343
- export { healthCheck } from './workflows/webhook/asn-receipt';
344
- ```
345
-
346
- ### 2. Main Webhook Workflow: `workflows/webhook/asn-receipt.ts`
347
-
348
- ```typescript
349
- /**
350
- * ASN (Advanced Ship Notice) Processing Webhook
351
- *
352
- * Receives ASN notifications from Acme 3PL/WMS and creates expected inventory
353
- * receipts in Fluent Commerce.
354
- *
355
- * ASN = Advanced Ship Notice (EDI 856) - notification that shipment is on the way
356
- *
357
- * Flow:
358
- * 1. Receive ASN webhook (XML or JSON payload from 3PL)
359
- * 2. Parse ASN structure (shipment header, containers, items)
360
- * 3. Map to Fluent expected receipt format
361
- * 4. Calculate expected arrival date (based on carrier and transit time)
362
- * 5. Create inventory receipt mutation (creates expected inventory)
363
- * 6. Send confirmation response to 3PL
364
- * 7. Optional: Send notification to warehouse ops team
365
- */
366
- import { webhook } from '@versori/run';
367
- import { Buffer } from 'node:buffer'; // Required for Versori runtime
368
- import {
369
- createClient,
370
- GraphQLMutationMapper,
371
- XMLParserService,
372
- JSONParserService,
373
- } from '@fluentcommerce/fc-connect-sdk';
374
- import { asnResolvers } from '../../resolvers/asn-resolvers';
375
- import asnMappingConfig from '../../mappings/asn-to-fluent-receipt.json' with { type: 'json' };
376
-
377
- /**
378
- * Main ASN webhook endpoint
379
- * Expects POST with XML or JSON ASN payload
380
- */
381
- export const processAsnWebhook = webhook('process-asn', {
382
- response: {
383
- mode: 'sync',
384
- onSuccess: (ctx) => new Response(JSON.stringify(ctx.data), {
385
- status: 200,
386
- headers: { 'Content-Type': 'application/json' }
387
- }),
388
- onError: (ctx, error) => new Response(JSON.stringify({
389
- success: false,
390
- error: error.message,
391
- recommendation: error.message?.includes('authentication') || error.message?.includes('401')
392
- ? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
393
- : error.message?.includes('mapping') || error.message?.includes('field')
394
- ? 'Check mapping configuration JSON and verify source paths match incoming ASN structure'
395
- : error.message?.includes('connection') || error.message?.includes('timeout')
396
- ? 'Check network connectivity and Fluent Commerce API availability'
397
- : 'Review error details and check ASN payload structure',
398
- timestamp: new Date().toISOString()
399
- }), {
400
- status: 500,
401
- headers: { 'Content-Type': 'application/json' }
402
- })
403
- }
404
- }, async (ctx) => {
405
- const { log, fetch, activation, connections, openKv } = ctx;
406
- const startTime = Date.now();
407
-
408
- log.info('🚚 [ASN] Processing Advanced Ship Notice');
409
-
410
- // ? Enhanced: Configuration validation
411
- if (!connections || !connections.fluent_commerce) {
412
- log.error('❌ [ASN] Configuration error: Missing fluent_commerce connection', {
413
- recommendation: 'Configure fluent_commerce connection in Connections section with OAuth2 credentials'
414
- });
415
- throw new Error('Missing fluent_commerce connection');
416
- }
417
-
418
- try {
419
- // =================================================================
420
- // STEP 1: EXTRACT AND PARSE ASN PAYLOAD
421
- // =================================================================
422
- // Get webhook payload
423
- // Supports both XML (EDI 856 format) and JSON
424
- const rawPayload = activation?.body;
425
- const contentType = activation?.headers?.['content-type'] || 'application/json';
426
-
427
- log.info('📦 [ASN] Payload received', {
428
- contentType,
429
- payloadSize: JSON.stringify(rawPayload).length,
430
- });
431
-
432
- // Determine format and parse
433
- let asnData: any;
434
- if (contentType.includes('xml') || contentType.includes('text')) {
435
- // Parse XML ASN (EDI 856 format)
436
- log.info('📄 [ASN] Parsing XML payload');
437
- const xmlParser = new XMLParserService();
438
- asnData = await xmlParser.parse(rawPayload);
439
- log.debug('✅ [ASN] XML parsed successfully', { rootKeys: Object.keys(asnData) });
440
- } else {
441
- // Assume JSON format
442
- log.info('📝 [ASN] Processing JSON payload');
443
- asnData = rawPayload;
444
- }
445
-
446
- // Extract ASN identifier for tracking
447
- // Common paths: ASN.shipment_id, ShipNotice.shipment_number, etc.
448
- const asnId =
449
- asnData?.ShipNotice?.['@shipment_id'] ||
450
- asnData?.ASN?.shipment_number ||
451
- asnData?.shipmentId ||
452
- 'UNKNOWN';
453
-
454
- log.info(`🔍 [ASN] Processing ASN: ${asnId}`);
455
-
456
- // =================================================================
457
- // STEP 2: CREATE FLUENT CLIENT
458
- // =================================================================
459
- // Create SDK logger adapter to map Versori log to SDK Logger interface
460
- const logger = {
461
- info: (msg: string, ...args: any[]) => log.info(msg, ...args),
462
- warn: (msg: string, ...args: any[]) => log.warn(msg, ...args),
463
- error: (msg: string, ...args: any[]) => log.error(msg, ...args),
464
- debug: (msg: string, ...args: any[]) => log.debug?.(msg, ...args) || log.info(msg, ...args)
465
- };
466
-
467
- // Create context for SDK client factory
468
- const fluentClient = await createClient({ ...ctx, log: logger }); // Auto-detects Versori context
469
-
470
- log.info('✅ [ASN] Fluent client initialized');
471
-
472
- // =================================================================
473
- // STEP 3: VALIDATE CONNECTION
474
- // =================================================================
475
- log.info('🔌 [ASN] Validating Fluent connection');
476
- await fluentClient.validateConnection();
477
- log.info('✅ [ASN] Connection validated successfully');
478
-
479
- // =================================================================
480
- // STEP 4: VALIDATE ASN (PREVENT DUPLICATES)
481
- // =================================================================
482
- // Query existing receipts to check if ASN already processed
483
- // IMPORTANT: Prevents duplicate expected inventory from same ASN
484
- log.info('🔍 [ASN] Checking for duplicate ASN');
485
- const duplicateCheckQuery = `
486
- query CheckExistingReceipt($ref: String!) {
487
- inventoryQuantities(ref: $ref, first: 1) {
488
- edges {
489
- node {
490
- id
491
- ref
492
- status
493
- }
494
- }
495
- }
496
- }`;
497
-
498
- const duplicateCheck = await fluentClient.graphql({
499
- query: duplicateCheckQuery,
500
- variables: {
501
- ref: `ASN-${asnId}`, // Use ASN ID as ref
502
- },
503
- });
504
-
505
- const existingReceipt = duplicateCheck.data?.inventoryQuantities?.edges?.[0]?.node;
506
-
507
- if (existingReceipt) {
508
- log.warn('⚠️ [ASN] Duplicate detected - ASN already processed', {
509
- asnId,
510
- existingReceiptId: existingReceipt.id,
511
- existingStatus: existingReceipt.status,
512
- });
513
-
514
- return {
515
- success: false,
516
- message: 'ASN already processed (duplicate)',
517
- asnId,
518
- existingReceiptId: existingReceipt.id,
519
- timestamp: new Date().toISOString(),
520
- duration: `${Date.now() - startTime}ms`,
521
- };
522
- }
523
-
524
- // =================================================================
525
- // STEP 5: SAVE RAW ASN FOR AUDIT TRAIL (KV Storage - Versori-compatible)
526
- // =================================================================
527
- log.info('Saving raw payload for audit trail');
528
- try {
529
- const kv = openKv(':project:');
530
- const timestamp = new Date().toISOString();
531
- const auditKey = ['asn', 'audit', asnId, timestamp];
532
-
533
- // Save original payload
534
- await kv.set([...auditKey, 'asn-raw'], asnData);
535
-
536
- log.info('Raw ASN saved to KV storage', { asnId, timestamp });
537
- } catch (kvError: any) {
538
- log.warn('Failed to save ASN to KV storage', { error: kvError.message });
539
- }
540
-
541
- // =================================================================
542
- // STEP 6: MAP ASN TO FLUENT EXPECTED RECEIPT FORMAT
543
- // =================================================================
544
- log.info('🗺️ [ASN] Starting ASN mapping');
545
- const mappingStartTime = Date.now();
546
-
547
- const mapper = new GraphQLMutationMapper(
548
- asnMappingConfig as any,
549
- logger,
550
- { fluentClient: fluentClient as any }
551
- );
552
-
553
- // Apply mapping with custom resolvers
554
- // Context includes fluentClient for API calls (SKU validation, etc.)
555
- // Returns MapWithNodesResult with query auto-generated!
556
- const mappingResult = await mapper.mapWithNodes(asnData, asnResolvers, {
557
- fluentClient: fluentClient as any,
558
- asnId,
559
- config: {
560
- retailerId: process.env.RETAILER_ID || '1',
561
- defaultLocation: process.env.DEFAULT_RECEIVING_LOCATION || 'DC-RECEIVING',
562
- },
563
- helpers: {
564
- fluentClient: fluentClient as any,
565
- },
566
- });
567
-
568
- if (!mappingResult.success) {
569
- log.error('❌ [ASN] Mapping failed', {
570
- errors: mappingResult.errors,
571
- recommendation: 'Check mapping configuration JSON and verify source paths match incoming ASN structure'
572
- });
573
- throw new Error(`ASN mapping failed: ${mappingResult.errors?.join(', ')}`);
574
- }
575
-
576
- log.info('✅ [ASN] Mapping successful', {
577
- asnId,
578
- itemCount: mappingResult.data?.items?.length || 0,
579
- duration: `${Date.now() - mappingStartTime}ms`,
580
- });
581
-
582
- // Save mapped data to KV storage
583
- try {
584
- const kv = openKv(':project:');
585
- const timestamp = new Date().toISOString();
586
- const auditKey = ['asn', 'audit', asnId, timestamp, 'fluent-mapped'];
587
- await kv.set(auditKey, mappingResult.data);
588
- } catch (kvError: any) {
589
- log.warn('Failed to save mapped data to KV', { error: kvError.message });
590
- }
591
-
592
- // =================================================================
593
- // STEP 7: CREATE EXPECTED RECEIPT IN FLUENT
594
- // =================================================================
595
- log.info('🚀 [ASN] Creating expected inventory receipt in Fluent');
596
- const mutationStartTime = Date.now();
597
-
598
- // mapWithNodes() auto-generates query - no need to call buildMutation()!
599
- // Execute mutation (use result.variables for GraphQL execution)
600
- const receiptResult = await fluentClient.graphql({
601
- query: mappingResult.query,
602
- variables: mappingResult.variables // ✅ Use variables (wrapped if fields pattern)
603
- });
604
-
605
- if (receiptResult.errors) {
606
- log.error('❌ [ASN] Receipt creation failed', {
607
- errors: receiptResult.errors,
608
- recommendation: 'Check GraphQL mutation structure and field types',
609
- });
610
- throw new Error(`Receipt creation failed: ${JSON.stringify(receiptResult.errors)}`);
611
- }
612
-
613
- const createdReceipt = receiptResult.data?.createInventoryQuantity;
614
-
615
- log.info('✅ [ASN] Expected receipt created successfully', {
616
- receiptId: createdReceipt?.id,
617
- asnId,
618
- duration: `${Date.now() - mutationStartTime}ms`,
619
- });
620
-
621
- // Save Fluent response to KV storage
622
- try {
623
- const kv = openKv(':project:');
624
- const timestamp = new Date().toISOString();
625
- const auditKey = ['asn', 'audit', asnId, timestamp, 'fluent-response'];
626
- await kv.set(auditKey, receiptResult);
627
- } catch (kvError: any) {
628
- log.warn('Failed to save Fluent response to KV', { error: kvError.message });
629
- }
630
-
631
- // =================================================================
632
- // STEP 8: RETURN SUCCESS RESPONSE TO 3PL
633
- // =================================================================
634
- const totalDuration = Date.now() - startTime;
635
- log.info('🎉 [ASN] Processing completed successfully', {
636
- asnId,
637
- totalDuration: `${totalDuration}ms`,
638
- });
639
-
640
- return {
641
- success: true,
642
- message: 'ASN processed successfully',
643
- data: {
644
- asnId,
645
- receiptId: createdReceipt?.id,
646
- receiptRef: createdReceipt?.ref,
647
- itemCount: mappingResult.data?.items?.length || 0,
648
- expectedDate: mappingResult.data?.expectedOn,
649
- timestamp: new Date().toISOString(),
650
- duration: `${totalDuration}ms`,
651
- },
652
- };
653
- } catch (error: any) {
654
- // ? Enhanced: Error logging with recommendations
655
- const totalDuration = Date.now() - startTime;
656
- const errorDetails = {
657
- message: error instanceof Error ? error.message : String(error),
658
- stack: error instanceof Error ? error.stack : undefined,
659
- errorType: error instanceof Error ? error.constructor.name : 'Error',
660
- duration: `${totalDuration}ms`,
661
- recommendation: error.message?.includes('authentication') || error.message?.includes('401')
662
- ? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
663
- : error.message?.includes('mapping') || error.message?.includes('field')
664
- ? 'Check mapping configuration JSON and verify source paths match incoming ASN structure'
665
- : error.message?.includes('connection') || error.message?.includes('timeout')
666
- ? 'Check network connectivity and Fluent Commerce API availability'
667
- : error.message?.includes('validation') || error.message?.includes('required')
668
- ? 'Ensure all required fields are present in the ASN webhook payload'
669
- : error.message?.includes('duplicate') || error.message?.includes('already processed')
670
- ? 'ASN may have been processed already - check existing receipts'
671
- : 'Review error details and check ASN payload structure',
672
- };
673
- log.error('❌ [ASN] Processing failed', errorDetails);
674
- throw error; // Let response handler catch it
675
- }
676
- });
677
-
678
- /**
679
- * Manual test endpoint - Upload ASN file for testing
680
- */
681
- export const manualAsnTest = webhook('manual-asn-test', {
682
- response: {
683
- mode: 'sync',
684
- onSuccess: (ctx) => new Response(JSON.stringify(ctx.data), {
685
- status: 200,
686
- headers: { 'Content-Type': 'application/json' }
687
- }),
688
- onError: (ctx, error) => new Response(JSON.stringify({
689
- success: false,
690
- error: error.message,
691
- recommendation: 'Check that test ASN file exists at expected path'
692
- }), {
693
- status: 400,
694
- headers: { 'Content-Type': 'application/json' }
695
- })
696
- }
697
- }, async (ctx) => {
698
- const { log } = ctx;
699
- const startTime = Date.now();
700
-
701
- log.info('🧪 [TEST] Manual ASN test triggered');
702
-
703
- // Load test ASN data
704
- const testAsnPath = path.join(__dirname, '../../data/test-asn-sample.json');
705
- if (!fs.existsSync(testAsnPath)) {
706
- log.error('❌ [TEST] Test ASN file not found', { expectedPath: testAsnPath });
707
- return {
708
- success: false,
709
- error: 'Test ASN file not found',
710
- expectedPath: testAsnPath,
711
- recommendation: 'Create test-asn-sample.json in the data/ directory',
712
- };
713
- }
714
-
715
- const testAsnData = JSON.parse(fs.readFileSync(testAsnPath, 'utf-8'));
716
-
717
- log.info('✅ [TEST] Test ASN loaded', {
718
- asnId: testAsnData.shipmentId,
719
- duration: `${Date.now() - startTime}ms`,
720
- });
721
-
722
- // Note: In production, you would trigger the main workflow via HTTP call
723
- // This is a simplified test endpoint for development
724
- return {
725
- success: true,
726
- message: 'Test ASN loaded - trigger processAsnWebhook endpoint to process',
727
- asnId: testAsnData.shipmentId,
728
- testDataPath: testAsnPath,
729
- duration: `${Date.now() - startTime}ms`,
730
- };
731
- });
732
-
733
- /**
734
- * Health check endpoint
735
- */
736
- export const healthCheck = webhook('health-check', {
737
- response: {
738
- mode: 'sync',
739
- onSuccess: (ctx) => new Response(JSON.stringify(ctx.data), {
740
- status: 200,
741
- headers: { 'Content-Type': 'application/json' }
742
- })
743
- }
744
- }, async (ctx) => {
745
- const { log } = ctx;
746
- log.info('💚 [HEALTH] Health check requested');
747
-
748
- return {
749
- success: true,
750
- service: 'ASN Processing',
751
- status: 'healthy',
752
- timestamp: new Date().toISOString(),
753
- };
754
- });
755
- ```
756
-
757
- ### 2. Mapping Configuration: `mappings/asn-to-fluent-receipt.json`
758
-
759
- ```json
760
- {
761
- "direction": "ingest",
762
- "sourceFormat": "json",
763
- "mutation": "createInventoryQuantity",
764
- "fields": {
765
- "ref": {
766
- "resolver": "custom.generateReceiptRef",
767
- "comment": "Generate unique receipt ref from ASN ID (REQUIRED)"
768
- },
769
- "locationRef": {
770
- "source": "destination.location_code",
771
- "resolver": "custom.normalizeLocationRef",
772
- "comment": "Receiving location (REQUIRED)"
773
- },
774
- "type": {
775
- "value": "EXPECTED",
776
- "comment": "Expected inventory type (REQUIRED)"
777
- },
778
- "status": {
779
- "value": "EXPECTED",
780
- "comment": "Status is EXPECTED until physically received (REQUIRED)"
781
- },
782
- "expectedOn": {
783
- "resolver": "custom.calculateExpectedDate",
784
- "comment": "Calculate based on ship date + carrier transit time (REQUIRED)"
785
- },
786
- "carrier": {
787
- "source": "shipment.carrier.name",
788
- "resolver": "sdk.trim",
789
- "comment": "Shipping carrier name"
790
- },
791
- "trackingNumber": {
792
- "source": "shipment.tracking_number",
793
- "resolver": "sdk.trim",
794
- "comment": "Carrier tracking number"
795
- },
796
- "retailer.id": {
797
- "resolver": "custom.getRetailerId",
798
- "comment": "Retailer ID from config (REQUIRED)"
799
- },
800
- "items": {
801
- "source": "shipment.items",
802
- "isArray": true,
803
- "comment": "Line items in the shipment",
804
- "fields": {
805
- "skuRef": {
806
- "source": "$.sku",
807
- "resolver": "custom.validateAndNormalizeSku",
808
- "comment": "Product SKU - must exist in Fluent (REQUIRED)"
809
- },
810
- "qty": {
811
- "source": "$.quantity",
812
- "resolver": "sdk.parseInt",
813
- "comment": "Expected quantity (REQUIRED)"
814
- },
815
- "lotNumber": {
816
- "source": "$.lot_number",
817
- "required": false,
818
- "comment": "Lot/batch number if applicable"
819
- },
820
- "serialNumbers": {
821
- "source": "$.serial_numbers",
822
- "isArray": true,
823
- "required": false,
824
- "comment": "Serial numbers for serialized items"
825
- },
826
- "expiryDate": {
827
- "source": "$.expiry_date",
828
- "resolver": "sdk.formatDate",
829
- "required": false,
830
- "comment": "Expiration date for perishable goods"
831
- },
832
- "containerRef": {
833
- "source": "$.container_id",
834
- "required": false,
835
- "comment": "Container/pallet ID for tracking"
836
- }
837
- }
838
- },
839
- "containers": {
840
- "source": "shipment.containers",
841
- "isArray": true,
842
- "required": false,
843
- "comment": "Container/pallet tracking information",
844
- "fields": {
845
- "containerRef": {
846
- "source": "$.container_id",
847
- "resolver": "sdk.toString"
848
- },
849
- "containerType": {
850
- "source": "$.type",
851
- "resolver": "sdk.uppercase",
852
- "comment": "PALLET, CARTON, TOTE, etc."
853
- },
854
- "weight": {
855
- "source": "$.weight",
856
- "resolver": "sdk.parseFloat"
857
- },
858
- "weightUnit": {
859
- "source": "$.weight_unit",
860
- "defaultValue": "LBS"
861
- },
862
- "dimensions": {
863
- "resolver": "custom.formatDimensions",
864
- "comment": "Format as length x width x height"
865
- }
866
- }
867
- },
868
- "attributes": {
869
- "resolver": "custom.buildReceiptAttributes",
870
- "comment": "Custom attributes for audit trail and tracking"
871
- }
872
- }
873
- }
874
- ```
875
-
876
- ### 3. Custom Resolvers: `src/resolvers/asn-resolvers.ts`
877
-
878
- ```typescript
879
- /**
880
- * ASN Processing Custom Resolvers
881
- */
882
- import type { ResolverMap } from './types';
883
-
884
- export const asnResolvers: ResolverMap = {
885
- /**
886
- * Generate unique receipt reference from ASN ID
887
- */
888
- 'custom.generateReceiptRef': (value: any, data: any, config: any, helpers: any): string => {
889
- const asnId = data.shipmentId || data.shipment_id || data.ASN?.id || 'UNKNOWN';
890
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
891
- return `ASN-${asnId}-${timestamp}`;
892
- },
893
-
894
- /**
895
- * Normalize location reference
896
- * Handles different location code formats from various 3PLs
897
- */
898
- 'custom.normalizeLocationRef': (value: any, data: any, config: any, helpers: any): string => {
899
- if (!value) {
900
- return config.defaultLocation || 'DC-RECEIVING';
901
- }
902
-
903
- // Normalize format: uppercase, replace spaces with hyphens
904
- return String(value)
905
- .toUpperCase()
906
- .trim()
907
- .replace(/\s+/g, '-');
908
- },
909
-
910
- /**
911
- * Calculate expected arrival date
912
- * Based on ship date + carrier transit time
913
- */
914
- 'custom.calculateExpectedDate': (value: any, data: any, config: any, helpers: any): string => {
915
- const shipDate = data.shipment?.ship_date || data.shipDate;
916
- const carrier = data.shipment?.carrier?.name || data.carrier;
917
-
918
- // Default transit times by carrier (in days)
919
- const transitTimes: Record<string, number> = {
920
- 'FEDEX_GROUND': 3,
921
- 'FEDEX_EXPRESS': 1,
922
- 'UPS_GROUND': 3,
923
- 'UPS_NEXT_DAY': 1,
924
- 'USPS': 5,
925
- 'DHL': 2,
926
- 'FREIGHT': 7,
927
- 'DEFAULT': 3,
928
- };
929
-
930
- // Get transit time
931
- const carrierKey = String(carrier).toUpperCase().replace(/\s+/g, '_');
932
- const transitDays = transitTimes[carrierKey] || transitTimes.DEFAULT;
933
-
934
- // Calculate expected date
935
- const shipDateObj = shipDate ? new Date(shipDate) : new Date();
936
- shipDateObj.setDate(shipDateObj.getDate() + transitDays);
937
-
938
- return shipDateObj.toISOString();
939
- },
940
-
941
- /**
942
- * Validate and normalize SKU
943
- * Ensures SKU exists in Fluent Commerce (ASYNC)
944
- */
945
- 'custom.validateAndNormalizeSku': async (
946
- value: any,
947
- data: any,
948
- config: any,
949
- helpers: any
950
- ): Promise<string> => {
951
- const sku = String(value).trim().toUpperCase();
952
-
953
- if (!helpers.fluentClient) {
954
- // If no client, just return normalized SKU
955
- return sku;
956
- }
957
-
958
- // Query Fluent to validate SKU exists
959
- const query = `
960
- query ValidateSku($skuRef: [String!]) {
961
- products(ref: $skuRef, first: 1) {
962
- edges {
963
- node {
964
- id
965
- ref
966
- }
967
- }
968
- }
969
- }`;
970
-
971
- try {
972
- const result = await helpers.fluentClient.graphql({
973
- query,
974
- variables: { skuRef: [sku] },
975
- });
976
-
977
- const product = result.data?.products?.edges?.[0]?.node;
978
-
979
- if (!product) {
980
- helpers.logger?.warn('SKU not found in Fluent Commerce', { sku });
981
- // Option 1: Throw error to halt processing
982
- // throw new Error(`SKU not found: ${sku}`);
983
-
984
- // Option 2: Return SKU anyway (let Fluent validation handle it)
985
- return sku;
986
- }
987
-
988
- helpers.logger?.debug('SKU validated successfully', { sku, productId: product.id });
989
- return sku;
990
- } catch (error: any) {
991
- helpers.logger?.error('SKU validation failed', { sku, error: error.message });
992
- return sku; // Return SKU anyway, let Fluent handle validation
993
- }
994
- },
995
-
996
- /**
997
- * Get retailer ID from configuration
998
- */
999
- 'custom.getRetailerId': (value: any, data: any, config: any, helpers: any): string => {
1000
- return config.retailerId || process.env.RETAILER_ID || '1';
1001
- },
1002
-
1003
- /**
1004
- * Format dimensions as string
1005
- */
1006
- 'custom.formatDimensions': (value: any, data: any, config: any, helpers: any): string => {
1007
- const container = data; // Current container object
1008
- const length = container.length || 0;
1009
- const width = container.width || 0;
1010
- const height = container.height || 0;
1011
- const unit = container.dimension_unit || 'IN';
1012
-
1013
- if (!length && !width && !height) {
1014
- return '';
1015
- }
1016
-
1017
- return `${length} x ${width} x ${height} ${unit}`;
1018
- },
1019
-
1020
- /**
1021
- * Build receipt attributes for audit trail
1022
- */
1023
- 'custom.buildReceiptAttributes': (
1024
- value: any,
1025
- data: any,
1026
- config: any,
1027
- helpers: any
1028
- ): Array<{ name: string; type: string; value: any }> => {
1029
- const attributes: Array<{ name: string; type: string; value: any }> = [];
1030
-
1031
- // Add ASN metadata
1032
- attributes.push({
1033
- name: 'asn_id',
1034
- type: 'STRING',
1035
- value: config.asnId || data.shipmentId || 'UNKNOWN',
1036
- });
1037
-
1038
- attributes.push({
1039
- name: 'asn_received_date',
1040
- type: 'STRING',
1041
- value: new Date().toISOString(),
1042
- });
1043
-
1044
- // Add carrier tracking
1045
- if (data.shipment?.tracking_number) {
1046
- attributes.push({
1047
- name: 'tracking_number',
1048
- type: 'STRING',
1049
- value: data.shipment.tracking_number,
1050
- });
1051
- }
1052
-
1053
- // Add origin information
1054
- if (data.shipment?.origin) {
1055
- attributes.push({
1056
- name: 'origin_location',
1057
- type: 'STRING',
1058
- value: JSON.stringify(data.shipment.origin),
1059
- });
1060
- }
1061
-
1062
- // Add PO number if present
1063
- if (data.purchase_order_number) {
1064
- attributes.push({
1065
- name: 'po_number',
1066
- type: 'STRING',
1067
- value: data.purchase_order_number,
1068
- });
1069
- }
1070
-
1071
- // Cross-dock flag
1072
- if (data.shipment?.is_crossdock) {
1073
- attributes.push({
1074
- name: 'is_crossdock',
1075
- type: 'BOOLEAN',
1076
- value: 'true',
1077
- });
1078
- }
1079
-
1080
- return attributes;
1081
- },
1082
- };
1083
- ```
1084
-
1085
- ### 4. Sample ASN Test Data: `data/test-asn-sample.json`
1086
-
1087
- ```json
1088
- {
1089
- "shipmentId": "ASN-20250117-001",
1090
- "purchase_order_number": "PO-12345",
1091
- "shipment": {
1092
- "ship_date": "2025-01-17T10:00:00Z",
1093
- "carrier": {
1094
- "name": "FEDEX_GROUND",
1095
- "scac": "FXFE"
1096
- },
1097
- "tracking_number": "123456789012",
1098
- "origin": {
1099
- "name": "Acme Distribution Center",
1100
- "address": "123 Warehouse Dr",
1101
- "city": "Memphis",
1102
- "state": "TN",
1103
- "zip": "38101"
1104
- },
1105
- "is_crossdock": false,
1106
- "items": [
1107
- {
1108
- "sku": "ACME-WIDGET-100",
1109
- "quantity": 50,
1110
- "lot_number": "LOT20250115",
1111
- "container_id": "PALLET-001"
1112
- },
1113
- {
1114
- "sku": "ACME-GADGET-200",
1115
- "quantity": 100,
1116
- "container_id": "PALLET-001"
1117
- },
1118
- {
1119
- "sku": "ACME-TOOL-300",
1120
- "quantity": 25,
1121
- "lot_number": "LOT20250110",
1122
- "expiry_date": "2026-01-10T00:00:00Z",
1123
- "container_id": "PALLET-002"
1124
- }
1125
- ],
1126
- "containers": [
1127
- {
1128
- "container_id": "PALLET-001",
1129
- "type": "PALLET",
1130
- "weight": 250.5,
1131
- "weight_unit": "LBS",
1132
- "length": 48,
1133
- "width": 40,
1134
- "height": 60,
1135
- "dimension_unit": "IN"
1136
- },
1137
- {
1138
- "container_id": "PALLET-002",
1139
- "type": "PALLET",
1140
- "weight": 180.0,
1141
- "weight_unit": "LBS",
1142
- "length": 48,
1143
- "width": 40,
1144
- "height": 48,
1145
- "dimension_unit": "IN"
1146
- }
1147
- ]
1148
- },
1149
- "destination": {
1150
- "location_code": "DC-01-RECEIVING",
1151
- "name": "Distribution Center 01",
1152
- "address": "789 Commerce Pkwy",
1153
- "city": "Atlanta",
1154
- "state": "GA",
1155
- "zip": "30301"
1156
- }
1157
- }
1158
- ```
1159
-
1160
- ### 5. Package Configuration: `package.json`
1161
-
1162
- ```json
1163
- {
1164
- "name": "asn-processing-connector",
1165
- "version": "1.0.0",
1166
- "description": "ASN/Purchase Order processing connector for Acme 3PL integration",
1167
- "versori": {
1168
- "workflows": "./workflows/asn-receipt.ts"
1169
- },
1170
- "dependencies": {
1171
- "@fluentcommerce/fc-connect-sdk": "^0.1.39",
1172
- "@versori/run": "latest"
1173
- },
1174
- "devDependencies": {
1175
- "@types/node": "^20.0.0",
1176
- "typescript": "^5.0.0"
1177
- },
1178
- "scripts": {
1179
- "deploy": "versori deploy",
1180
- "logs": "versori logs",
1181
- "test": "node -r ts-node/register workflows/asn-receipt.ts"
1182
- }
1183
- }
1184
- ```
1185
-
1186
- ---
1187
-
1188
- ## Key Patterns Explained
1189
-
1190
- ### Pattern 1: ASN Duplicate Prevention
1191
-
1192
- **Check for Existing Receipt Before Creating:**
1193
-
1194
- ```typescript
1195
- // Query existing receipts using ASN ID as ref
1196
- const duplicateCheckQuery = `
1197
- query CheckExistingReceipt($ref: String!) {
1198
- inventoryQuantities(ref: $ref, first: 1) {
1199
- edges {
1200
- node {
1201
- id
1202
- ref
1203
- status
1204
- }
1205
- }
1206
- }
1207
- }`;
1208
-
1209
- const duplicateCheck = await fluentClient.graphql({
1210
- query: duplicateCheckQuery,
1211
- variables: {
1212
- ref: `ASN-${asnId}`, // Unique ref from ASN shipment ID
1213
- },
1214
- });
1215
-
1216
- const existingReceipt = duplicateCheck.data?.inventoryQuantities?.edges?.[0]?.node;
1217
-
1218
- if (existingReceipt) {
1219
- // ASN already processed - return early
1220
- return {
1221
- status: 200,
1222
- body: {
1223
- success: false,
1224
- message: 'ASN already processed (duplicate)',
1225
- existingReceiptId: existingReceipt.id,
1226
- },
1227
- };
1228
- }
1229
- ```
1230
-
1231
- **Why this matters**: 3PLs may send duplicate ASN notifications due to:
1232
- - Network retries
1233
- - System glitches
1234
- - Manual re-sends
1235
- - Webhook replay
1236
-
1237
- **Without duplicate prevention**: Creates duplicate expected inventory, causing:
1238
- - Inflated ATP calculations
1239
- - Incorrect stock levels
1240
- - Failed physical receipts (already expected)
1241
-
1242
- **Best practices**:
1243
- - Always use ASN shipment ID as part of receipt ref
1244
- - Query before creating
1245
- - Log duplicate attempts for monitoring
1246
-
1247
- ### Pattern 2: Expected Date Calculation
1248
-
1249
- **Calculate Arrival Date Based on Carrier Transit Time:**
1250
-
1251
- ```typescript
1252
- 'custom.calculateExpectedDate': (value, data, config, helpers) => {
1253
- const shipDate = data.shipment?.ship_date || data.shipDate;
1254
- const carrier = data.shipment?.carrier?.name || data.carrier;
1255
-
1256
- // Transit time lookup by carrier
1257
- const transitTimes = {
1258
- 'FEDEX_GROUND': 3,
1259
- 'FEDEX_EXPRESS': 1,
1260
- 'UPS_GROUND': 3,
1261
- 'UPS_NEXT_DAY': 1,
1262
- 'USPS': 5,
1263
- 'DHL': 2,
1264
- 'FREIGHT': 7,
1265
- 'DEFAULT': 3,
1266
- };
1267
-
1268
- const carrierKey = String(carrier).toUpperCase().replace(/\s+/g, '_');
1269
- const transitDays = transitTimes[carrierKey] || transitTimes.DEFAULT;
1270
-
1271
- // Add transit days to ship date
1272
- const shipDateObj = shipDate ? new Date(shipDate) : new Date();
1273
- shipDateObj.setDate(shipDateObj.getDate() + transitDays);
1274
-
1275
- return shipDateObj.toISOString();
1276
- };
1277
- ```
1278
-
1279
- **Why this matters**: Expected date drives:
1280
- - ATP calculations (Available To Promise)
1281
- - Allocation timing
1282
- - Customer promise dates
1283
- - Warehouse receiving schedules
1284
-
1285
- **Improvements for production:**
1286
-
1287
- ```typescript
1288
- // Add business days logic (skip weekends)
1289
- function addBusinessDays(date: Date, days: number): Date {
1290
- let result = new Date(date);
1291
- let addedDays = 0;
1292
-
1293
- while (addedDays < days) {
1294
- result.setDate(result.getDate() + 1);
1295
- // Skip weekends (0 = Sunday, 6 = Saturday)
1296
- if (result.getDay() !== 0 && result.getDay() !== 6) {
1297
- addedDays++;
1298
- }
1299
- }
1300
-
1301
- return result;
1302
- }
1303
-
1304
- // Add holiday logic
1305
- const holidays = ['2025-01-01', '2025-07-04', '2025-12-25'];
1306
-
1307
- function isHoliday(date: Date): boolean {
1308
- const dateStr = date.toISOString().substring(0, 10);
1309
- return holidays.includes(dateStr);
1310
- }
1311
- ```
1312
-
1313
- ### Pattern 3: SKU Validation (Async Resolver)
1314
-
1315
- **Validate SKU Exists in Fluent Before Creating Receipt:**
1316
-
1317
- ```typescript
1318
- 'custom.validateAndNormalizeSku': async (value, data, config, helpers) => {
1319
- const sku = String(value).trim().toUpperCase();
1320
-
1321
- // Query Fluent to check if SKU exists
1322
- const query = `
1323
- query ValidateSku($skuRef: [String!]) {
1324
- products(ref: $skuRef, first: 1) {
1325
- edges {
1326
- node {
1327
- id
1328
- ref
1329
- }
1330
- }
1331
- }
1332
- }
1333
- `;
1334
-
1335
- const result = await helpers.fluentClient.graphql({
1336
- query,
1337
- variables: { skuRef: [sku] },
1338
- });
1339
-
1340
- const product = result.data?.products?.edges?.[0]?.node;
1341
-
1342
- if (!product) {
1343
- // SKU not found - options:
1344
- // 1. Throw error (halt processing)
1345
- throw new Error(`SKU not found: ${sku}`);
1346
-
1347
- // 2. Log warning and continue (let Fluent handle validation)
1348
- helpers.logger?.warn('SKU not found', { sku });
1349
- return sku;
1350
- }
1351
-
1352
- return sku;
1353
- };
1354
- ```
1355
-
1356
- **When to use SKU validation**:
1357
- - ✅ **3PL sends ASN before products are created** (prevents invalid receipts)
1358
- - ✅ **SKU format differs between systems** (can normalize)
1359
- - ✅ **Need early warning** (alert on unknown SKUs)
1360
-
1361
- **When to skip**:
1362
- - ❌ **High volume** (100+ SKUs per ASN = slow)
1363
- - ❌ **Products always exist** (pre-synced)
1364
- - ❌ **Performance critical** (let Fluent validation handle it)
1365
-
1366
- **Performance optimization**:
1367
-
1368
- ```typescript
1369
- // Cache SKU validation results in resolver context
1370
- const skuCache = new Map<string, boolean>();
1371
-
1372
- if (skuCache.has(sku)) {
1373
- return sku; // Already validated
1374
- }
1375
-
1376
- const isValid = await validateSku(sku);
1377
- skuCache.set(sku, isValid);
1378
- ```
1379
-
1380
- ### Pattern 4: Container/Pallet Tracking
1381
-
1382
- **Track Containers for Efficient Receiving:**
1383
-
1384
- ```json
1385
- {
1386
- "containers": {
1387
- "source": "shipment.containers",
1388
- "isArray": true,
1389
- "fields": {
1390
- "containerRef": {
1391
- "source": "$.container_id"
1392
- },
1393
- "containerType": {
1394
- "source": "$.type",
1395
- "comment": "PALLET, CARTON, TOTE"
1396
- },
1397
- "weight": { "source": "$.weight" },
1398
- "dimensions": {
1399
- "resolver": "custom.formatDimensions"
1400
- }
1401
- }
1402
- }
1403
- }
1404
- ```
1405
-
1406
- **Why container tracking matters**:
1407
- - **Warehouse efficiency**: Receive entire pallets at once
1408
- - **Put-away optimization**: Route containers to correct zones
1409
- - **Cross-docking**: Direct routing without storage
1410
- - **Audit trail**: Track physical container movement
1411
-
1412
- **Real-world example**:
1413
-
1414
- ```
1415
- ASN contains:
1416
- - PALLET-001: 50x WIDGET + 100x GADGET
1417
- - PALLET-002: 25x TOOL (refrigerated)
1418
-
1419
- Warehouse receives PALLET-001 first:
1420
- → Scan PALLET-001 barcode
1421
- → System shows all 2 SKUs on pallet
1422
- → Receive all at once (no item-by-item scan)
1423
- → PALLET-002 still expected
1424
- ```
1425
-
1426
- ### Pattern 5: Cross-Dock Scenario Handling
1427
-
1428
- **Handle Direct Fulfillment Without Storage:**
1429
-
1430
- ```typescript
1431
- // In ASN data
1432
- {
1433
- "shipment": {
1434
- "is_crossdock": true, // Flag for cross-dock shipment
1435
- "destination_order_ref": "ORDER-12345"
1436
- }
1437
- }
1438
-
1439
- // Custom resolver for cross-dock logic
1440
- 'custom.buildReceiptAttributes': (value, data, config, helpers) => {
1441
- const attributes = [];
1442
-
1443
- if (data.shipment?.is_crossdock) {
1444
- attributes.push({
1445
- name: 'is_crossdock',
1446
- type: 'BOOLEAN',
1447
- value: 'true',
1448
- });
1449
-
1450
- // Link to destination order
1451
- if (data.shipment.destination_order_ref) {
1452
- attributes.push({
1453
- name: 'crossdock_order_ref',
1454
- type: 'STRING',
1455
- value: data.shipment.destination_order_ref,
1456
- });
1457
- }
1458
-
1459
- // Mark for expedited processing
1460
- attributes.push({
1461
- name: 'receiving_priority',
1462
- type: 'STRING',
1463
- value: 'HIGH',
1464
- });
1465
- }
1466
-
1467
- return attributes;
1468
- };
1469
- ```
1470
-
1471
- **Cross-dock workflow**:
1472
- 1. **ASN received** with `is_crossdock: true`
1473
- 2. **Expected receipt created** with crossdock flag
1474
- 3. **Physical receipt** → immediately allocated to order
1475
- 4. **Skip put-away** → direct to packing station
1476
- 5. **Ship within hours** (not days)
1477
-
1478
- **Benefits**:
1479
- - Faster order fulfillment (same-day ship)
1480
- - Reduced warehouse touches
1481
- - Lower storage costs
1482
- - Improved customer satisfaction
1483
-
1484
- ### Pattern 6: XML vs JSON Payload Handling
1485
-
1486
- **Support Multiple Payload Formats:**
1487
-
1488
- ```typescript
1489
- // Detect content type from header
1490
- const contentType = ctx.activation?.headers?.['content-type'] || 'application/json';
1491
-
1492
- let asnData: any;
1493
-
1494
- if (contentType.includes('xml') || contentType.includes('text')) {
1495
- // Parse XML (EDI 856 format)
1496
- const xmlParser = new XMLParserService();
1497
- asnData = await xmlParser.parse(rawPayload);
1498
- } else {
1499
- // Assume JSON
1500
- asnData = rawPayload;
1501
- }
1502
- ```
1503
-
1504
- **Why support both**:
1505
- - **Enterprise 3PLs**: Often use EDI 856 XML format
1506
- - **Modern 3PLs**: Use JSON REST APIs
1507
- - **Flexibility**: Handle both without separate connectors
1508
-
1509
- **EDI 856 XML example**:
1510
-
1511
- ```xml
1512
- <?xml version="1.0"?>
1513
- <ShipNotice shipment_id="ASN-001">
1514
- <Shipment>
1515
- <ShipDate>2025-01-17</ShipDate>
1516
- <Carrier name="FEDEX_GROUND"/>
1517
- <Items>
1518
- <Item>
1519
- <SKU>WIDGET-100</SKU>
1520
- <Quantity>50</Quantity>
1521
- </Item>
1522
- </Items>
1523
- </Shipment>
1524
- </ShipNotice>
1525
- ```
1526
-
1527
- **JSON equivalent**:
1528
-
1529
- ```json
1530
- {
1531
- "shipmentId": "ASN-001",
1532
- "shipment": {
1533
- "ship_date": "2025-01-17",
1534
- "carrier": { "name": "FEDEX_GROUND" },
1535
- "items": [
1536
- { "sku": "WIDGET-100", "quantity": 50 }
1537
- ]
1538
- }
1539
- }
1540
- ```
1541
-
1542
- ---
1543
-
1544
- ## Testing the Workflow
1545
-
1546
- ### 1. Create Test ASN File
1547
-
1548
- Already provided in `data/test-asn-sample.json` above.
1549
-
1550
- ### 2. Deploy to Versori Platform
1551
-
1552
- ```bash
1553
- cd asn-processing-connector
1554
- npm install
1555
-
1556
- # Deploy to Versori
1557
- versori deploy
1558
-
1559
- # Or if using Versori CLI v2+
1560
- npx @versori/cli deploy
1561
- ```
1562
-
1563
- ### 3. Configure Versori Connections
1564
-
1565
- In the Versori console:
1566
-
1567
- 1. Add **Fluent Commerce** connection:
1568
- - Connection name: `fluent_commerce`
1569
- - Base URL: `https://api.fluentcommerce.com/graphql`
1570
- - Auth: OAuth2 (Client Credentials or Password Grant)
1571
- - Client ID, Client Secret, Username, Password
1572
-
1573
- 2. Configure activation variables (optional):
1574
- - `OUTPUT_DIR`: Where to save audit files
1575
- - `RETAILER_ID`: Fluent retailer ID
1576
- - `DEFAULT_RECEIVING_LOCATION`: Default location code
1577
-
1578
- ### 4. Test via Webhook
1579
-
1580
- ```bash
1581
- # Get webhook URL from Versori console (usually https://{workspace}.versori.io/{workflow})
1582
- # Example: https://acme.versori.io/processAsnWebhook
1583
-
1584
- # Send test ASN via HTTP POST
1585
- curl -X POST https://acme.versori.io/processAsnWebhook \
1586
- -H "Content-Type: application/json" \
1587
- -d @data/test-asn-sample.json
1588
- ```
1589
-
1590
- ### 5. Use Manual Test Endpoint
1591
-
1592
- ```bash
1593
- # Trigger manual test (loads test-asn-sample.json from filesystem)
1594
- curl https://acme.versori.io/manualAsnTest
1595
- ```
1596
-
1597
- ### 6. Check Health Endpoint
1598
-
1599
- ```bash
1600
- # Verify service is running
1601
- curl https://acme.versori.io/healthCheck
1602
- ```
1603
-
1604
- ### 7. Verify in Fluent Commerce
1605
-
1606
- 1. Log into Fluent Console
1607
- 2. Navigate to Inventory → Inventory Quantities
1608
- 3. Filter by status: `EXPECTED`
1609
- 4. Find receipt with ref `ASN-20250117-001-*`
1610
- 5. Verify:
1611
- - Quantity matches ASN
1612
- - Expected date calculated correctly
1613
- - Attributes contain ASN metadata
1614
-
1615
- ### 8. Monitor Logs
1616
-
1617
- ```bash
1618
- # View real-time logs in Versori console
1619
- # Or use Versori CLI
1620
- versori logs --workflow=processAsnWebhook --tail
1621
-
1622
- # Check for errors
1623
- versori logs --workflow=processAsnWebhook --level=error
1624
- ```
1625
-
1626
- ---
1627
-
1628
- ## Common Issues and Solutions
1629
-
1630
- ### Issue 1: "Duplicate entity" Error from Fluent
1631
-
1632
- **Symptoms:**
1633
- - GraphQL error: "Entity with ref already exists"
1634
- - Second ASN attempt fails
1635
-
1636
- **Root Cause:**
1637
- - Receipt ref not unique
1638
- - Missing timestamp in ref generation
1639
-
1640
- **Solution:**
1641
-
1642
- ```typescript
1643
- // Ensure unique ref with timestamp
1644
- 'custom.generateReceiptRef': (value, data, config, helpers) => {
1645
- const asnId = data.shipmentId || 'UNKNOWN';
1646
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
1647
- const randomSuffix = Math.random().toString(36).substring(7);
1648
-
1649
- // Format: ASN-{asnId}-{timestamp}-{random}
1650
- return `ASN-${asnId}-${timestamp}-${randomSuffix}`;
1651
- };
1652
- ```
1653
-
1654
- ### Issue 2: Expected Date in the Past
1655
-
1656
- **Symptoms:**
1657
- - Expected date shows yesterday or earlier
1658
- - ATP calculation incorrect
1659
-
1660
- **Root Cause:**
1661
- - Ship date from ASN is in the past
1662
- - Transit time calculation starts from old date
1663
-
1664
- **Solution:**
1665
-
1666
- ```typescript
1667
- 'custom.calculateExpectedDate': (value, data, config, helpers) => {
1668
- const shipDate = data.shipment?.ship_date;
1669
- const carrier = data.shipment?.carrier?.name;
1670
-
1671
- // Get transit days
1672
- const transitDays = getTransitDays(carrier);
1673
-
1674
- // Use ship date or TODAY (whichever is later)
1675
- const baseDate = shipDate ? new Date(shipDate) : new Date();
1676
- const today = new Date();
1677
-
1678
- // If ship date is in the past, use today instead
1679
- if (baseDate < today) {
1680
- baseDate.setTime(today.getTime());
1681
- }
1682
-
1683
- // Add transit days
1684
- baseDate.setDate(baseDate.getDate() + transitDays);
1685
-
1686
- return baseDate.toISOString();
1687
- };
1688
- ```
1689
-
1690
- ### Issue 3: SKU Validation Slows Processing
1691
-
1692
- **Symptoms:**
1693
- - Webhook timeouts (>30 seconds)
1694
- - ASN with 100+ SKUs fails
1695
-
1696
- **Root Cause:**
1697
- - Sequential SKU validation queries
1698
- - No caching
1699
-
1700
- **Solution:**
1701
-
1702
- ```typescript
1703
- // Batch validate all SKUs in single query
1704
- 'custom.validateAllSkus': async (value, data, config, helpers) => {
1705
- const allSkus = data.shipment?.items?.map((item: any) => item.sku) || [];
1706
-
1707
- // Single query for all SKUs
1708
- const query = `
1709
- query ValidateSkus($skuRefs: [String!]) {
1710
- products(ref: $skuRefs, first: 200) {
1711
- edges {
1712
- node {
1713
- id
1714
- ref
1715
- }
1716
- }
1717
- }
1718
- }
1719
- `;
1720
-
1721
- const result = await helpers.fluentClient.graphql({
1722
- query,
1723
- variables: { skuRefs: allSkus },
1724
- });
1725
-
1726
- const validSkus = new Set(
1727
- result.data?.products?.edges?.map((e: any) => e.node.ref) || []
1728
- );
1729
-
1730
- // Check for missing SKUs
1731
- const missingSkus = allSkus.filter(sku => !validSkus.has(sku));
1732
-
1733
- if (missingSkus.length > 0) {
1734
- helpers.logger?.warn('Invalid SKUs found', { missingSkus });
1735
- // Option: throw error or continue
1736
- }
1737
-
1738
- return validSkus;
1739
- };
1740
- ```
1741
-
1742
- ### Issue 4: Cross-Dock Flag Not Honored
1743
-
1744
- **Symptoms:**
1745
- - Cross-dock inventory goes to storage
1746
- - Fulfillment delayed
1747
-
1748
- **Root Cause:**
1749
- - Warehouse receiving app doesn't check attributes
1750
- - Missing workflow trigger
1751
-
1752
- **Solution:**
1753
-
1754
- **Option 1: Create Fluent Event**
1755
-
1756
- ```typescript
1757
- // After creating receipt, send event if cross-dock
1758
- if (data.shipment?.is_crossdock) {
1759
- await fluentClient.sendEvent({
1760
- name: 'InventoryReceiptCrossDock',
1761
- entityType: 'INVENTORY',
1762
- entityRef: createdReceipt.ref,
1763
- data: {
1764
- destinationOrderRef: data.shipment.destination_order_ref,
1765
- priority: 'HIGH',
1766
- },
1767
- });
1768
- }
1769
- ```
1770
-
1771
- **Option 2: Use Different Location**
1772
-
1773
- ```typescript
1774
- // Route cross-dock receipts to special location
1775
- 'custom.normalizeLocationRef': (value, data, config, helpers) => {
1776
- if (data.shipment?.is_crossdock) {
1777
- return 'DC-CROSSDOCK-RECEIVING'; // Special receiving zone
1778
- }
1779
- return value || config.defaultLocation;
1780
- };
1781
- ```
1782
-
1783
- ### Issue 5: Container Data Not Appearing in Fluent
1784
-
1785
- **Symptoms:**
1786
- - Container/pallet info missing in Fluent
1787
- - Can't track containers
1788
-
1789
- **Root Cause:**
1790
- - Fluent schema doesn't support container arrays directly
1791
- - Need to use attributes
1792
-
1793
- **Solution:**
1794
-
1795
- ```typescript
1796
- // Store containers in attributes instead of separate field
1797
- 'custom.buildReceiptAttributes': (value, data, config, helpers) => {
1798
- const attributes = [];
1799
-
1800
- // Serialize containers to JSON attribute
1801
- if (data.shipment?.containers) {
1802
- attributes.push({
1803
- name: 'containers',
1804
- type: 'STRING',
1805
- value: JSON.stringify(data.shipment.containers),
1806
- });
1807
-
1808
- // Also add container count for quick reference
1809
- attributes.push({
1810
- name: 'container_count',
1811
- type: 'INTEGER',
1812
- value: String(data.shipment.containers.length),
1813
- });
1814
- }
1815
-
1816
- return attributes;
1817
- };
1818
- ```
1819
-
1820
- ---
1821
-
1822
- ## Related Guides
1823
-
1824
- - **[Connector Platform Scheduled: Cycle Count Reconciliation](../workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md)** - Reconciliation patterns
1825
- - **[Universal Mapping Guide](../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)** - Field mapping patterns
1826
- - **[GraphQL Mutation Mapper](../../../02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md)** - Mutation generation
1827
- - **[XML Parsing Guide](../../../02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md)** - EDI 856 XML handling
1828
-
1829
- ---
1830
-
1831
- ## Production Checklist
1832
-
1833
- Before deploying to production:
1834
- - [ ] Test with real 3PL ASN payloads (XML and JSON)
1835
- - [ ] Configure carrier transit times based on real data
1836
- - [ ] Set up duplicate ASN monitoring/alerting
1837
- - [ ] Verify SKU validation strategy (batch vs individual)
1838
- - [ ] Test cross-dock scenario if applicable
1839
- - [ ] Configure webhook retry policy in 3PL system
1840
- - [ ] Set up error notifications (email/Slack)
1841
- - [ ] Document ASN format variations by 3PL
1842
- - [ ] Test large ASNs (100+ SKUs, 10+ containers)
1843
- - [ ] Verify expected date business days logic
1844
-
1845
- ---
1846
-
1847
- ## Performance Considerations
1848
-
1849
- **Large ASNs (100+ SKUs)**:
1850
-
1851
- ```typescript
1852
- // Process items in chunks to avoid timeouts
1853
- const CHUNK_SIZE = 50;
1854
-
1855
- for (let i = 0; i < items.length; i += CHUNK_SIZE) {
1856
- const chunk = items.slice(i, i + CHUNK_SIZE);
1857
- await processItemChunk(chunk);
1858
- }
1859
- ```
1860
-
1861
- **High Volume (1000+ ASNs/day)**:
1862
-
1863
- ```typescript
1864
- // Use background processing with queue
1865
- export const processAsnWebhookQueued = webhook('queue-asn', {
1866
- response: {
1867
- mode: 'sync',
1868
- onSuccess: (ctx) => new Response(JSON.stringify(ctx.data), {
1869
- status: 202, // Accepted
1870
- headers: { 'Content-Type': 'application/json' }
1871
- })
1872
- }
1873
- }, async (ctx) => {
1874
- const { activation } = ctx;
1875
-
1876
- // Immediately acknowledge receipt
1877
- const asnId = activation.body.shipmentId;
1878
-
1879
- // Queue for async processing (implement queueAsnForProcessing function)
1880
- await queueAsnForProcessing(asnId, activation.body);
1881
-
1882
- return {
1883
- success: true,
1884
- message: 'ASN queued for processing',
1885
- asnId,
1886
- };
1887
- });
1888
- ```
1889
-
1890
- ---
1891
-
1892
- ## Next Steps
1893
-
1894
- 1. **Add Receipt Confirmation**: Send confirmation back to 3PL when receipt created
1895
- 2. **Implement Discrepancy Handling**: Compare expected vs actual receipt quantities
1896
- 3. **Add Serial Number Tracking**: Full serial number lifecycle
1897
- 4. **Integrate with WMS**: Push ASN to warehouse management system
1898
- 5. **Build Dashboard**: Real-time ASN status monitoring
1899
- 6. **Add Carrier Tracking Integration**: Real-time shipment tracking via carrier APIs
1900
-
1901
- ---
1902
-
1903
- **Need Help?**
1904
- - Review SDK documentation: `/docs/readme.md`
1905
- - Check example connectors: `/connectors/Sample versori connectors/`
1906
- - Connector platform documentation: Check your platform's docs
1
+ ---
2
+ template_id: tpl-webhook-asn-purchase-order
3
+ canonical_filename: template-webhook-asn-purchase-order.md
4
+ version: 2.0.0
5
+ sdk_version: ^0.1.39
6
+ runtime: versori
7
+ direction: ingestion
8
+ source: webhook-xml-json-asn
9
+ destination: fluent-graphql
10
+ entity: inventory-receipt
11
+ format: xml-json
12
+ logging: versori
13
+ status: stable
14
+ features:
15
+ - webhook-signature-validation
16
+ - batched-events
17
+ - attribute-transformation
18
+ - memory-management
19
+ - enhanced-logging
20
+ ---
21
+
22
+ # Template: Webhook - ASN & Purchase Order Processing
23
+
24
+ **Template Version:** 2.0.0
25
+ **SDK Version:** @fluentcommerce/fc-connect-sdk@^0.1.39
26
+ **Last Updated:** 2025-01-24
27
+ **Deployment Target:** Versori Platform
28
+
29
+ **🆕 Version 2.0.0 Enhancements:**
30
+ - ✅ **Webhook Signature Validation** - Secure webhook verification with HMAC-SHA256
31
+ - ✅ **Batched Events** - Process events in optimized batches to reduce API calls
32
+ - ✅ **Attribute Transformation** - Handle nested arrays and complex data structures
33
+ - ✅ **Memory Management** - Clear large arrays after processing batches
34
+ - ✅ **Enhanced Logging** - Track batch processing and event submission with emoji indicators
35
+
36
+ **FC Connect SDK Use Case Guide**
37
+
38
+ > **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
39
+ > **Version**: Use latest - `npm install @fluentcommerce/fc-connect-sdk@latest`
40
+
41
+ **Context**: Process Advanced Ship Notices (ASN) from 3PL warehouse and create expected inventory receipts in Fluent Commerce
42
+
43
+ **Complexity**: Medium-High
44
+
45
+ **Runtime**: Versori Platform
46
+
47
+ **Estimated Lines**: ~750 lines (modular structure)
48
+
49
+ ---
50
+
51
+ ## STEP 1: Understand This Template
52
+
53
+ **What This Template Does:**
54
+
55
+ - HTTP webhook endpoint receiving ASN data from 3PL/WMS
56
+ - XML/JSON parsing for ASN payload (EDI 856 compatible structure)
57
+ - GraphQL mutation to create expected inventory receipts
58
+ - Container/pallet tracking support
59
+ - Cross-dock scenario handling (direct fulfillment without storage)
60
+ - Expected date calculation (carrier transit time)
61
+ - Custom resolvers for address normalization and SKU validation
62
+ - Audit trail and notification system
63
+ - Error handling for duplicate ASN prevention
64
+ - **Sync + Fire-and-Forget Pattern**: Fast webhook response, background processing
65
+
66
+ **Key SDK Components:**
67
+
68
+ - `createClient()` - Universal client factory (auto-detects Versori context)
69
+ - `GraphQLMutationMapper` - ASN → GraphQL mutation mapping
70
+ - `XMLParserService` / `JSONParserService` - ASN parsing (EDI 856 format)
71
+ - Native Versori `log` - Use `log` from context
72
+
73
+ **Entity Type:**
74
+
75
+ - **InventoryReceipt** - Fluent entity for expected inventory
76
+ - **GraphQL Mutation** - Uses `createInventoryReceipt` mutation
77
+
78
+ **Critical Patterns:**
79
+
80
+ - **Sync + Fire-and-Forget**: Webhook validates quickly, returns immediately, processes ASN in background
81
+ - **External JSON Config**: Mapping configuration in separate JSON file (`config/asn-mapping.json`)
82
+ - **Modular Architecture**: Separate services, workflows, config, types folders
83
+ - **Background Processing**: Long-running operations (GraphQL mutations, validation) happen asynchronously
84
+ - **Error Handling**: Comprehensive error handling with duplicate detection
85
+
86
+ **When to Use This Template:**
87
+
88
+ - ✅ ASN processing from 3PL/WMS systems
89
+ - ✅ EDI 856 format (XML or JSON)
90
+ - ✅ Need fast webhook response (don't wait for receipt creation)
91
+ - ✅ Container/pallet tracking
92
+ - ✅ Cross-dock scenarios
93
+
94
+ **When NOT to Use:**
95
+
96
+ - ❌ Bulk ASN processing (use Batch API or scheduled workflows)
97
+ - ❌ Real-time inventory updates (use Event API)
98
+ - ❌ Need synchronous receipt creation (wait for result before responding)
99
+
100
+ ---
101
+
102
+ ## STEP 2: Implementation Prompt for Claude Code
103
+
104
+ **Copy this prompt and send to Claude Code to generate the complete implementation:**
105
+
106
+ ```
107
+ Create a Versori webhook workflow for ASN (Advanced Ship Notice) processing to Fluent Commerce.
108
+
109
+ REQUIREMENTS:
110
+ 1. Runtime: Versori Platform (HTTP webhook)
111
+ 2. Source: ASN data via HTTP POST webhook (XML or JSON, EDI 856 format)
112
+ 3. Destination: Fluent Commerce GraphQL API (createInventoryReceipt mutation)
113
+ 4. Format: XML or JSON (EDI 856 compatible)
114
+ 5. Entity: InventoryReceipt (GraphQL mutation)
115
+
116
+ KEY FEATURES:
117
+ - Sync + fire-and-forget pattern (fast webhook response, background processing)
118
+ - External JSON mapping configuration (config/asn-mapping.json)
119
+ - Modular architecture (workflows/, services/, config/, types/)
120
+ - XML/JSON parsing with EDI 856 structure support
121
+ - GraphQLMutationMapper for ASN → GraphQL transformation
122
+ - Custom resolvers for address normalization and SKU validation
123
+ - Duplicate ASN detection and prevention
124
+ - Audit trail (save input/output files)
125
+
126
+ CRITICAL REQUIREMENTS:
127
+ 1. Webhook Mode: response: { mode: 'sync' } (fast response)
128
+ 2. Background Processing: Fire-and-forget pattern (no await on long operations)
129
+ 3. Mapping Config: External JSON file (config/asn-mapping.json)
130
+ 4. Modular Structure: Separate services/, config/, types/ folders
131
+ 5. Native Logging: Use log from context (no LoggingService)
132
+ 6. Error Handling: Duplicate detection, comprehensive error responses
133
+
134
+ SDK METHODS TO USE:
135
+ - createClient({ ...ctx, log }) - Pass full Versori context
136
+ - new GraphQLMutationMapper(mappingConfig, log, { fluentClient: client }) - Initialize mapper
137
+ - mapper.mapWithNodes(asnData, customResolvers, context) - Map ASN with custom resolvers (REQUIRED for custom resolvers)
138
+ - mapWithNodes() now returns query automatically (no need for buildMutation())
139
+ - client.graphql({ query, variables }) - Execute GraphQL mutation
140
+
141
+ FORBIDDEN PATTERNS:
142
+ - ❌ Inline mapping config (use external JSON)
143
+ - ❌ await on background processing (use fire-and-forget)
144
+ - ❌ LoggingService (use native log from context)
145
+ - ❌ All code in one file (use modular structure)
146
+ - ❌ async mode webhook (use sync + fire-and-forget)
147
+ ```
148
+
149
+ ---
150
+
151
+ ## STEP 3: Detailed Flow Documentation
152
+
153
+ ### Complete Processing Flow
154
+
155
+ ```
156
+ ┌─────────────────────────────────────────────────────────────┐
157
+ │ 1. WEBHOOK RECEIVED │
158
+ │ POST https://{workspace}.versori.run/process-asn │
159
+ │ Content-Type: application/xml or application/json │
160
+ │ Body: <ShipNotice>...</ShipNotice> or { ... } │
161
+ └────────────────────┬────────────────────────────────────────┘
162
+
163
+
164
+ ┌─────────────────────────────────────────────────────────────┐
165
+ │ 2. QUICK VALIDATION (Synchronous, ~10-50ms) │
166
+ │ - Check fluent_commerce connection exists │
167
+ │ - Validate ASN payload present │
168
+ │ - Return HTTP 200 OK immediately │
169
+ └────────────────────┬────────────────────────────────────────┘
170
+
171
+
172
+ ┌─────────────────────────────────────────────────────────────┐
173
+ │ 3. BACKGROUND PROCESSING (Fire-and-Forget) │
174
+ │ ┌─────────────────────────────────────────────────────┐ │
175
+ │ │ 3a. Initialize Fluent Client │ │
176
+ │ │ - createClient({ ...ctx, log }) │ │
177
+ │ └─────────────────────────────────────────────────────┘ │
178
+ │ ┌─────────────────────────────────────────────────────┐ │
179
+ │ │ 3b. Parse ASN (XML or JSON) │ │
180
+ │ │ - XMLParserService or JSONParserService │ │
181
+ │ │ - Extract ASN identifier │ │
182
+ │ └─────────────────────────────────────────────────────┘ │
183
+ │ ┌─────────────────────────────────────────────────────┐ │
184
+ │ │ 3c. Check for Duplicates │ │
185
+ │ │ - Query Fluent for existing receipt │ │
186
+ │ │ - Return early if duplicate │ │
187
+ │ └─────────────────────────────────────────────────────┘ │
188
+ │ ┌─────────────────────────────────────────────────────┐ │
189
+ │ │ 3d. Map ASN to GraphQL Variables │ │
190
+ │ │ - Load mapping config from JSON │ │
191
+ │ │ - GraphQLMutationMapper.map() │ │
192
+ │ │ - Apply custom resolvers │ │
193
+ │ │ - Returns { success, data, query, context } │ │
194
+ │ └─────────────────────────────────────────────────────┘ │
195
+ │ ┌─────────────────────────────────────────────────────┐ │
196
+ │ │ 3e. Execute GraphQL Mutation │ │
197
+ │ │ - client.graphql({ query: result.query, │ │
198
+ │ │ variables: result.variables })│ │
199
+ │ └─────────────────────────────────────────────────────┘ │
200
+ │ ┌─────────────────────────────────────────────────────┐ │
201
+ │ │ 3g. Save Audit Trail (Optional) │ │
202
+ │ │ - asn-input.json │ │
203
+ │ │ - mapped-variables.json │ │
204
+ │ │ - graphql-response.json │ │
205
+ │ └─────────────────────────────────────────────────────┘ │
206
+ └─────────────────────────────────────────────────────────────┘
207
+ ```
208
+
209
+ ### Response Timing
210
+
211
+ | Stage | Timing | Blocking |
212
+ |-------|--------|----------|
213
+ | **Webhook Validation** | ~10-50ms | ✅ Yes (blocks response) |
214
+ | **Background Processing** | ~1000-3000ms | ❌ No (fire-and-forget) |
215
+ | **Total Response Time** | ~10-50ms | ✅ Fast response |
216
+
217
+ **Key Benefit**: Webhook caller receives immediate acknowledgment (~50ms) while ASN processing happens in background (~2-3s).
218
+
219
+ ---
220
+
221
+ ## STEP 4: Production Modular Structure
222
+
223
+ > **✅ This section shows the COMPLETE production-ready modular structure.**
224
+ > All files are shown with proper imports/exports and folder organization.
225
+
226
+ ### Complete Project Structure
227
+
228
+ ```
229
+ asn-purchase-order-processing/
230
+ ├── package.json # Dependencies and Versori config
231
+ ├── index.ts # Entry point - exports all workflows
232
+ └── src/
233
+ ├── workflows/
234
+ │ └── webhook/
235
+ │ └── asn-receipt.ts # Webhook: Receive ASN notifications
236
+
237
+ ├── services/
238
+ │ └── asn-processing.service.ts # Shared orchestration logic (reusable)
239
+
240
+ ├── resolvers/
241
+ │ └── asn-resolvers.ts # Custom resolvers for transformations
242
+
243
+ ├── config/
244
+ │ └── asn-mapping.json # Mapping configuration (external JSON)
245
+
246
+ └── types/
247
+ └── asn.types.ts # TypeScript interfaces
248
+ ```
249
+
250
+ **Why This Structure?**
251
+
252
+ - ✅ **Clear separation**: Webhook handlers vs business logic
253
+ - ✅ **Reusable services**: ASN processing logic can be reused
254
+ - ✅ **External config**: Mapping changes don't require code changes
255
+ - ✅ **Custom resolvers**: Separate file for complex transformations
256
+ - ✅ **Type safety**: TypeScript interfaces for better IDE support
257
+ - ✅ **Scalable**: Easy to add new ASN formats or processing steps
258
+
259
+ ---
260
+
261
+ ## SDK Methods Used
262
+
263
+ ```typescript
264
+ // Core SDK imports
265
+ import {
266
+ createClient,
267
+ GraphQLMutationMapper,
268
+ XMLParserService,
269
+ JSONParserService,
270
+ } from '@fluentcommerce/fc-connect-sdk';
271
+
272
+ // Versori imports
273
+ import { webhook } from '@versori/run';
274
+ import { Buffer } from 'node:buffer'; // Required for Versori runtime
275
+
276
+ // Key methods
277
+ const logger = { info, warn, error, debug }; // Map Versori log to SDK Logger interface
278
+ const client = await createClient({ ...ctx, log: logger }); // Auto-detects Versori context
279
+ new GraphQLMutationMapper(config, logger, { fluentClient: client }); // Map ASN to GraphQL (uses logger adapter)
280
+ new XMLParserService(); // Parse ASN XML (EDI 856)
281
+ mapper.mapWithNodes(asnData, resolvers, context); // Apply mapping with custom logic
282
+ // mapWithNodes() now returns query automatically - use result.query
283
+ client.graphql({ query, variables }); // Execute mutation
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Versori Workflows Structure
289
+
290
+ **Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
291
+
292
+ **Trigger Types:**
293
+ - **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
294
+ - **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
295
+ - **`http()`** → External API calls (chained from webhook/schedule)
296
+ - **`fn()`** → Internal processing (chained from webhook/schedule)
297
+
298
+ ### Recommended Project Structure
299
+
300
+ ```
301
+ asn-purchase-order-processing/
302
+ ├── index.ts # Entry point - exports all workflows
303
+ └── src/
304
+ ├── workflows/
305
+ │ └── webhook/
306
+ │ └── asn-receipt.ts # Webhook: Receive ASN notifications
307
+
308
+ ├── services/
309
+ │ └── asn-processing.service.ts # Shared orchestration logic (reusable)
310
+
311
+ └── config/
312
+ └── asn-mapping.json # Mapping configuration
313
+ ```
314
+
315
+ **Benefits:**
316
+ - ✅ Clear trigger separation (`webhook/` vs `scheduled/`)
317
+ - ✅ Descriptive file names (easy to browse and understand)
318
+ - ✅ Scalable (add new workflows without cluttering)
319
+ - ✅ Reusable code in `services/` (DRY principle)
320
+ - ✅ Easy to modify individual workflows without affecting others
321
+
322
+ ---
323
+
324
+ ## Complete Working Code
325
+
326
+ ### 1. Entry Point: `index.ts`
327
+
328
+ ```typescript
329
+ /**
330
+ * Entry point - Export all workflows for Versori platform
331
+ *
332
+ * This file exports all workflows to be registered with Versori.
333
+ * Each workflow is defined in its own file for better organization.
334
+ *
335
+ * MEMORY INTERPRETER PATTERN:
336
+ * Versori's interpreter reads this file and registers all exported workflows.
337
+ * Do NOT use dynamic imports or conditional exports.
338
+ */
339
+
340
+ // Webhook workflows
341
+ export { processAsnWebhook } from './workflows/webhook/asn-receipt';
342
+ export { manualAsnTest } from './workflows/webhook/asn-receipt';
343
+ export { healthCheck } from './workflows/webhook/asn-receipt';
344
+ ```
345
+
346
+ ### 2. Main Webhook Workflow: `workflows/webhook/asn-receipt.ts`
347
+
348
+ ```typescript
349
+ /**
350
+ * ASN (Advanced Ship Notice) Processing Webhook
351
+ *
352
+ * Receives ASN notifications from Acme 3PL/WMS and creates expected inventory
353
+ * receipts in Fluent Commerce.
354
+ *
355
+ * ASN = Advanced Ship Notice (EDI 856) - notification that shipment is on the way
356
+ *
357
+ * Flow:
358
+ * 1. Receive ASN webhook (XML or JSON payload from 3PL)
359
+ * 2. Parse ASN structure (shipment header, containers, items)
360
+ * 3. Map to Fluent expected receipt format
361
+ * 4. Calculate expected arrival date (based on carrier and transit time)
362
+ * 5. Create inventory receipt mutation (creates expected inventory)
363
+ * 6. Send confirmation response to 3PL
364
+ * 7. Optional: Send notification to warehouse ops team
365
+ */
366
+ import { webhook } from '@versori/run';
367
+ import { Buffer } from 'node:buffer'; // Required for Versori runtime
368
+ import {
369
+ createClient,
370
+ GraphQLMutationMapper,
371
+ XMLParserService,
372
+ JSONParserService,
373
+ } from '@fluentcommerce/fc-connect-sdk';
374
+ import { asnResolvers } from '../../resolvers/asn-resolvers';
375
+ import asnMappingConfig from '../../mappings/asn-to-fluent-receipt.json' with { type: 'json' };
376
+
377
+ /**
378
+ * Main ASN webhook endpoint
379
+ * Expects POST with XML or JSON ASN payload
380
+ */
381
+ export const processAsnWebhook = webhook('process-asn', {
382
+ response: {
383
+ mode: 'sync',
384
+ onSuccess: (ctx) => new Response(JSON.stringify(ctx.data), {
385
+ status: 200,
386
+ headers: { 'Content-Type': 'application/json' }
387
+ }),
388
+ onError: (ctx, error) => new Response(JSON.stringify({
389
+ success: false,
390
+ error: error.message,
391
+ recommendation: error.message?.includes('authentication') || error.message?.includes('401')
392
+ ? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
393
+ : error.message?.includes('mapping') || error.message?.includes('field')
394
+ ? 'Check mapping configuration JSON and verify source paths match incoming ASN structure'
395
+ : error.message?.includes('connection') || error.message?.includes('timeout')
396
+ ? 'Check network connectivity and Fluent Commerce API availability'
397
+ : 'Review error details and check ASN payload structure',
398
+ timestamp: new Date().toISOString()
399
+ }), {
400
+ status: 500,
401
+ headers: { 'Content-Type': 'application/json' }
402
+ })
403
+ }
404
+ }, async (ctx) => {
405
+ const { log, fetch, activation, connections, openKv } = ctx;
406
+ const startTime = Date.now();
407
+
408
+ log.info('🚚 [ASN] Processing Advanced Ship Notice');
409
+
410
+ // ? Enhanced: Configuration validation
411
+ if (!connections || !connections.fluent_commerce) {
412
+ log.error('❌ [ASN] Configuration error: Missing fluent_commerce connection', {
413
+ recommendation: 'Configure fluent_commerce connection in Connections section with OAuth2 credentials'
414
+ });
415
+ throw new Error('Missing fluent_commerce connection');
416
+ }
417
+
418
+ try {
419
+ // =================================================================
420
+ // STEP 1: EXTRACT AND PARSE ASN PAYLOAD
421
+ // =================================================================
422
+ // Get webhook payload
423
+ // Supports both XML (EDI 856 format) and JSON
424
+ const rawPayload = activation?.body;
425
+ const contentType = activation?.headers?.['content-type'] || 'application/json';
426
+
427
+ log.info('📦 [ASN] Payload received', {
428
+ contentType,
429
+ payloadSize: JSON.stringify(rawPayload).length,
430
+ });
431
+
432
+ // Determine format and parse
433
+ let asnData: any;
434
+ if (contentType.includes('xml') || contentType.includes('text')) {
435
+ // Parse XML ASN (EDI 856 format)
436
+ log.info('📄 [ASN] Parsing XML payload');
437
+ const xmlParser = new XMLParserService();
438
+ asnData = await xmlParser.parse(rawPayload);
439
+ log.debug('✅ [ASN] XML parsed successfully', { rootKeys: Object.keys(asnData) });
440
+ } else {
441
+ // Assume JSON format
442
+ log.info('📝 [ASN] Processing JSON payload');
443
+ asnData = rawPayload;
444
+ }
445
+
446
+ // Extract ASN identifier for tracking
447
+ // Common paths: ASN.shipment_id, ShipNotice.shipment_number, etc.
448
+ const asnId =
449
+ asnData?.ShipNotice?.['@shipment_id'] ||
450
+ asnData?.ASN?.shipment_number ||
451
+ asnData?.shipmentId ||
452
+ 'UNKNOWN';
453
+
454
+ log.info(`🔍 [ASN] Processing ASN: ${asnId}`);
455
+
456
+ // =================================================================
457
+ // STEP 2: CREATE FLUENT CLIENT
458
+ // =================================================================
459
+ // Create SDK logger adapter to map Versori log to SDK Logger interface
460
+ const logger = {
461
+ info: (msg: string, ...args: any[]) => log.info(msg, ...args),
462
+ warn: (msg: string, ...args: any[]) => log.warn(msg, ...args),
463
+ error: (msg: string, ...args: any[]) => log.error(msg, ...args),
464
+ debug: (msg: string, ...args: any[]) => log.debug?.(msg, ...args) || log.info(msg, ...args)
465
+ };
466
+
467
+ // Create context for SDK client factory
468
+ const fluentClient = await createClient({ ...ctx, log: logger }); // Auto-detects Versori context
469
+
470
+ log.info('✅ [ASN] Fluent client initialized');
471
+
472
+ // =================================================================
473
+ // STEP 3: VALIDATE CONNECTION
474
+ // =================================================================
475
+ log.info('🔌 [ASN] Validating Fluent connection');
476
+ await fluentClient.validateConnection();
477
+ log.info('✅ [ASN] Connection validated successfully');
478
+
479
+ // =================================================================
480
+ // STEP 4: VALIDATE ASN (PREVENT DUPLICATES)
481
+ // =================================================================
482
+ // Query existing receipts to check if ASN already processed
483
+ // IMPORTANT: Prevents duplicate expected inventory from same ASN
484
+ log.info('🔍 [ASN] Checking for duplicate ASN');
485
+ const duplicateCheckQuery = `
486
+ query CheckExistingReceipt($ref: String!) {
487
+ inventoryQuantities(ref: $ref, first: 1) {
488
+ edges {
489
+ node {
490
+ id
491
+ ref
492
+ status
493
+ }
494
+ }
495
+ }
496
+ }`;
497
+
498
+ const duplicateCheck = await fluentClient.graphql({
499
+ query: duplicateCheckQuery,
500
+ variables: {
501
+ ref: `ASN-${asnId}`, // Use ASN ID as ref
502
+ },
503
+ });
504
+
505
+ const existingReceipt = duplicateCheck.data?.inventoryQuantities?.edges?.[0]?.node;
506
+
507
+ if (existingReceipt) {
508
+ log.warn('⚠️ [ASN] Duplicate detected - ASN already processed', {
509
+ asnId,
510
+ existingReceiptId: existingReceipt.id,
511
+ existingStatus: existingReceipt.status,
512
+ });
513
+
514
+ return {
515
+ success: false,
516
+ message: 'ASN already processed (duplicate)',
517
+ asnId,
518
+ existingReceiptId: existingReceipt.id,
519
+ timestamp: new Date().toISOString(),
520
+ duration: `${Date.now() - startTime}ms`,
521
+ };
522
+ }
523
+
524
+ // =================================================================
525
+ // STEP 5: SAVE RAW ASN FOR AUDIT TRAIL (KV Storage - Versori-compatible)
526
+ // =================================================================
527
+ log.info('Saving raw payload for audit trail');
528
+ try {
529
+ const kv = openKv(':project:');
530
+ const timestamp = new Date().toISOString();
531
+ const auditKey = ['asn', 'audit', asnId, timestamp];
532
+
533
+ // Save original payload
534
+ await kv.set([...auditKey, 'asn-raw'], asnData);
535
+
536
+ log.info('Raw ASN saved to KV storage', { asnId, timestamp });
537
+ } catch (kvError: any) {
538
+ log.warn('Failed to save ASN to KV storage', { error: kvError.message });
539
+ }
540
+
541
+ // =================================================================
542
+ // STEP 6: MAP ASN TO FLUENT EXPECTED RECEIPT FORMAT
543
+ // =================================================================
544
+ log.info('🗺️ [ASN] Starting ASN mapping');
545
+ const mappingStartTime = Date.now();
546
+
547
+ const mapper = new GraphQLMutationMapper(
548
+ asnMappingConfig as any,
549
+ logger,
550
+ { fluentClient: fluentClient as any }
551
+ );
552
+
553
+ // Apply mapping with custom resolvers
554
+ // Context includes fluentClient for API calls (SKU validation, etc.)
555
+ // Returns MapWithNodesResult with query auto-generated!
556
+ const mappingResult = await mapper.mapWithNodes(asnData, asnResolvers, {
557
+ fluentClient: fluentClient as any,
558
+ asnId,
559
+ config: {
560
+ retailerId: process.env.RETAILER_ID || '1',
561
+ defaultLocation: process.env.DEFAULT_RECEIVING_LOCATION || 'DC-RECEIVING',
562
+ },
563
+ helpers: {
564
+ fluentClient: fluentClient as any,
565
+ },
566
+ });
567
+
568
+ if (!mappingResult.success) {
569
+ log.error('❌ [ASN] Mapping failed', {
570
+ errors: mappingResult.errors,
571
+ recommendation: 'Check mapping configuration JSON and verify source paths match incoming ASN structure'
572
+ });
573
+ throw new Error(`ASN mapping failed: ${mappingResult.errors?.join(', ')}`);
574
+ }
575
+
576
+ log.info('✅ [ASN] Mapping successful', {
577
+ asnId,
578
+ itemCount: mappingResult.data?.items?.length || 0,
579
+ duration: `${Date.now() - mappingStartTime}ms`,
580
+ });
581
+
582
+ // Save mapped data to KV storage
583
+ try {
584
+ const kv = openKv(':project:');
585
+ const timestamp = new Date().toISOString();
586
+ const auditKey = ['asn', 'audit', asnId, timestamp, 'fluent-mapped'];
587
+ await kv.set(auditKey, mappingResult.data);
588
+ } catch (kvError: any) {
589
+ log.warn('Failed to save mapped data to KV', { error: kvError.message });
590
+ }
591
+
592
+ // =================================================================
593
+ // STEP 7: CREATE EXPECTED RECEIPT IN FLUENT
594
+ // =================================================================
595
+ log.info('🚀 [ASN] Creating expected inventory receipt in Fluent');
596
+ const mutationStartTime = Date.now();
597
+
598
+ // mapWithNodes() auto-generates query - no need to call buildMutation()!
599
+ // Execute mutation (use result.variables for GraphQL execution)
600
+ const receiptResult = await fluentClient.graphql({
601
+ query: mappingResult.query,
602
+ variables: mappingResult.variables // ✅ Use variables (wrapped if fields pattern)
603
+ });
604
+
605
+ if (receiptResult.errors) {
606
+ log.error('❌ [ASN] Receipt creation failed', {
607
+ errors: receiptResult.errors,
608
+ recommendation: 'Check GraphQL mutation structure and field types',
609
+ });
610
+ throw new Error(`Receipt creation failed: ${JSON.stringify(receiptResult.errors)}`);
611
+ }
612
+
613
+ const createdReceipt = receiptResult.data?.createInventoryQuantity;
614
+
615
+ log.info('✅ [ASN] Expected receipt created successfully', {
616
+ receiptId: createdReceipt?.id,
617
+ asnId,
618
+ duration: `${Date.now() - mutationStartTime}ms`,
619
+ });
620
+
621
+ // Save Fluent response to KV storage
622
+ try {
623
+ const kv = openKv(':project:');
624
+ const timestamp = new Date().toISOString();
625
+ const auditKey = ['asn', 'audit', asnId, timestamp, 'fluent-response'];
626
+ await kv.set(auditKey, receiptResult);
627
+ } catch (kvError: any) {
628
+ log.warn('Failed to save Fluent response to KV', { error: kvError.message });
629
+ }
630
+
631
+ // =================================================================
632
+ // STEP 8: RETURN SUCCESS RESPONSE TO 3PL
633
+ // =================================================================
634
+ const totalDuration = Date.now() - startTime;
635
+ log.info('🎉 [ASN] Processing completed successfully', {
636
+ asnId,
637
+ totalDuration: `${totalDuration}ms`,
638
+ });
639
+
640
+ return {
641
+ success: true,
642
+ message: 'ASN processed successfully',
643
+ data: {
644
+ asnId,
645
+ receiptId: createdReceipt?.id,
646
+ receiptRef: createdReceipt?.ref,
647
+ itemCount: mappingResult.data?.items?.length || 0,
648
+ expectedDate: mappingResult.data?.expectedOn,
649
+ timestamp: new Date().toISOString(),
650
+ duration: `${totalDuration}ms`,
651
+ },
652
+ };
653
+ } catch (error: any) {
654
+ // ? Enhanced: Error logging with recommendations
655
+ const totalDuration = Date.now() - startTime;
656
+ const errorDetails = {
657
+ message: error instanceof Error ? error.message : String(error),
658
+ stack: error instanceof Error ? error.stack : undefined,
659
+ errorType: error instanceof Error ? error.constructor.name : 'Error',
660
+ duration: `${totalDuration}ms`,
661
+ recommendation: error.message?.includes('authentication') || error.message?.includes('401')
662
+ ? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
663
+ : error.message?.includes('mapping') || error.message?.includes('field')
664
+ ? 'Check mapping configuration JSON and verify source paths match incoming ASN structure'
665
+ : error.message?.includes('connection') || error.message?.includes('timeout')
666
+ ? 'Check network connectivity and Fluent Commerce API availability'
667
+ : error.message?.includes('validation') || error.message?.includes('required')
668
+ ? 'Ensure all required fields are present in the ASN webhook payload'
669
+ : error.message?.includes('duplicate') || error.message?.includes('already processed')
670
+ ? 'ASN may have been processed already - check existing receipts'
671
+ : 'Review error details and check ASN payload structure',
672
+ };
673
+ log.error('❌ [ASN] Processing failed', errorDetails);
674
+ throw error; // Let response handler catch it
675
+ }
676
+ });
677
+
678
+ /**
679
+ * Manual test endpoint - Upload ASN file for testing
680
+ */
681
+ export const manualAsnTest = webhook('manual-asn-test', {
682
+ response: {
683
+ mode: 'sync',
684
+ onSuccess: (ctx) => new Response(JSON.stringify(ctx.data), {
685
+ status: 200,
686
+ headers: { 'Content-Type': 'application/json' }
687
+ }),
688
+ onError: (ctx, error) => new Response(JSON.stringify({
689
+ success: false,
690
+ error: error.message,
691
+ recommendation: 'Check that test ASN file exists at expected path'
692
+ }), {
693
+ status: 400,
694
+ headers: { 'Content-Type': 'application/json' }
695
+ })
696
+ }
697
+ }, async (ctx) => {
698
+ const { log } = ctx;
699
+ const startTime = Date.now();
700
+
701
+ log.info('🧪 [TEST] Manual ASN test triggered');
702
+
703
+ // Load test ASN data
704
+ const testAsnPath = path.join(__dirname, '../../data/test-asn-sample.json');
705
+ if (!fs.existsSync(testAsnPath)) {
706
+ log.error('❌ [TEST] Test ASN file not found', { expectedPath: testAsnPath });
707
+ return {
708
+ success: false,
709
+ error: 'Test ASN file not found',
710
+ expectedPath: testAsnPath,
711
+ recommendation: 'Create test-asn-sample.json in the data/ directory',
712
+ };
713
+ }
714
+
715
+ const testAsnData = JSON.parse(fs.readFileSync(testAsnPath, 'utf-8'));
716
+
717
+ log.info('✅ [TEST] Test ASN loaded', {
718
+ asnId: testAsnData.shipmentId,
719
+ duration: `${Date.now() - startTime}ms`,
720
+ });
721
+
722
+ // Note: In production, you would trigger the main workflow via HTTP call
723
+ // This is a simplified test endpoint for development
724
+ return {
725
+ success: true,
726
+ message: 'Test ASN loaded - trigger processAsnWebhook endpoint to process',
727
+ asnId: testAsnData.shipmentId,
728
+ testDataPath: testAsnPath,
729
+ duration: `${Date.now() - startTime}ms`,
730
+ };
731
+ });
732
+
733
+ /**
734
+ * Health check endpoint
735
+ */
736
+ export const healthCheck = webhook('health-check', {
737
+ response: {
738
+ mode: 'sync',
739
+ onSuccess: (ctx) => new Response(JSON.stringify(ctx.data), {
740
+ status: 200,
741
+ headers: { 'Content-Type': 'application/json' }
742
+ })
743
+ }
744
+ }, async (ctx) => {
745
+ const { log } = ctx;
746
+ log.info('💚 [HEALTH] Health check requested');
747
+
748
+ return {
749
+ success: true,
750
+ service: 'ASN Processing',
751
+ status: 'healthy',
752
+ timestamp: new Date().toISOString(),
753
+ };
754
+ });
755
+ ```
756
+
757
+ ### 2. Mapping Configuration: `mappings/asn-to-fluent-receipt.json`
758
+
759
+ ```json
760
+ {
761
+ "direction": "ingest",
762
+ "sourceFormat": "json",
763
+ "mutation": "createInventoryQuantity",
764
+ "fields": {
765
+ "ref": {
766
+ "resolver": "custom.generateReceiptRef",
767
+ "comment": "Generate unique receipt ref from ASN ID (REQUIRED)"
768
+ },
769
+ "locationRef": {
770
+ "source": "destination.location_code",
771
+ "resolver": "custom.normalizeLocationRef",
772
+ "comment": "Receiving location (REQUIRED)"
773
+ },
774
+ "type": {
775
+ "value": "EXPECTED",
776
+ "comment": "Expected inventory type (REQUIRED)"
777
+ },
778
+ "status": {
779
+ "value": "EXPECTED",
780
+ "comment": "Status is EXPECTED until physically received (REQUIRED)"
781
+ },
782
+ "expectedOn": {
783
+ "resolver": "custom.calculateExpectedDate",
784
+ "comment": "Calculate based on ship date + carrier transit time (REQUIRED)"
785
+ },
786
+ "carrier": {
787
+ "source": "shipment.carrier.name",
788
+ "resolver": "sdk.trim",
789
+ "comment": "Shipping carrier name"
790
+ },
791
+ "trackingNumber": {
792
+ "source": "shipment.tracking_number",
793
+ "resolver": "sdk.trim",
794
+ "comment": "Carrier tracking number"
795
+ },
796
+ "retailer.id": {
797
+ "resolver": "custom.getRetailerId",
798
+ "comment": "Retailer ID from config (REQUIRED)"
799
+ },
800
+ "items": {
801
+ "source": "shipment.items",
802
+ "isArray": true,
803
+ "comment": "Line items in the shipment",
804
+ "fields": {
805
+ "skuRef": {
806
+ "source": "$.sku",
807
+ "resolver": "custom.validateAndNormalizeSku",
808
+ "comment": "Product SKU - must exist in Fluent (REQUIRED)"
809
+ },
810
+ "qty": {
811
+ "source": "$.quantity",
812
+ "resolver": "sdk.parseInt",
813
+ "comment": "Expected quantity (REQUIRED)"
814
+ },
815
+ "lotNumber": {
816
+ "source": "$.lot_number",
817
+ "required": false,
818
+ "comment": "Lot/batch number if applicable"
819
+ },
820
+ "serialNumbers": {
821
+ "source": "$.serial_numbers",
822
+ "isArray": true,
823
+ "required": false,
824
+ "comment": "Serial numbers for serialized items"
825
+ },
826
+ "expiryDate": {
827
+ "source": "$.expiry_date",
828
+ "resolver": "sdk.formatDate",
829
+ "required": false,
830
+ "comment": "Expiration date for perishable goods"
831
+ },
832
+ "containerRef": {
833
+ "source": "$.container_id",
834
+ "required": false,
835
+ "comment": "Container/pallet ID for tracking"
836
+ }
837
+ }
838
+ },
839
+ "containers": {
840
+ "source": "shipment.containers",
841
+ "isArray": true,
842
+ "required": false,
843
+ "comment": "Container/pallet tracking information",
844
+ "fields": {
845
+ "containerRef": {
846
+ "source": "$.container_id",
847
+ "resolver": "sdk.toString"
848
+ },
849
+ "containerType": {
850
+ "source": "$.type",
851
+ "resolver": "sdk.uppercase",
852
+ "comment": "PALLET, CARTON, TOTE, etc."
853
+ },
854
+ "weight": {
855
+ "source": "$.weight",
856
+ "resolver": "sdk.parseFloat"
857
+ },
858
+ "weightUnit": {
859
+ "source": "$.weight_unit",
860
+ "defaultValue": "LBS"
861
+ },
862
+ "dimensions": {
863
+ "resolver": "custom.formatDimensions",
864
+ "comment": "Format as length x width x height"
865
+ }
866
+ }
867
+ },
868
+ "attributes": {
869
+ "resolver": "custom.buildReceiptAttributes",
870
+ "comment": "Custom attributes for audit trail and tracking"
871
+ }
872
+ }
873
+ }
874
+ ```
875
+
876
+ ### 3. Custom Resolvers: `src/resolvers/asn-resolvers.ts`
877
+
878
+ ```typescript
879
+ /**
880
+ * ASN Processing Custom Resolvers
881
+ */
882
+ import type { ResolverMap } from './types';
883
+
884
+ export const asnResolvers: ResolverMap = {
885
+ /**
886
+ * Generate unique receipt reference from ASN ID
887
+ */
888
+ 'custom.generateReceiptRef': (value: any, data: any, config: any, helpers: any): string => {
889
+ const asnId = data.shipmentId || data.shipment_id || data.ASN?.id || 'UNKNOWN';
890
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
891
+ return `ASN-${asnId}-${timestamp}`;
892
+ },
893
+
894
+ /**
895
+ * Normalize location reference
896
+ * Handles different location code formats from various 3PLs
897
+ */
898
+ 'custom.normalizeLocationRef': (value: any, data: any, config: any, helpers: any): string => {
899
+ if (!value) {
900
+ return config.defaultLocation || 'DC-RECEIVING';
901
+ }
902
+
903
+ // Normalize format: uppercase, replace spaces with hyphens
904
+ return String(value)
905
+ .toUpperCase()
906
+ .trim()
907
+ .replace(/\s+/g, '-');
908
+ },
909
+
910
+ /**
911
+ * Calculate expected arrival date
912
+ * Based on ship date + carrier transit time
913
+ */
914
+ 'custom.calculateExpectedDate': (value: any, data: any, config: any, helpers: any): string => {
915
+ const shipDate = data.shipment?.ship_date || data.shipDate;
916
+ const carrier = data.shipment?.carrier?.name || data.carrier;
917
+
918
+ // Default transit times by carrier (in days)
919
+ const transitTimes: Record<string, number> = {
920
+ 'FEDEX_GROUND': 3,
921
+ 'FEDEX_EXPRESS': 1,
922
+ 'UPS_GROUND': 3,
923
+ 'UPS_NEXT_DAY': 1,
924
+ 'USPS': 5,
925
+ 'DHL': 2,
926
+ 'FREIGHT': 7,
927
+ 'DEFAULT': 3,
928
+ };
929
+
930
+ // Get transit time
931
+ const carrierKey = String(carrier).toUpperCase().replace(/\s+/g, '_');
932
+ const transitDays = transitTimes[carrierKey] || transitTimes.DEFAULT;
933
+
934
+ // Calculate expected date
935
+ const shipDateObj = shipDate ? new Date(shipDate) : new Date();
936
+ shipDateObj.setDate(shipDateObj.getDate() + transitDays);
937
+
938
+ return shipDateObj.toISOString();
939
+ },
940
+
941
+ /**
942
+ * Validate and normalize SKU
943
+ * Ensures SKU exists in Fluent Commerce (ASYNC)
944
+ */
945
+ 'custom.validateAndNormalizeSku': async (
946
+ value: any,
947
+ data: any,
948
+ config: any,
949
+ helpers: any
950
+ ): Promise<string> => {
951
+ const sku = String(value).trim().toUpperCase();
952
+
953
+ if (!helpers.fluentClient) {
954
+ // If no client, just return normalized SKU
955
+ return sku;
956
+ }
957
+
958
+ // Query Fluent to validate SKU exists
959
+ const query = `
960
+ query ValidateSku($skuRef: [String!]) {
961
+ products(ref: $skuRef, first: 1) {
962
+ edges {
963
+ node {
964
+ id
965
+ ref
966
+ }
967
+ }
968
+ }
969
+ }`;
970
+
971
+ try {
972
+ const result = await helpers.fluentClient.graphql({
973
+ query,
974
+ variables: { skuRef: [sku] },
975
+ });
976
+
977
+ const product = result.data?.products?.edges?.[0]?.node;
978
+
979
+ if (!product) {
980
+ helpers.logger?.warn('SKU not found in Fluent Commerce', { sku });
981
+ // Option 1: Throw error to halt processing
982
+ // throw new Error(`SKU not found: ${sku}`);
983
+
984
+ // Option 2: Return SKU anyway (let Fluent validation handle it)
985
+ return sku;
986
+ }
987
+
988
+ helpers.logger?.debug('SKU validated successfully', { sku, productId: product.id });
989
+ return sku;
990
+ } catch (error: any) {
991
+ helpers.logger?.error('SKU validation failed', { sku, error: error.message });
992
+ return sku; // Return SKU anyway, let Fluent handle validation
993
+ }
994
+ },
995
+
996
+ /**
997
+ * Get retailer ID from configuration
998
+ */
999
+ 'custom.getRetailerId': (value: any, data: any, config: any, helpers: any): string => {
1000
+ return config.retailerId || process.env.RETAILER_ID || '1';
1001
+ },
1002
+
1003
+ /**
1004
+ * Format dimensions as string
1005
+ */
1006
+ 'custom.formatDimensions': (value: any, data: any, config: any, helpers: any): string => {
1007
+ const container = data; // Current container object
1008
+ const length = container.length || 0;
1009
+ const width = container.width || 0;
1010
+ const height = container.height || 0;
1011
+ const unit = container.dimension_unit || 'IN';
1012
+
1013
+ if (!length && !width && !height) {
1014
+ return '';
1015
+ }
1016
+
1017
+ return `${length} x ${width} x ${height} ${unit}`;
1018
+ },
1019
+
1020
+ /**
1021
+ * Build receipt attributes for audit trail
1022
+ */
1023
+ 'custom.buildReceiptAttributes': (
1024
+ value: any,
1025
+ data: any,
1026
+ config: any,
1027
+ helpers: any
1028
+ ): Array<{ name: string; type: string; value: any }> => {
1029
+ const attributes: Array<{ name: string; type: string; value: any }> = [];
1030
+
1031
+ // Add ASN metadata
1032
+ attributes.push({
1033
+ name: 'asn_id',
1034
+ type: 'STRING',
1035
+ value: config.asnId || data.shipmentId || 'UNKNOWN',
1036
+ });
1037
+
1038
+ attributes.push({
1039
+ name: 'asn_received_date',
1040
+ type: 'STRING',
1041
+ value: new Date().toISOString(),
1042
+ });
1043
+
1044
+ // Add carrier tracking
1045
+ if (data.shipment?.tracking_number) {
1046
+ attributes.push({
1047
+ name: 'tracking_number',
1048
+ type: 'STRING',
1049
+ value: data.shipment.tracking_number,
1050
+ });
1051
+ }
1052
+
1053
+ // Add origin information
1054
+ if (data.shipment?.origin) {
1055
+ attributes.push({
1056
+ name: 'origin_location',
1057
+ type: 'STRING',
1058
+ value: JSON.stringify(data.shipment.origin),
1059
+ });
1060
+ }
1061
+
1062
+ // Add PO number if present
1063
+ if (data.purchase_order_number) {
1064
+ attributes.push({
1065
+ name: 'po_number',
1066
+ type: 'STRING',
1067
+ value: data.purchase_order_number,
1068
+ });
1069
+ }
1070
+
1071
+ // Cross-dock flag
1072
+ if (data.shipment?.is_crossdock) {
1073
+ attributes.push({
1074
+ name: 'is_crossdock',
1075
+ type: 'BOOLEAN',
1076
+ value: 'true',
1077
+ });
1078
+ }
1079
+
1080
+ return attributes;
1081
+ },
1082
+ };
1083
+ ```
1084
+
1085
+ ### 4. Sample ASN Test Data: `data/test-asn-sample.json`
1086
+
1087
+ ```json
1088
+ {
1089
+ "shipmentId": "ASN-20250117-001",
1090
+ "purchase_order_number": "PO-12345",
1091
+ "shipment": {
1092
+ "ship_date": "2025-01-17T10:00:00Z",
1093
+ "carrier": {
1094
+ "name": "FEDEX_GROUND",
1095
+ "scac": "FXFE"
1096
+ },
1097
+ "tracking_number": "123456789012",
1098
+ "origin": {
1099
+ "name": "Acme Distribution Center",
1100
+ "address": "123 Warehouse Dr",
1101
+ "city": "Memphis",
1102
+ "state": "TN",
1103
+ "zip": "38101"
1104
+ },
1105
+ "is_crossdock": false,
1106
+ "items": [
1107
+ {
1108
+ "sku": "ACME-WIDGET-100",
1109
+ "quantity": 50,
1110
+ "lot_number": "LOT20250115",
1111
+ "container_id": "PALLET-001"
1112
+ },
1113
+ {
1114
+ "sku": "ACME-GADGET-200",
1115
+ "quantity": 100,
1116
+ "container_id": "PALLET-001"
1117
+ },
1118
+ {
1119
+ "sku": "ACME-TOOL-300",
1120
+ "quantity": 25,
1121
+ "lot_number": "LOT20250110",
1122
+ "expiry_date": "2026-01-10T00:00:00Z",
1123
+ "container_id": "PALLET-002"
1124
+ }
1125
+ ],
1126
+ "containers": [
1127
+ {
1128
+ "container_id": "PALLET-001",
1129
+ "type": "PALLET",
1130
+ "weight": 250.5,
1131
+ "weight_unit": "LBS",
1132
+ "length": 48,
1133
+ "width": 40,
1134
+ "height": 60,
1135
+ "dimension_unit": "IN"
1136
+ },
1137
+ {
1138
+ "container_id": "PALLET-002",
1139
+ "type": "PALLET",
1140
+ "weight": 180.0,
1141
+ "weight_unit": "LBS",
1142
+ "length": 48,
1143
+ "width": 40,
1144
+ "height": 48,
1145
+ "dimension_unit": "IN"
1146
+ }
1147
+ ]
1148
+ },
1149
+ "destination": {
1150
+ "location_code": "DC-01-RECEIVING",
1151
+ "name": "Distribution Center 01",
1152
+ "address": "789 Commerce Pkwy",
1153
+ "city": "Atlanta",
1154
+ "state": "GA",
1155
+ "zip": "30301"
1156
+ }
1157
+ }
1158
+ ```
1159
+
1160
+ ### 5. Package Configuration: `package.json`
1161
+
1162
+ ```json
1163
+ {
1164
+ "name": "asn-processing-connector",
1165
+ "version": "1.0.0",
1166
+ "description": "ASN/Purchase Order processing connector for Acme 3PL integration",
1167
+ "versori": {
1168
+ "workflows": "./workflows/asn-receipt.ts"
1169
+ },
1170
+ "dependencies": {
1171
+ "@fluentcommerce/fc-connect-sdk": "^0.1.39",
1172
+ "@versori/run": "latest"
1173
+ },
1174
+ "devDependencies": {
1175
+ "@types/node": "^20.0.0",
1176
+ "typescript": "^5.0.0"
1177
+ },
1178
+ "scripts": {
1179
+ "deploy": "versori deploy",
1180
+ "logs": "versori logs",
1181
+ "test": "node -r ts-node/register workflows/asn-receipt.ts"
1182
+ }
1183
+ }
1184
+ ```
1185
+
1186
+ ---
1187
+
1188
+ ## Key Patterns Explained
1189
+
1190
+ ### Pattern 1: ASN Duplicate Prevention
1191
+
1192
+ **Check for Existing Receipt Before Creating:**
1193
+
1194
+ ```typescript
1195
+ // Query existing receipts using ASN ID as ref
1196
+ const duplicateCheckQuery = `
1197
+ query CheckExistingReceipt($ref: String!) {
1198
+ inventoryQuantities(ref: $ref, first: 1) {
1199
+ edges {
1200
+ node {
1201
+ id
1202
+ ref
1203
+ status
1204
+ }
1205
+ }
1206
+ }
1207
+ }`;
1208
+
1209
+ const duplicateCheck = await fluentClient.graphql({
1210
+ query: duplicateCheckQuery,
1211
+ variables: {
1212
+ ref: `ASN-${asnId}`, // Unique ref from ASN shipment ID
1213
+ },
1214
+ });
1215
+
1216
+ const existingReceipt = duplicateCheck.data?.inventoryQuantities?.edges?.[0]?.node;
1217
+
1218
+ if (existingReceipt) {
1219
+ // ASN already processed - return early
1220
+ return {
1221
+ status: 200,
1222
+ body: {
1223
+ success: false,
1224
+ message: 'ASN already processed (duplicate)',
1225
+ existingReceiptId: existingReceipt.id,
1226
+ },
1227
+ };
1228
+ }
1229
+ ```
1230
+
1231
+ **Why this matters**: 3PLs may send duplicate ASN notifications due to:
1232
+ - Network retries
1233
+ - System glitches
1234
+ - Manual re-sends
1235
+ - Webhook replay
1236
+
1237
+ **Without duplicate prevention**: Creates duplicate expected inventory, causing:
1238
+ - Inflated ATP calculations
1239
+ - Incorrect stock levels
1240
+ - Failed physical receipts (already expected)
1241
+
1242
+ **Best practices**:
1243
+ - Always use ASN shipment ID as part of receipt ref
1244
+ - Query before creating
1245
+ - Log duplicate attempts for monitoring
1246
+
1247
+ ### Pattern 2: Expected Date Calculation
1248
+
1249
+ **Calculate Arrival Date Based on Carrier Transit Time:**
1250
+
1251
+ ```typescript
1252
+ 'custom.calculateExpectedDate': (value, data, config, helpers) => {
1253
+ const shipDate = data.shipment?.ship_date || data.shipDate;
1254
+ const carrier = data.shipment?.carrier?.name || data.carrier;
1255
+
1256
+ // Transit time lookup by carrier
1257
+ const transitTimes = {
1258
+ 'FEDEX_GROUND': 3,
1259
+ 'FEDEX_EXPRESS': 1,
1260
+ 'UPS_GROUND': 3,
1261
+ 'UPS_NEXT_DAY': 1,
1262
+ 'USPS': 5,
1263
+ 'DHL': 2,
1264
+ 'FREIGHT': 7,
1265
+ 'DEFAULT': 3,
1266
+ };
1267
+
1268
+ const carrierKey = String(carrier).toUpperCase().replace(/\s+/g, '_');
1269
+ const transitDays = transitTimes[carrierKey] || transitTimes.DEFAULT;
1270
+
1271
+ // Add transit days to ship date
1272
+ const shipDateObj = shipDate ? new Date(shipDate) : new Date();
1273
+ shipDateObj.setDate(shipDateObj.getDate() + transitDays);
1274
+
1275
+ return shipDateObj.toISOString();
1276
+ };
1277
+ ```
1278
+
1279
+ **Why this matters**: Expected date drives:
1280
+ - ATP calculations (Available To Promise)
1281
+ - Allocation timing
1282
+ - Customer promise dates
1283
+ - Warehouse receiving schedules
1284
+
1285
+ **Improvements for production:**
1286
+
1287
+ ```typescript
1288
+ // Add business days logic (skip weekends)
1289
+ function addBusinessDays(date: Date, days: number): Date {
1290
+ let result = new Date(date);
1291
+ let addedDays = 0;
1292
+
1293
+ while (addedDays < days) {
1294
+ result.setDate(result.getDate() + 1);
1295
+ // Skip weekends (0 = Sunday, 6 = Saturday)
1296
+ if (result.getDay() !== 0 && result.getDay() !== 6) {
1297
+ addedDays++;
1298
+ }
1299
+ }
1300
+
1301
+ return result;
1302
+ }
1303
+
1304
+ // Add holiday logic
1305
+ const holidays = ['2025-01-01', '2025-07-04', '2025-12-25'];
1306
+
1307
+ function isHoliday(date: Date): boolean {
1308
+ const dateStr = date.toISOString().substring(0, 10);
1309
+ return holidays.includes(dateStr);
1310
+ }
1311
+ ```
1312
+
1313
+ ### Pattern 3: SKU Validation (Async Resolver)
1314
+
1315
+ **Validate SKU Exists in Fluent Before Creating Receipt:**
1316
+
1317
+ ```typescript
1318
+ 'custom.validateAndNormalizeSku': async (value, data, config, helpers) => {
1319
+ const sku = String(value).trim().toUpperCase();
1320
+
1321
+ // Query Fluent to check if SKU exists
1322
+ const query = `
1323
+ query ValidateSku($skuRef: [String!]) {
1324
+ products(ref: $skuRef, first: 1) {
1325
+ edges {
1326
+ node {
1327
+ id
1328
+ ref
1329
+ }
1330
+ }
1331
+ }
1332
+ }
1333
+ `;
1334
+
1335
+ const result = await helpers.fluentClient.graphql({
1336
+ query,
1337
+ variables: { skuRef: [sku] },
1338
+ });
1339
+
1340
+ const product = result.data?.products?.edges?.[0]?.node;
1341
+
1342
+ if (!product) {
1343
+ // SKU not found - options:
1344
+ // 1. Throw error (halt processing)
1345
+ throw new Error(`SKU not found: ${sku}`);
1346
+
1347
+ // 2. Log warning and continue (let Fluent handle validation)
1348
+ helpers.logger?.warn('SKU not found', { sku });
1349
+ return sku;
1350
+ }
1351
+
1352
+ return sku;
1353
+ };
1354
+ ```
1355
+
1356
+ **When to use SKU validation**:
1357
+ - ✅ **3PL sends ASN before products are created** (prevents invalid receipts)
1358
+ - ✅ **SKU format differs between systems** (can normalize)
1359
+ - ✅ **Need early warning** (alert on unknown SKUs)
1360
+
1361
+ **When to skip**:
1362
+ - ❌ **High volume** (100+ SKUs per ASN = slow)
1363
+ - ❌ **Products always exist** (pre-synced)
1364
+ - ❌ **Performance critical** (let Fluent validation handle it)
1365
+
1366
+ **Performance optimization**:
1367
+
1368
+ ```typescript
1369
+ // Cache SKU validation results in resolver context
1370
+ const skuCache = new Map<string, boolean>();
1371
+
1372
+ if (skuCache.has(sku)) {
1373
+ return sku; // Already validated
1374
+ }
1375
+
1376
+ const isValid = await validateSku(sku);
1377
+ skuCache.set(sku, isValid);
1378
+ ```
1379
+
1380
+ ### Pattern 4: Container/Pallet Tracking
1381
+
1382
+ **Track Containers for Efficient Receiving:**
1383
+
1384
+ ```json
1385
+ {
1386
+ "containers": {
1387
+ "source": "shipment.containers",
1388
+ "isArray": true,
1389
+ "fields": {
1390
+ "containerRef": {
1391
+ "source": "$.container_id"
1392
+ },
1393
+ "containerType": {
1394
+ "source": "$.type",
1395
+ "comment": "PALLET, CARTON, TOTE"
1396
+ },
1397
+ "weight": { "source": "$.weight" },
1398
+ "dimensions": {
1399
+ "resolver": "custom.formatDimensions"
1400
+ }
1401
+ }
1402
+ }
1403
+ }
1404
+ ```
1405
+
1406
+ **Why container tracking matters**:
1407
+ - **Warehouse efficiency**: Receive entire pallets at once
1408
+ - **Put-away optimization**: Route containers to correct zones
1409
+ - **Cross-docking**: Direct routing without storage
1410
+ - **Audit trail**: Track physical container movement
1411
+
1412
+ **Real-world example**:
1413
+
1414
+ ```
1415
+ ASN contains:
1416
+ - PALLET-001: 50x WIDGET + 100x GADGET
1417
+ - PALLET-002: 25x TOOL (refrigerated)
1418
+
1419
+ Warehouse receives PALLET-001 first:
1420
+ → Scan PALLET-001 barcode
1421
+ → System shows all 2 SKUs on pallet
1422
+ → Receive all at once (no item-by-item scan)
1423
+ → PALLET-002 still expected
1424
+ ```
1425
+
1426
+ ### Pattern 5: Cross-Dock Scenario Handling
1427
+
1428
+ **Handle Direct Fulfillment Without Storage:**
1429
+
1430
+ ```typescript
1431
+ // In ASN data
1432
+ {
1433
+ "shipment": {
1434
+ "is_crossdock": true, // Flag for cross-dock shipment
1435
+ "destination_order_ref": "ORDER-12345"
1436
+ }
1437
+ }
1438
+
1439
+ // Custom resolver for cross-dock logic
1440
+ 'custom.buildReceiptAttributes': (value, data, config, helpers) => {
1441
+ const attributes = [];
1442
+
1443
+ if (data.shipment?.is_crossdock) {
1444
+ attributes.push({
1445
+ name: 'is_crossdock',
1446
+ type: 'BOOLEAN',
1447
+ value: 'true',
1448
+ });
1449
+
1450
+ // Link to destination order
1451
+ if (data.shipment.destination_order_ref) {
1452
+ attributes.push({
1453
+ name: 'crossdock_order_ref',
1454
+ type: 'STRING',
1455
+ value: data.shipment.destination_order_ref,
1456
+ });
1457
+ }
1458
+
1459
+ // Mark for expedited processing
1460
+ attributes.push({
1461
+ name: 'receiving_priority',
1462
+ type: 'STRING',
1463
+ value: 'HIGH',
1464
+ });
1465
+ }
1466
+
1467
+ return attributes;
1468
+ };
1469
+ ```
1470
+
1471
+ **Cross-dock workflow**:
1472
+ 1. **ASN received** with `is_crossdock: true`
1473
+ 2. **Expected receipt created** with crossdock flag
1474
+ 3. **Physical receipt** → immediately allocated to order
1475
+ 4. **Skip put-away** → direct to packing station
1476
+ 5. **Ship within hours** (not days)
1477
+
1478
+ **Benefits**:
1479
+ - Faster order fulfillment (same-day ship)
1480
+ - Reduced warehouse touches
1481
+ - Lower storage costs
1482
+ - Improved customer satisfaction
1483
+
1484
+ ### Pattern 6: XML vs JSON Payload Handling
1485
+
1486
+ **Support Multiple Payload Formats:**
1487
+
1488
+ ```typescript
1489
+ // Detect content type from header
1490
+ const contentType = ctx.activation?.headers?.['content-type'] || 'application/json';
1491
+
1492
+ let asnData: any;
1493
+
1494
+ if (contentType.includes('xml') || contentType.includes('text')) {
1495
+ // Parse XML (EDI 856 format)
1496
+ const xmlParser = new XMLParserService();
1497
+ asnData = await xmlParser.parse(rawPayload);
1498
+ } else {
1499
+ // Assume JSON
1500
+ asnData = rawPayload;
1501
+ }
1502
+ ```
1503
+
1504
+ **Why support both**:
1505
+ - **Enterprise 3PLs**: Often use EDI 856 XML format
1506
+ - **Modern 3PLs**: Use JSON REST APIs
1507
+ - **Flexibility**: Handle both without separate connectors
1508
+
1509
+ **EDI 856 XML example**:
1510
+
1511
+ ```xml
1512
+ <?xml version="1.0"?>
1513
+ <ShipNotice shipment_id="ASN-001">
1514
+ <Shipment>
1515
+ <ShipDate>2025-01-17</ShipDate>
1516
+ <Carrier name="FEDEX_GROUND"/>
1517
+ <Items>
1518
+ <Item>
1519
+ <SKU>WIDGET-100</SKU>
1520
+ <Quantity>50</Quantity>
1521
+ </Item>
1522
+ </Items>
1523
+ </Shipment>
1524
+ </ShipNotice>
1525
+ ```
1526
+
1527
+ **JSON equivalent**:
1528
+
1529
+ ```json
1530
+ {
1531
+ "shipmentId": "ASN-001",
1532
+ "shipment": {
1533
+ "ship_date": "2025-01-17",
1534
+ "carrier": { "name": "FEDEX_GROUND" },
1535
+ "items": [
1536
+ { "sku": "WIDGET-100", "quantity": 50 }
1537
+ ]
1538
+ }
1539
+ }
1540
+ ```
1541
+
1542
+ ---
1543
+
1544
+ ## Testing the Workflow
1545
+
1546
+ ### 1. Create Test ASN File
1547
+
1548
+ Already provided in `data/test-asn-sample.json` above.
1549
+
1550
+ ### 2. Deploy to Versori Platform
1551
+
1552
+ ```bash
1553
+ cd asn-processing-connector
1554
+ npm install
1555
+
1556
+ # Deploy to Versori
1557
+ versori deploy
1558
+
1559
+ # Or if using Versori CLI v2+
1560
+ npx @versori/cli deploy
1561
+ ```
1562
+
1563
+ ### 3. Configure Versori Connections
1564
+
1565
+ In the Versori console:
1566
+
1567
+ 1. Add **Fluent Commerce** connection:
1568
+ - Connection name: `fluent_commerce`
1569
+ - Base URL: `https://api.fluentcommerce.com/graphql`
1570
+ - Auth: OAuth2 (Client Credentials or Password Grant)
1571
+ - Client ID, Client Secret, Username, Password
1572
+
1573
+ 2. Configure activation variables (optional):
1574
+ - `OUTPUT_DIR`: Where to save audit files
1575
+ - `RETAILER_ID`: Fluent retailer ID
1576
+ - `DEFAULT_RECEIVING_LOCATION`: Default location code
1577
+
1578
+ ### 4. Test via Webhook
1579
+
1580
+ ```bash
1581
+ # Get webhook URL from Versori console (usually https://{workspace}.versori.io/{workflow})
1582
+ # Example: https://acme.versori.io/processAsnWebhook
1583
+
1584
+ # Send test ASN via HTTP POST
1585
+ curl -X POST https://acme.versori.io/processAsnWebhook \
1586
+ -H "Content-Type: application/json" \
1587
+ -d @data/test-asn-sample.json
1588
+ ```
1589
+
1590
+ ### 5. Use Manual Test Endpoint
1591
+
1592
+ ```bash
1593
+ # Trigger manual test (loads test-asn-sample.json from filesystem)
1594
+ curl https://acme.versori.io/manualAsnTest
1595
+ ```
1596
+
1597
+ ### 6. Check Health Endpoint
1598
+
1599
+ ```bash
1600
+ # Verify service is running
1601
+ curl https://acme.versori.io/healthCheck
1602
+ ```
1603
+
1604
+ ### 7. Verify in Fluent Commerce
1605
+
1606
+ 1. Log into Fluent Console
1607
+ 2. Navigate to Inventory → Inventory Quantities
1608
+ 3. Filter by status: `EXPECTED`
1609
+ 4. Find receipt with ref `ASN-20250117-001-*`
1610
+ 5. Verify:
1611
+ - Quantity matches ASN
1612
+ - Expected date calculated correctly
1613
+ - Attributes contain ASN metadata
1614
+
1615
+ ### 8. Monitor Logs
1616
+
1617
+ ```bash
1618
+ # View real-time logs in Versori console
1619
+ # Or use Versori CLI
1620
+ versori logs --workflow=processAsnWebhook --tail
1621
+
1622
+ # Check for errors
1623
+ versori logs --workflow=processAsnWebhook --level=error
1624
+ ```
1625
+
1626
+ ---
1627
+
1628
+ ## Common Issues and Solutions
1629
+
1630
+ ### Issue 1: "Duplicate entity" Error from Fluent
1631
+
1632
+ **Symptoms:**
1633
+ - GraphQL error: "Entity with ref already exists"
1634
+ - Second ASN attempt fails
1635
+
1636
+ **Root Cause:**
1637
+ - Receipt ref not unique
1638
+ - Missing timestamp in ref generation
1639
+
1640
+ **Solution:**
1641
+
1642
+ ```typescript
1643
+ // Ensure unique ref with timestamp
1644
+ 'custom.generateReceiptRef': (value, data, config, helpers) => {
1645
+ const asnId = data.shipmentId || 'UNKNOWN';
1646
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
1647
+ const randomSuffix = Math.random().toString(36).substring(7);
1648
+
1649
+ // Format: ASN-{asnId}-{timestamp}-{random}
1650
+ return `ASN-${asnId}-${timestamp}-${randomSuffix}`;
1651
+ };
1652
+ ```
1653
+
1654
+ ### Issue 2: Expected Date in the Past
1655
+
1656
+ **Symptoms:**
1657
+ - Expected date shows yesterday or earlier
1658
+ - ATP calculation incorrect
1659
+
1660
+ **Root Cause:**
1661
+ - Ship date from ASN is in the past
1662
+ - Transit time calculation starts from old date
1663
+
1664
+ **Solution:**
1665
+
1666
+ ```typescript
1667
+ 'custom.calculateExpectedDate': (value, data, config, helpers) => {
1668
+ const shipDate = data.shipment?.ship_date;
1669
+ const carrier = data.shipment?.carrier?.name;
1670
+
1671
+ // Get transit days
1672
+ const transitDays = getTransitDays(carrier);
1673
+
1674
+ // Use ship date or TODAY (whichever is later)
1675
+ const baseDate = shipDate ? new Date(shipDate) : new Date();
1676
+ const today = new Date();
1677
+
1678
+ // If ship date is in the past, use today instead
1679
+ if (baseDate < today) {
1680
+ baseDate.setTime(today.getTime());
1681
+ }
1682
+
1683
+ // Add transit days
1684
+ baseDate.setDate(baseDate.getDate() + transitDays);
1685
+
1686
+ return baseDate.toISOString();
1687
+ };
1688
+ ```
1689
+
1690
+ ### Issue 3: SKU Validation Slows Processing
1691
+
1692
+ **Symptoms:**
1693
+ - Webhook timeouts (>30 seconds)
1694
+ - ASN with 100+ SKUs fails
1695
+
1696
+ **Root Cause:**
1697
+ - Sequential SKU validation queries
1698
+ - No caching
1699
+
1700
+ **Solution:**
1701
+
1702
+ ```typescript
1703
+ // Batch validate all SKUs in single query
1704
+ 'custom.validateAllSkus': async (value, data, config, helpers) => {
1705
+ const allSkus = data.shipment?.items?.map((item: any) => item.sku) || [];
1706
+
1707
+ // Single query for all SKUs
1708
+ const query = `
1709
+ query ValidateSkus($skuRefs: [String!]) {
1710
+ products(ref: $skuRefs, first: 200) {
1711
+ edges {
1712
+ node {
1713
+ id
1714
+ ref
1715
+ }
1716
+ }
1717
+ }
1718
+ }
1719
+ `;
1720
+
1721
+ const result = await helpers.fluentClient.graphql({
1722
+ query,
1723
+ variables: { skuRefs: allSkus },
1724
+ });
1725
+
1726
+ const validSkus = new Set(
1727
+ result.data?.products?.edges?.map((e: any) => e.node.ref) || []
1728
+ );
1729
+
1730
+ // Check for missing SKUs
1731
+ const missingSkus = allSkus.filter(sku => !validSkus.has(sku));
1732
+
1733
+ if (missingSkus.length > 0) {
1734
+ helpers.logger?.warn('Invalid SKUs found', { missingSkus });
1735
+ // Option: throw error or continue
1736
+ }
1737
+
1738
+ return validSkus;
1739
+ };
1740
+ ```
1741
+
1742
+ ### Issue 4: Cross-Dock Flag Not Honored
1743
+
1744
+ **Symptoms:**
1745
+ - Cross-dock inventory goes to storage
1746
+ - Fulfillment delayed
1747
+
1748
+ **Root Cause:**
1749
+ - Warehouse receiving app doesn't check attributes
1750
+ - Missing workflow trigger
1751
+
1752
+ **Solution:**
1753
+
1754
+ **Option 1: Create Fluent Event**
1755
+
1756
+ ```typescript
1757
+ // After creating receipt, send event if cross-dock
1758
+ if (data.shipment?.is_crossdock) {
1759
+ await fluentClient.sendEvent({
1760
+ name: 'InventoryReceiptCrossDock',
1761
+ entityType: 'INVENTORY',
1762
+ entityRef: createdReceipt.ref,
1763
+ data: {
1764
+ destinationOrderRef: data.shipment.destination_order_ref,
1765
+ priority: 'HIGH',
1766
+ },
1767
+ });
1768
+ }
1769
+ ```
1770
+
1771
+ **Option 2: Use Different Location**
1772
+
1773
+ ```typescript
1774
+ // Route cross-dock receipts to special location
1775
+ 'custom.normalizeLocationRef': (value, data, config, helpers) => {
1776
+ if (data.shipment?.is_crossdock) {
1777
+ return 'DC-CROSSDOCK-RECEIVING'; // Special receiving zone
1778
+ }
1779
+ return value || config.defaultLocation;
1780
+ };
1781
+ ```
1782
+
1783
+ ### Issue 5: Container Data Not Appearing in Fluent
1784
+
1785
+ **Symptoms:**
1786
+ - Container/pallet info missing in Fluent
1787
+ - Can't track containers
1788
+
1789
+ **Root Cause:**
1790
+ - Fluent schema doesn't support container arrays directly
1791
+ - Need to use attributes
1792
+
1793
+ **Solution:**
1794
+
1795
+ ```typescript
1796
+ // Store containers in attributes instead of separate field
1797
+ 'custom.buildReceiptAttributes': (value, data, config, helpers) => {
1798
+ const attributes = [];
1799
+
1800
+ // Serialize containers to JSON attribute
1801
+ if (data.shipment?.containers) {
1802
+ attributes.push({
1803
+ name: 'containers',
1804
+ type: 'STRING',
1805
+ value: JSON.stringify(data.shipment.containers),
1806
+ });
1807
+
1808
+ // Also add container count for quick reference
1809
+ attributes.push({
1810
+ name: 'container_count',
1811
+ type: 'INTEGER',
1812
+ value: String(data.shipment.containers.length),
1813
+ });
1814
+ }
1815
+
1816
+ return attributes;
1817
+ };
1818
+ ```
1819
+
1820
+ ---
1821
+
1822
+ ## Related Guides
1823
+
1824
+ - **[Connector Platform Scheduled: Cycle Count Reconciliation](../workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md)** - Reconciliation patterns
1825
+ - **[Universal Mapping Guide](../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)** - Field mapping patterns
1826
+ - **[GraphQL Mutation Mapper](../../../02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md)** - Mutation generation
1827
+ - **[XML Parsing Guide](../../../02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md)** - EDI 856 XML handling
1828
+
1829
+ ---
1830
+
1831
+ ## Production Checklist
1832
+
1833
+ Before deploying to production:
1834
+ - [ ] Test with real 3PL ASN payloads (XML and JSON)
1835
+ - [ ] Configure carrier transit times based on real data
1836
+ - [ ] Set up duplicate ASN monitoring/alerting
1837
+ - [ ] Verify SKU validation strategy (batch vs individual)
1838
+ - [ ] Test cross-dock scenario if applicable
1839
+ - [ ] Configure webhook retry policy in 3PL system
1840
+ - [ ] Set up error notifications (email/Slack)
1841
+ - [ ] Document ASN format variations by 3PL
1842
+ - [ ] Test large ASNs (100+ SKUs, 10+ containers)
1843
+ - [ ] Verify expected date business days logic
1844
+
1845
+ ---
1846
+
1847
+ ## Performance Considerations
1848
+
1849
+ **Large ASNs (100+ SKUs)**:
1850
+
1851
+ ```typescript
1852
+ // Process items in chunks to avoid timeouts
1853
+ const CHUNK_SIZE = 50;
1854
+
1855
+ for (let i = 0; i < items.length; i += CHUNK_SIZE) {
1856
+ const chunk = items.slice(i, i + CHUNK_SIZE);
1857
+ await processItemChunk(chunk);
1858
+ }
1859
+ ```
1860
+
1861
+ **High Volume (1000+ ASNs/day)**:
1862
+
1863
+ ```typescript
1864
+ // Use background processing with queue
1865
+ export const processAsnWebhookQueued = webhook('queue-asn', {
1866
+ response: {
1867
+ mode: 'sync',
1868
+ onSuccess: (ctx) => new Response(JSON.stringify(ctx.data), {
1869
+ status: 202, // Accepted
1870
+ headers: { 'Content-Type': 'application/json' }
1871
+ })
1872
+ }
1873
+ }, async (ctx) => {
1874
+ const { activation } = ctx;
1875
+
1876
+ // Immediately acknowledge receipt
1877
+ const asnId = activation.body.shipmentId;
1878
+
1879
+ // Queue for async processing (implement queueAsnForProcessing function)
1880
+ await queueAsnForProcessing(asnId, activation.body);
1881
+
1882
+ return {
1883
+ success: true,
1884
+ message: 'ASN queued for processing',
1885
+ asnId,
1886
+ };
1887
+ });
1888
+ ```
1889
+
1890
+ ---
1891
+
1892
+ ## Next Steps
1893
+
1894
+ 1. **Add Receipt Confirmation**: Send confirmation back to 3PL when receipt created
1895
+ 2. **Implement Discrepancy Handling**: Compare expected vs actual receipt quantities
1896
+ 3. **Add Serial Number Tracking**: Full serial number lifecycle
1897
+ 4. **Integrate with WMS**: Push ASN to warehouse management system
1898
+ 5. **Build Dashboard**: Real-time ASN status monitoring
1899
+ 6. **Add Carrier Tracking Integration**: Real-time shipment tracking via carrier APIs
1900
+
1901
+ ---
1902
+
1903
+ **Need Help?**
1904
+ - Review SDK documentation: `/docs/readme.md`
1905
+ - Check example connectors: `/connectors/Sample versori connectors/`
1906
+ - Connector platform documentation: Check your platform's docs