@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.
- package/CHANGELOG.md +30 -2
- package/README.md +39 -0
- package/dist/cjs/auth/index.d.ts +3 -0
- package/dist/cjs/auth/index.js +13 -0
- package/dist/cjs/auth/profile-loader.d.ts +18 -0
- package/dist/cjs/auth/profile-loader.js +208 -0
- package/dist/cjs/client-factory.d.ts +4 -0
- package/dist/cjs/client-factory.js +10 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/index.d.ts +3 -1
- package/dist/cjs/index.js +8 -2
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/auth/index.d.ts +3 -0
- package/dist/esm/auth/index.js +2 -0
- package/dist/esm/auth/profile-loader.d.ts +18 -0
- package/dist/esm/auth/profile-loader.js +169 -0
- package/dist/esm/client-factory.d.ts +4 -0
- package/dist/esm/client-factory.js +9 -0
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/utils/pagination-helpers.js +38 -2
- package/dist/esm/versori/fluent-versori-client.js +11 -5
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/dist/types/auth/index.d.ts +3 -0
- package/dist/types/auth/profile-loader.d.ts +18 -0
- package/dist/types/client-factory.d.ts +4 -0
- package/dist/types/index.d.ts +3 -1
- package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
- package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
- package/docs/00-START-HERE/cli-documentation-index.md +202 -202
- package/docs/00-START-HERE/cli-quick-reference.md +252 -252
- package/docs/00-START-HERE/decision-tree.md +552 -552
- package/docs/00-START-HERE/getting-started.md +1070 -1070
- package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
- package/docs/00-START-HERE/readme.md +237 -237
- package/docs/00-START-HERE/retailerid-configuration.md +404 -404
- package/docs/00-START-HERE/sdk-philosophy.md +794 -794
- package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
- package/docs/01-TEMPLATES/faq.md +686 -686
- package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
- package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
- package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
- package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
- package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
- package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
- package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
- package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
- package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
- package/docs/01-TEMPLATES/readme.md +957 -957
- package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
- package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
- package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
- package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
- package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
- package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
- package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
- package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
- package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
- package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
- package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
- package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
- package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
- package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
- package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
- package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
- package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
- package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
- package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -482
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
- package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
- package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
- package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
- package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
- package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
- package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
- package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
- package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
- package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
- package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
- package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
- package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
- package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
- package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
- package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
- package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
- package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
- package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
- package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
- package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
- package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
- package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
- package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
- package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
- package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
- package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
- package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
- package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
- package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
- package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
- package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
- package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
- package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
- package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
- package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
- package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
- package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
- package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
- package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
- package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
- package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
- package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
- package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
- package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
- package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
- package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
- package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
- package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
- package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
- package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
- package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
- package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
- package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
- package/docs/02-CORE-GUIDES/readme.md +194 -194
- package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
- package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
- package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
- package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
- package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
- package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
- package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
- package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
- package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
- package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
- package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
- package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
- package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
- package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
- package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
- package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
- package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
- package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
- package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
- package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
- package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
- package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
- package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
- package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
- package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
- package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
- package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/readme.md +159 -159
- package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
- package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
- package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
- package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
- package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
- package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
- package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
- package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
- package/docs/04-REFERENCE/architecture/readme.md +279 -279
- package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
- package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
- package/docs/04-REFERENCE/platforms/readme.md +135 -135
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
- package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
- package/docs/04-REFERENCE/readme.md +148 -148
- package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
- package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
- package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
- package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
- package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
- package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
- package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
- package/docs/04-REFERENCE/schema/readme.md +141 -141
- package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
- package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
- package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
- package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
- package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
- package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
- package/docs/04-REFERENCE/testing/readme.md +86 -86
- package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
- package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
- package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
- package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
- package/docs/template-loading-matrix.md +242 -242
- 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
|