@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/clients/fluent-client.js +13 -6
- 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/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 -520
- 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
- package/docs/02-CORE-GUIDES/api-reference/cli-profile-integration.md +0 -377
package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md
CHANGED
|
@@ -1,2162 +1,2162 @@
|
|
|
1
|
-
# Versori Inter-Location Transfer Processing
|
|
2
|
-
|
|
3
|
-
**FC Connect SDK Use Case Guide**
|
|
4
|
-
|
|
5
|
-
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
6
|
-
> **Version**: Use latest - `npm install @fluentcommerce/fc-connect-sdk@latest`
|
|
7
|
-
|
|
8
|
-
**Context**: Webhook-triggered or scheduled workflow for processing inventory transfers between warehouses/stores with in-transit tracking
|
|
9
|
-
|
|
10
|
-
**Complexity**: Medium
|
|
11
|
-
|
|
12
|
-
**Runtime**: Versori Platform (Webhook/Scheduled)
|
|
13
|
-
|
|
14
|
-
**Estimated Lines**: ~1,200 lines (modular, production-ready)
|
|
15
|
-
|
|
16
|
-
## What You'll Build
|
|
17
|
-
|
|
18
|
-
- Versori webhook/scheduled workflow for transfer requests
|
|
19
|
-
- CSV and JSON input format support
|
|
20
|
-
- Parse transfer data (source, destination, items, quantities)
|
|
21
|
-
- Create paired dispatch/receipt mutations (GraphQL)
|
|
22
|
-
- In-transit inventory tracking with VersoriKV
|
|
23
|
-
- Variance handling (damaged items, shortages)
|
|
24
|
-
- Rollback logic on failure
|
|
25
|
-
- Email notifications on completion
|
|
26
|
-
- Approval workflow for variances > 5%
|
|
27
|
-
|
|
28
|
-
## SDK Methods Used
|
|
29
|
-
|
|
30
|
-
- `createClient(ctx)` - Auto-detects Versori context
|
|
31
|
-
- `GraphQLMutationMapper(config, logger, { fluentClient, customResolvers })` - Map transfers to GraphQL mutations
|
|
32
|
-
- `mapper.mapWithNodes(sourceData)` - Transform transfer data (resolvers from constructor)
|
|
33
|
-
- `client.graphql(payload)` - Execute mutations
|
|
34
|
-
- `VersoriKVAdapter(openKv())` - State management
|
|
35
|
-
- `StateService(logger)` - Distributed locking
|
|
36
|
-
- `UniversalMapper(mappingConfig)` - CSV field mapping
|
|
37
|
-
- `CSVParserService()` - Parse CSV transfer files
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## Versori Workflows Structure
|
|
42
|
-
|
|
43
|
-
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
44
|
-
|
|
45
|
-
**Trigger Types:**
|
|
46
|
-
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
47
|
-
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
48
|
-
- **`http()`** → External API calls (chained from webhook/schedule)
|
|
49
|
-
- **`fn()`** → Internal processing (chained from webhook/schedule)
|
|
50
|
-
|
|
51
|
-
### Recommended Project Structure
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
inter-location-transfers/
|
|
55
|
-
├── index.ts # Entry point - registers all workflows
|
|
56
|
-
└── src/
|
|
57
|
-
├── workflows/
|
|
58
|
-
│ ├── webhook/
|
|
59
|
-
│ │ ├── initiate-transfer.ts # Webhook: Process transfer requests
|
|
60
|
-
│ │ ├── receive-transfer.ts # Webhook: Receipt confirmation
|
|
61
|
-
│ │ └── export-transfer-report.ts # Webhook: Export reports
|
|
62
|
-
│ │
|
|
63
|
-
│ └── scheduled/
|
|
64
|
-
│ └── reconcile-transfers.ts # Scheduled: Daily reconciliation
|
|
65
|
-
│
|
|
66
|
-
├── services/
|
|
67
|
-
│ └── transfer.service.ts # Shared orchestration logic (reusable)
|
|
68
|
-
│
|
|
69
|
-
└── config/
|
|
70
|
-
└── transfer-mapping.json # Mapping configuration
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
**Benefits:**
|
|
74
|
-
- ✅ Clear trigger separation (`webhook/` vs `scheduled/`)
|
|
75
|
-
- ✅ Descriptive file names (easy to browse and understand)
|
|
76
|
-
- ✅ Scalable (add new workflows without cluttering)
|
|
77
|
-
- ✅ Reusable code in `services/` (DRY principle)
|
|
78
|
-
- ✅ Easy to modify individual workflows without affecting others
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
|
|
82
|
-
## Use Case Profile
|
|
83
|
-
|
|
84
|
-
- **Volume**: Medium (50-500 transfers/day)
|
|
85
|
-
- **Latency**: Near real-time (< 5 minutes)
|
|
86
|
-
- **Complexity**: Medium (paired mutations, state tracking, variance handling)
|
|
87
|
-
- **Pattern**: Webhook/scheduled → parse → dispatch mutation → receipt mutation → state tracking
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
## Complete Working Code
|
|
92
|
-
|
|
93
|
-
### 1. Entry Point: `index.ts`
|
|
94
|
-
|
|
95
|
-
```typescript
|
|
96
|
-
/**
|
|
97
|
-
* Entry Point - Registers all workflows with Versori platform
|
|
98
|
-
*
|
|
99
|
-
* ⚠️ CRITICAL PATTERN: MemoryInterpreter Setup
|
|
100
|
-
* This pattern ensures workflows execute in the correct Versori runtime context
|
|
101
|
-
* and prevents "unexpected export" errors during deployment.
|
|
102
|
-
*
|
|
103
|
-
* COMPONENTS:
|
|
104
|
-
* 1. Import ALL workflows (scheduled, webhook, etc.)
|
|
105
|
-
* 2. Create MemoryInterpreter with workflow array
|
|
106
|
-
* 3. NO direct exports - interpreter handles workflow registration
|
|
107
|
-
*
|
|
108
|
-
* File Structure:
|
|
109
|
-
* - src/workflows/scheduled/ → Time-based triggers (cron)
|
|
110
|
-
* - src/workflows/webhook/ → HTTP-based triggers (webhooks)
|
|
111
|
-
*/
|
|
112
|
-
|
|
113
|
-
import { MemoryInterpreter } from '@versori/run';
|
|
114
|
-
|
|
115
|
-
// Import webhook workflows
|
|
116
|
-
import { initiateTransfer } from './src/workflows/webhook/initiate-transfer';
|
|
117
|
-
import { receiveTransfer } from './src/workflows/webhook/receive-transfer';
|
|
118
|
-
import { exportTransferReport } from './src/workflows/webhook/export-transfer-report';
|
|
119
|
-
|
|
120
|
-
// Import scheduled workflows
|
|
121
|
-
import { reconcileTransfers } from './src/workflows/scheduled/reconcile-transfers';
|
|
122
|
-
|
|
123
|
-
// ✅ CORRECT: Register via MemoryInterpreter (Versori standard)
|
|
124
|
-
new MemoryInterpreter([
|
|
125
|
-
// Webhooks (HTTP-based triggers)
|
|
126
|
-
initiateTransfer,
|
|
127
|
-
receiveTransfer,
|
|
128
|
-
exportTransferReport,
|
|
129
|
-
|
|
130
|
-
// Scheduled (time-based triggers)
|
|
131
|
-
reconcileTransfers,
|
|
132
|
-
]);
|
|
133
|
-
|
|
134
|
-
// ❌ WRONG: Direct exports cause "unexpected export" errors
|
|
135
|
-
// export { initiateTransfer, receiveTransfer, reconcileTransfers };
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* What Gets Exposed:
|
|
139
|
-
* ✅ initiateTransfer → https://{workspace}.versori.run/initiate-transfer
|
|
140
|
-
* ✅ receiveTransfer → https://{workspace}.versori.run/receive-transfer
|
|
141
|
-
* ✅ exportTransferReport → https://{workspace}.versori.run/export-transfer-report
|
|
142
|
-
* ❌ reconcileTransfers → NOT exposed (runs automatically on cron)
|
|
143
|
-
*/
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
---
|
|
147
|
-
|
|
148
|
-
### 2. Service File: `src/services/transfer.service.ts`
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
/**
|
|
152
|
-
* Transfer Service - Shared orchestration logic
|
|
153
|
-
*
|
|
154
|
-
* This service provides reusable business logic for transfer processing:
|
|
155
|
-
* - Dispatch mutations (remove from source)
|
|
156
|
-
* - Receipt mutations (add to destination)
|
|
157
|
-
* - Variance calculation and approval
|
|
158
|
-
* - Rollback logic on failure
|
|
159
|
-
* - State management with VersoriKV
|
|
160
|
-
* - Distributed locking for concurrency
|
|
161
|
-
*
|
|
162
|
-
* All business logic is centralized here - workflows just delegate to service functions.
|
|
163
|
-
*/
|
|
164
|
-
|
|
165
|
-
import { Buffer } from 'node:buffer'; // Required for Versori/Deno
|
|
166
|
-
import {
|
|
167
|
-
createClient,
|
|
168
|
-
GraphQLMutationMapper,
|
|
169
|
-
UniversalMapper,
|
|
170
|
-
VersoriKVAdapter,
|
|
171
|
-
StateService,
|
|
172
|
-
CSVParserService,
|
|
173
|
-
S3DataSource,
|
|
174
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
175
|
-
|
|
176
|
-
import type {
|
|
177
|
-
FluentClient,
|
|
178
|
-
Logger,
|
|
179
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Transfer data interfaces
|
|
183
|
-
*/
|
|
184
|
-
export interface TransferRequest {
|
|
185
|
-
transferId: string;
|
|
186
|
-
sourceLocationRef: string;
|
|
187
|
-
destinationLocationRef: string;
|
|
188
|
-
requestedDate: string;
|
|
189
|
-
expectedDeliveryDate?: string;
|
|
190
|
-
items: TransferItem[];
|
|
191
|
-
metadata?: {
|
|
192
|
-
reason?: string;
|
|
193
|
-
priority?: string;
|
|
194
|
-
requestedBy?: string;
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export interface TransferItem {
|
|
199
|
-
skuRef: string;
|
|
200
|
-
requestedQty: number;
|
|
201
|
-
actualQty?: number; // For receipt confirmation
|
|
202
|
-
condition?: string;
|
|
203
|
-
notes?: string;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export interface TransferState {
|
|
207
|
-
transferId: string;
|
|
208
|
-
status: 'PENDING' | 'DISPATCHED' | 'IN_TRANSIT' | 'RECEIVED' | 'FAILED' | 'ROLLED_BACK';
|
|
209
|
-
dispatchMutationId?: string;
|
|
210
|
-
receiptMutationId?: string;
|
|
211
|
-
dispatchedAt?: string;
|
|
212
|
-
receivedAt?: string;
|
|
213
|
-
variance?: VarianceDetails;
|
|
214
|
-
originalRequest?: TransferRequest;
|
|
215
|
-
receiptData?: any;
|
|
216
|
-
createdAt: string;
|
|
217
|
-
updatedAt: string;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export interface VarianceDetails {
|
|
221
|
-
hasVariance: boolean;
|
|
222
|
-
variancePercentage: number;
|
|
223
|
-
requiresApproval: boolean;
|
|
224
|
-
items: {
|
|
225
|
-
skuRef: string;
|
|
226
|
-
expected: number;
|
|
227
|
-
actual: number;
|
|
228
|
-
difference: number;
|
|
229
|
-
reason?: string;
|
|
230
|
-
}[];
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export interface TransferResult {
|
|
234
|
-
success: boolean;
|
|
235
|
-
transferId?: string;
|
|
236
|
-
status?: string;
|
|
237
|
-
dispatchMutationId?: string;
|
|
238
|
-
receiptMutationId?: string;
|
|
239
|
-
variance?: VarianceDetails;
|
|
240
|
-
error?: string;
|
|
241
|
-
currentStatus?: string;
|
|
242
|
-
message?: string;
|
|
243
|
-
recommendation?: string;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Process transfer request (dispatch from source)
|
|
248
|
-
*
|
|
249
|
-
* This function orchestrates the complete transfer dispatch:
|
|
250
|
-
* 1. Client initialization with retailerId
|
|
251
|
-
* 2. Lock acquisition for concurrency control
|
|
252
|
-
* 3. Transfer state initialization
|
|
253
|
-
* 4. Dispatch mutation creation
|
|
254
|
-
* 5. State updates and cleanup
|
|
255
|
-
*
|
|
256
|
-
* @param ctx - Versori context object
|
|
257
|
-
* @param transferRequest - Transfer request data
|
|
258
|
-
*/
|
|
259
|
-
export async function processTransferRequest(
|
|
260
|
-
ctx: any,
|
|
261
|
-
transferRequest: TransferRequest
|
|
262
|
-
): Promise<TransferResult> {
|
|
263
|
-
const { log, activation, openKv } = ctx;
|
|
264
|
-
const startTime = Date.now();
|
|
265
|
-
|
|
266
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
267
|
-
log.info('🚀 TRANSFER REQUEST PROCESSING STARTED', {
|
|
268
|
-
transferId: transferRequest.transferId,
|
|
269
|
-
sourceLocation: transferRequest.sourceLocationRef,
|
|
270
|
-
destinationLocation: transferRequest.destinationLocationRef,
|
|
271
|
-
itemCount: transferRequest.items.length,
|
|
272
|
-
});
|
|
273
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
274
|
-
|
|
275
|
-
try {
|
|
276
|
-
// Initialize client with retailerId and validateConnection
|
|
277
|
-
log.info('🔧 Initializing Fluent Commerce client...');
|
|
278
|
-
const client = await createClient({ ...ctx, validateConnection: true });
|
|
279
|
-
const fluentRetailerId = activation.getVariable('fluentRetailerId');
|
|
280
|
-
|
|
281
|
-
if (!fluentRetailerId) {
|
|
282
|
-
log.error('❌ Missing required fluentRetailerId activation variable');
|
|
283
|
-
return {
|
|
284
|
-
success: false,
|
|
285
|
-
error: 'fluentRetailerId activation variable is required',
|
|
286
|
-
recommendation: 'Please configure fluentRetailerId in the Activation Variables section',
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
client.setRetailerId(fluentRetailerId);
|
|
291
|
-
log.info('✅ Client initialized', { retailerId: fluentRetailerId });
|
|
292
|
-
|
|
293
|
-
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
294
|
-
const stateService = new StateService(log);
|
|
295
|
-
|
|
296
|
-
// Check if transfer already processed
|
|
297
|
-
log.info('🔍 Checking transfer state...');
|
|
298
|
-
const existingState = await getTransferState(kvAdapter, transferRequest.transferId);
|
|
299
|
-
|
|
300
|
-
if (existingState && existingState.status !== 'FAILED') {
|
|
301
|
-
log.warn('⚠️ Transfer already processed', {
|
|
302
|
-
transferId: transferRequest.transferId,
|
|
303
|
-
status: existingState.status,
|
|
304
|
-
});
|
|
305
|
-
return {
|
|
306
|
-
success: false,
|
|
307
|
-
error: 'Transfer already processed',
|
|
308
|
-
transferId: transferRequest.transferId,
|
|
309
|
-
currentStatus: existingState.status,
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Acquire lock to prevent concurrent processing
|
|
314
|
-
log.info('🔒 Acquiring transfer lock...');
|
|
315
|
-
const lockName = `transfer-lock:${transferRequest.transferId}`;
|
|
316
|
-
const lockAcquired = await stateService.acquireLock(lockName, kvAdapter, 15);
|
|
317
|
-
|
|
318
|
-
if (!lockAcquired) {
|
|
319
|
-
log.warn('⚠️ Could not acquire lock - transfer already being processed');
|
|
320
|
-
return {
|
|
321
|
-
success: false,
|
|
322
|
-
error: 'Transfer is already being processed',
|
|
323
|
-
transferId: transferRequest.transferId,
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
try {
|
|
328
|
-
// Create initial transfer state
|
|
329
|
-
log.info('📝 Initializing transfer state...');
|
|
330
|
-
const transferState: TransferState = {
|
|
331
|
-
transferId: transferRequest.transferId,
|
|
332
|
-
status: 'PENDING',
|
|
333
|
-
originalRequest: transferRequest,
|
|
334
|
-
createdAt: new Date().toISOString(),
|
|
335
|
-
updatedAt: new Date().toISOString(),
|
|
336
|
-
};
|
|
337
|
-
await saveTransferState(kvAdapter, transferState);
|
|
338
|
-
log.info('✅ Transfer state initialized');
|
|
339
|
-
|
|
340
|
-
// Step 1: Create dispatch mutation (remove from source)
|
|
341
|
-
log.info('📦 Creating dispatch mutation...');
|
|
342
|
-
const dispatchStartTime = Date.now();
|
|
343
|
-
const dispatchResult = await createDispatchMutation(client, transferRequest, log);
|
|
344
|
-
|
|
345
|
-
if (!dispatchResult.success) {
|
|
346
|
-
throw new Error(`Dispatch failed: ${dispatchResult.error}`);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const dispatchDuration = Date.now() - dispatchStartTime;
|
|
350
|
-
log.info('✅ Dispatch mutation created', {
|
|
351
|
-
mutationId: dispatchResult.mutationId,
|
|
352
|
-
duration: `${dispatchDuration}ms`,
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
// Update state with dispatch info
|
|
356
|
-
transferState.status = 'DISPATCHED';
|
|
357
|
-
transferState.dispatchMutationId = dispatchResult.mutationId;
|
|
358
|
-
transferState.dispatchedAt = new Date().toISOString();
|
|
359
|
-
transferState.updatedAt = new Date().toISOString();
|
|
360
|
-
await saveTransferState(kvAdapter, transferState);
|
|
361
|
-
|
|
362
|
-
// Step 2: Mark as in-transit
|
|
363
|
-
transferState.status = 'IN_TRANSIT';
|
|
364
|
-
transferState.updatedAt = new Date().toISOString();
|
|
365
|
-
await saveTransferState(kvAdapter, transferState);
|
|
366
|
-
log.info('✅ Transfer marked as in-transit');
|
|
367
|
-
|
|
368
|
-
const totalDuration = Date.now() - startTime;
|
|
369
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
370
|
-
log.info('✅ TRANSFER REQUEST COMPLETED', {
|
|
371
|
-
transferId: transferRequest.transferId,
|
|
372
|
-
status: 'IN_TRANSIT',
|
|
373
|
-
duration: `${totalDuration}ms`,
|
|
374
|
-
});
|
|
375
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
376
|
-
|
|
377
|
-
// For webhook trigger, return success after dispatch
|
|
378
|
-
// Receipt will be confirmed via separate webhook
|
|
379
|
-
return {
|
|
380
|
-
success: true,
|
|
381
|
-
transferId: transferRequest.transferId,
|
|
382
|
-
status: 'IN_TRANSIT',
|
|
383
|
-
dispatchMutationId: dispatchResult.mutationId,
|
|
384
|
-
message: 'Transfer dispatched successfully, awaiting receipt confirmation',
|
|
385
|
-
};
|
|
386
|
-
} finally {
|
|
387
|
-
// Always release lock
|
|
388
|
-
log.info('🔓 Releasing transfer lock...');
|
|
389
|
-
await stateService.releaseLock(lockName, kvAdapter);
|
|
390
|
-
}
|
|
391
|
-
} catch (error: any) {
|
|
392
|
-
const duration = Date.now() - startTime;
|
|
393
|
-
log.error('═══════════════════════════════════════════════════════');
|
|
394
|
-
log.error('❌ TRANSFER REQUEST FAILED', {
|
|
395
|
-
transferId: transferRequest.transferId,
|
|
396
|
-
error: error instanceof Error ? error.message : String(error),
|
|
397
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
398
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
399
|
-
duration: `${duration}ms`,
|
|
400
|
-
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
401
|
-
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
402
|
-
: error.message?.includes('mapping') || error.message?.includes('field')
|
|
403
|
-
? 'Check mapping configuration and verify transfer request payload structure'
|
|
404
|
-
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
405
|
-
? 'Check network connectivity and Fluent Commerce API availability'
|
|
406
|
-
: error.message?.includes('lock') || error.message?.includes('concurrent')
|
|
407
|
-
? 'Concurrent transfer processing detected - retry after lock release'
|
|
408
|
-
: 'Review error details and check transfer request payload structure',
|
|
409
|
-
});
|
|
410
|
-
log.error('═══════════════════════════════════════════════════════');
|
|
411
|
-
|
|
412
|
-
return {
|
|
413
|
-
success: false,
|
|
414
|
-
error: error.message,
|
|
415
|
-
transferId: transferRequest.transferId,
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Process receipt confirmation (add to destination)
|
|
422
|
-
*/
|
|
423
|
-
export async function processReceiptConfirmation(
|
|
424
|
-
ctx: any,
|
|
425
|
-
receiptData: { transferId: string; receivedDate: string; items: TransferItem[] }
|
|
426
|
-
): Promise<TransferResult> {
|
|
427
|
-
const { log, activation, openKv } = ctx;
|
|
428
|
-
const startTime = Date.now();
|
|
429
|
-
|
|
430
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
431
|
-
log.info('📦 RECEIPT CONFIRMATION STARTED', {
|
|
432
|
-
transferId: receiptData.transferId,
|
|
433
|
-
itemCount: receiptData.items.length,
|
|
434
|
-
});
|
|
435
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
436
|
-
|
|
437
|
-
try {
|
|
438
|
-
const client = await createClient({ ...ctx, validateConnection: true });
|
|
439
|
-
const fluentRetailerId = activation.getVariable('fluentRetailerId');
|
|
440
|
-
|
|
441
|
-
if (fluentRetailerId) {
|
|
442
|
-
client.setRetailerId(fluentRetailerId);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
446
|
-
const stateService = new StateService(log);
|
|
447
|
-
|
|
448
|
-
// Get transfer state
|
|
449
|
-
log.info('🔍 Retrieving transfer state...');
|
|
450
|
-
const transferState = await getTransferState(kvAdapter, receiptData.transferId);
|
|
451
|
-
|
|
452
|
-
if (!transferState) {
|
|
453
|
-
throw new Error('Transfer not found');
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (transferState.status !== 'IN_TRANSIT') {
|
|
457
|
-
throw new Error(`Invalid transfer status: ${transferState.status}`);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Acquire lock
|
|
461
|
-
log.info('🔒 Acquiring transfer lock...');
|
|
462
|
-
const lockName = `transfer-lock:${receiptData.transferId}`;
|
|
463
|
-
const lockAcquired = await stateService.acquireLock(lockName, kvAdapter, 15);
|
|
464
|
-
|
|
465
|
-
if (!lockAcquired) {
|
|
466
|
-
throw new Error('Could not acquire lock');
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
try {
|
|
470
|
-
// Calculate variance
|
|
471
|
-
log.info('🔍 Calculating variance...');
|
|
472
|
-
const variance = calculateVariance(
|
|
473
|
-
transferState.originalRequest?.items || receiptData.items,
|
|
474
|
-
receiptData.items
|
|
475
|
-
);
|
|
476
|
-
transferState.variance = variance;
|
|
477
|
-
transferState.receiptData = receiptData;
|
|
478
|
-
|
|
479
|
-
log.info('📊 Variance calculated', {
|
|
480
|
-
hasVariance: variance.hasVariance,
|
|
481
|
-
variancePercentage: variance.variancePercentage.toFixed(2) + '%',
|
|
482
|
-
requiresApproval: variance.requiresApproval,
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
// Check if variance requires approval (> 5%)
|
|
486
|
-
if (variance.requiresApproval) {
|
|
487
|
-
transferState.status = 'PENDING';
|
|
488
|
-
transferState.updatedAt = new Date().toISOString();
|
|
489
|
-
await saveTransferState(kvAdapter, transferState);
|
|
490
|
-
|
|
491
|
-
log.warn('⚠️ Variance requires approval', {
|
|
492
|
-
variancePercentage: variance.variancePercentage.toFixed(2) + '%',
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
// Send email notification
|
|
496
|
-
await sendVarianceNotification(receiptData.transferId, variance, activation, log);
|
|
497
|
-
|
|
498
|
-
const duration = Date.now() - startTime;
|
|
499
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
500
|
-
log.info('⚠️ RECEIPT PENDING APPROVAL', {
|
|
501
|
-
transferId: receiptData.transferId,
|
|
502
|
-
duration: `${duration}ms`,
|
|
503
|
-
});
|
|
504
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
505
|
-
|
|
506
|
-
return {
|
|
507
|
-
success: true,
|
|
508
|
-
status: 'PENDING_APPROVAL',
|
|
509
|
-
transferId: receiptData.transferId,
|
|
510
|
-
variance,
|
|
511
|
-
message: 'Variance requires manual approval',
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// Create receipt mutation (add to destination)
|
|
516
|
-
log.info('📦 Creating receipt mutation...');
|
|
517
|
-
const receiptStartTime = Date.now();
|
|
518
|
-
const receiptResult = await createReceiptMutation(
|
|
519
|
-
client,
|
|
520
|
-
receiptData.transferId,
|
|
521
|
-
receiptData.items,
|
|
522
|
-
transferState,
|
|
523
|
-
log
|
|
524
|
-
);
|
|
525
|
-
|
|
526
|
-
if (!receiptResult.success) {
|
|
527
|
-
throw new Error(`Receipt failed: ${receiptResult.error}`);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
const receiptDuration = Date.now() - receiptStartTime;
|
|
531
|
-
log.info('✅ Receipt mutation created', {
|
|
532
|
-
mutationId: receiptResult.mutationId,
|
|
533
|
-
duration: `${receiptDuration}ms`,
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
// Update state
|
|
537
|
-
transferState.status = 'RECEIVED';
|
|
538
|
-
transferState.receiptMutationId = receiptResult.mutationId;
|
|
539
|
-
transferState.receivedAt = new Date().toISOString();
|
|
540
|
-
transferState.updatedAt = new Date().toISOString();
|
|
541
|
-
await saveTransferState(kvAdapter, transferState);
|
|
542
|
-
|
|
543
|
-
// Send completion notification
|
|
544
|
-
await sendCompletionNotification(receiptData.transferId, transferState, activation, log);
|
|
545
|
-
|
|
546
|
-
const totalDuration = Date.now() - startTime;
|
|
547
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
548
|
-
log.info('✅ RECEIPT CONFIRMATION COMPLETED', {
|
|
549
|
-
transferId: receiptData.transferId,
|
|
550
|
-
duration: `${totalDuration}ms`,
|
|
551
|
-
});
|
|
552
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
553
|
-
|
|
554
|
-
return {
|
|
555
|
-
success: true,
|
|
556
|
-
status: 'RECEIVED',
|
|
557
|
-
transferId: receiptData.transferId,
|
|
558
|
-
receiptMutationId: receiptResult.mutationId,
|
|
559
|
-
variance: variance.hasVariance ? variance : undefined,
|
|
560
|
-
};
|
|
561
|
-
} finally {
|
|
562
|
-
log.info('🔓 Releasing transfer lock...');
|
|
563
|
-
await stateService.releaseLock(lockName, kvAdapter);
|
|
564
|
-
}
|
|
565
|
-
} catch (error: any) {
|
|
566
|
-
const duration = Date.now() - startTime;
|
|
567
|
-
log.error('═══════════════════════════════════════════════════════');
|
|
568
|
-
log.error('❌ RECEIPT CONFIRMATION FAILED', {
|
|
569
|
-
transferId: receiptData.transferId,
|
|
570
|
-
error: error instanceof Error ? error.message : String(error),
|
|
571
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
572
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
573
|
-
duration: `${duration}ms`,
|
|
574
|
-
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
575
|
-
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
576
|
-
: error.message?.includes('not found') || error.message?.includes('missing')
|
|
577
|
-
? 'Transfer not found - verify transferId and check transfer state in KV store'
|
|
578
|
-
: error.message?.includes('status') || error.message?.includes('invalid')
|
|
579
|
-
? 'Invalid transfer status - ensure transfer is in IN_TRANSIT status'
|
|
580
|
-
: error.message?.includes('lock') || error.message?.includes('concurrent')
|
|
581
|
-
? 'Concurrent receipt processing detected - retry after lock release'
|
|
582
|
-
: error.message?.includes('variance') || error.message?.includes('approval')
|
|
583
|
-
? 'Variance requires approval - check variance calculation and approval workflow'
|
|
584
|
-
: 'Review error details and check receipt confirmation payload structure',
|
|
585
|
-
});
|
|
586
|
-
log.error('═══════════════════════════════════════════════════════');
|
|
587
|
-
|
|
588
|
-
return {
|
|
589
|
-
success: false,
|
|
590
|
-
error: error.message,
|
|
591
|
-
transferId: receiptData.transferId,
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
/**
|
|
597
|
-
* Process variance approval/rejection
|
|
598
|
-
*/
|
|
599
|
-
export async function processVarianceApproval(
|
|
600
|
-
ctx: any,
|
|
601
|
-
approvalData: { transferId: string; approved: boolean; notes?: string }
|
|
602
|
-
): Promise<TransferResult> {
|
|
603
|
-
const { log, activation, openKv } = ctx;
|
|
604
|
-
const { transferId, approved, notes } = approvalData;
|
|
605
|
-
const startTime = Date.now();
|
|
606
|
-
|
|
607
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
608
|
-
log.info('📝 VARIANCE APPROVAL PROCESSING', {
|
|
609
|
-
transferId,
|
|
610
|
-
approved,
|
|
611
|
-
});
|
|
612
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
613
|
-
|
|
614
|
-
try {
|
|
615
|
-
const client = await createClient({ ...ctx, validateConnection: true });
|
|
616
|
-
const fluentRetailerId = activation.getVariable('fluentRetailerId');
|
|
617
|
-
|
|
618
|
-
if (fluentRetailerId) {
|
|
619
|
-
client.setRetailerId(fluentRetailerId);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
623
|
-
const stateService = new StateService(log);
|
|
624
|
-
|
|
625
|
-
const transferState = await getTransferState(kvAdapter, transferId);
|
|
626
|
-
if (!transferState) {
|
|
627
|
-
throw new Error('Transfer not found');
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const lockName = `transfer-lock:${transferId}`;
|
|
631
|
-
const lockAcquired = await stateService.acquireLock(lockName, kvAdapter, 15);
|
|
632
|
-
if (!lockAcquired) {
|
|
633
|
-
throw new Error('Could not acquire lock');
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
try {
|
|
637
|
-
if (!approved) {
|
|
638
|
-
// Rollback: reverse the dispatch
|
|
639
|
-
log.info('❌ Variance rejected - rolling back transfer...');
|
|
640
|
-
const rollbackResult = await rollbackTransfer(client, transferState, log);
|
|
641
|
-
|
|
642
|
-
transferState.status = 'ROLLED_BACK';
|
|
643
|
-
transferState.updatedAt = new Date().toISOString();
|
|
644
|
-
await saveTransferState(kvAdapter, transferState);
|
|
645
|
-
|
|
646
|
-
const duration = Date.now() - startTime;
|
|
647
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
648
|
-
log.info('✅ TRANSFER ROLLED BACK', {
|
|
649
|
-
transferId,
|
|
650
|
-
duration: `${duration}ms`,
|
|
651
|
-
});
|
|
652
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
653
|
-
|
|
654
|
-
return {
|
|
655
|
-
success: true,
|
|
656
|
-
status: 'ROLLED_BACK',
|
|
657
|
-
transferId,
|
|
658
|
-
message: 'Transfer rolled back due to variance rejection',
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// Approved: complete receipt with actual quantities
|
|
663
|
-
log.info('✅ Variance approved - completing receipt...');
|
|
664
|
-
const receiptData = transferState.receiptData;
|
|
665
|
-
if (!receiptData) {
|
|
666
|
-
throw new Error('Receipt data not found in transfer state');
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
const receiptResult = await createReceiptMutation(
|
|
670
|
-
client,
|
|
671
|
-
transferId,
|
|
672
|
-
receiptData.items,
|
|
673
|
-
transferState,
|
|
674
|
-
log
|
|
675
|
-
);
|
|
676
|
-
|
|
677
|
-
if (!receiptResult.success) {
|
|
678
|
-
throw new Error(`Receipt failed: ${receiptResult.error}`);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
transferState.status = 'RECEIVED';
|
|
682
|
-
transferState.receiptMutationId = receiptResult.mutationId;
|
|
683
|
-
transferState.receivedAt = new Date().toISOString();
|
|
684
|
-
transferState.updatedAt = new Date().toISOString();
|
|
685
|
-
await saveTransferState(kvAdapter, transferState);
|
|
686
|
-
|
|
687
|
-
const duration = Date.now() - startTime;
|
|
688
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
689
|
-
log.info('✅ VARIANCE APPROVAL COMPLETED', {
|
|
690
|
-
transferId,
|
|
691
|
-
duration: `${duration}ms`,
|
|
692
|
-
});
|
|
693
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
694
|
-
|
|
695
|
-
return {
|
|
696
|
-
success: true,
|
|
697
|
-
status: 'RECEIVED',
|
|
698
|
-
transferId,
|
|
699
|
-
receiptMutationId: receiptResult.mutationId,
|
|
700
|
-
};
|
|
701
|
-
} finally {
|
|
702
|
-
await stateService.releaseLock(lockName, kvAdapter);
|
|
703
|
-
}
|
|
704
|
-
} catch (error: any) {
|
|
705
|
-
const duration = Date.now() - startTime;
|
|
706
|
-
log.error('═══════════════════════════════════════════════════════');
|
|
707
|
-
log.error('❌ VARIANCE APPROVAL FAILED', {
|
|
708
|
-
transferId,
|
|
709
|
-
error: error instanceof Error ? error.message : String(error),
|
|
710
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
711
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
712
|
-
duration: `${duration}ms`,
|
|
713
|
-
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
714
|
-
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
715
|
-
: error.message?.includes('not found') || error.message?.includes('missing')
|
|
716
|
-
? 'Transfer not found or receipt data missing - verify transferId and transfer state'
|
|
717
|
-
: error.message?.includes('lock') || error.message?.includes('concurrent')
|
|
718
|
-
? 'Concurrent approval processing detected - retry after lock release'
|
|
719
|
-
: error.message?.includes('receipt') || error.message?.includes('mutation')
|
|
720
|
-
? 'Check receipt mutation payload and inventory availability at destination'
|
|
721
|
-
: 'Review error details and check variance approval payload structure',
|
|
722
|
-
});
|
|
723
|
-
log.error('═══════════════════════════════════════════════════════');
|
|
724
|
-
|
|
725
|
-
return {
|
|
726
|
-
success: false,
|
|
727
|
-
error: error.message,
|
|
728
|
-
transferId,
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
/**
|
|
734
|
-
* Run reconciliation (scheduled)
|
|
735
|
-
*/
|
|
736
|
-
export async function runReconciliation(
|
|
737
|
-
ctx: any,
|
|
738
|
-
jobId: string,
|
|
739
|
-
tracker: any
|
|
740
|
-
): Promise<any> {
|
|
741
|
-
const { log, openKv } = ctx;
|
|
742
|
-
const startTime = Date.now();
|
|
743
|
-
|
|
744
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
745
|
-
log.info('🔄 RECONCILIATION STARTED', { jobId });
|
|
746
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
747
|
-
|
|
748
|
-
try {
|
|
749
|
-
await tracker.updateJob(jobId, { stage: 'discovery', status: 'processing' });
|
|
750
|
-
|
|
751
|
-
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
752
|
-
|
|
753
|
-
// Find all in-transit transfers older than 24 hours
|
|
754
|
-
log.info('🔍 Discovering stale transfers...');
|
|
755
|
-
const allTransfers = await getAllTransferStates(kvAdapter);
|
|
756
|
-
const staleTransfers = allTransfers.filter(t => {
|
|
757
|
-
if (t.status !== 'IN_TRANSIT') return false;
|
|
758
|
-
const dispatchedAt = new Date(t.dispatchedAt || t.createdAt);
|
|
759
|
-
const hoursSinceDispatch = (Date.now() - dispatchedAt.getTime()) / (1000 * 60 * 60);
|
|
760
|
-
return hoursSinceDispatch > 24;
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
log.info('📊 Stale transfers found', { count: staleTransfers.length });
|
|
764
|
-
|
|
765
|
-
await tracker.updateJob(jobId, {
|
|
766
|
-
stage: 'processing',
|
|
767
|
-
metadata: { staleCount: staleTransfers.length },
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
// Process each stale transfer (generate alerts, etc.)
|
|
771
|
-
let alertsSent = 0;
|
|
772
|
-
for (const transfer of staleTransfers) {
|
|
773
|
-
log.warn('⚠️ Stale transfer detected', {
|
|
774
|
-
transferId: transfer.transferId,
|
|
775
|
-
status: transfer.status,
|
|
776
|
-
age: Math.floor((Date.now() - new Date(transfer.dispatchedAt || transfer.createdAt).getTime()) / (1000 * 60 * 60)) + ' hours',
|
|
777
|
-
});
|
|
778
|
-
// Send alert email (implement as needed)
|
|
779
|
-
alertsSent++;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
const duration = Date.now() - startTime;
|
|
783
|
-
const result = {
|
|
784
|
-
staleTransfersFound: staleTransfers.length,
|
|
785
|
-
alertsSent,
|
|
786
|
-
duration: `${duration}ms`,
|
|
787
|
-
};
|
|
788
|
-
|
|
789
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
790
|
-
log.info('✅ RECONCILIATION COMPLETED', result);
|
|
791
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
792
|
-
|
|
793
|
-
return result;
|
|
794
|
-
} catch (error: any) {
|
|
795
|
-
const duration = Date.now() - startTime;
|
|
796
|
-
log.error('═══════════════════════════════════════════════════════');
|
|
797
|
-
log.error('❌ RECONCILIATION FAILED', {
|
|
798
|
-
jobId,
|
|
799
|
-
error: error instanceof Error ? error.message : String(error),
|
|
800
|
-
duration: `${duration}ms`,
|
|
801
|
-
});
|
|
802
|
-
log.error('═══════════════════════════════════════════════════════');
|
|
803
|
-
throw error;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
/**
|
|
808
|
-
* Helper: Create dispatch mutation (remove from source location)
|
|
809
|
-
*/
|
|
810
|
-
async function createDispatchMutation(
|
|
811
|
-
client: FluentClient,
|
|
812
|
-
transferRequest: TransferRequest,
|
|
813
|
-
log: Logger
|
|
814
|
-
): Promise<{ success: boolean; mutationId?: string; error?: string }> {
|
|
815
|
-
try {
|
|
816
|
-
const dispatchMapper = new GraphQLMutationMapper(
|
|
817
|
-
{
|
|
818
|
-
version: '1.0.0',
|
|
819
|
-
mutation: 'updateInventoryQuantity',
|
|
820
|
-
sourceFormat: 'json',
|
|
821
|
-
fields: {
|
|
822
|
-
'input.ref': {
|
|
823
|
-
resolver: 'custom.generateDispatchRef',
|
|
824
|
-
},
|
|
825
|
-
'input.locationRef': {
|
|
826
|
-
source: 'sourceLocationRef',
|
|
827
|
-
},
|
|
828
|
-
'input.items[]': {
|
|
829
|
-
source: 'items',
|
|
830
|
-
fields: {
|
|
831
|
-
skuRef: { source: 'skuRef' },
|
|
832
|
-
qty: {
|
|
833
|
-
source: 'requestedQty',
|
|
834
|
-
resolver: 'custom.negateQty', // Make negative for dispatch
|
|
835
|
-
},
|
|
836
|
-
type: { value: 'DISPATCH' },
|
|
837
|
-
status: { value: 'AVAILABLE' },
|
|
838
|
-
},
|
|
839
|
-
},
|
|
840
|
-
},
|
|
841
|
-
returnFields: ['id', 'ref'],
|
|
842
|
-
} as any,
|
|
843
|
-
client,
|
|
844
|
-
log
|
|
845
|
-
);
|
|
846
|
-
|
|
847
|
-
// Custom resolvers
|
|
848
|
-
const resolvers = {
|
|
849
|
-
'custom.generateDispatchRef': () => {
|
|
850
|
-
return `DISPATCH-${transferRequest.transferId}-${Date.now()}`;
|
|
851
|
-
},
|
|
852
|
-
'custom.negateQty': (qty: any) => {
|
|
853
|
-
return -Math.abs(qty);
|
|
854
|
-
},
|
|
855
|
-
};
|
|
856
|
-
|
|
857
|
-
const result = await dispatchMapper.mapWithNodes(transferRequest, resolvers, {
|
|
858
|
-
config: {},
|
|
859
|
-
helpers: {},
|
|
860
|
-
});
|
|
861
|
-
|
|
862
|
-
if (!result.success) {
|
|
863
|
-
throw new Error(`Mapping failed: ${result.errors?.join(', ')}`);
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
// Execute mutation (query is auto-generated in result)
|
|
867
|
-
const response = await client.graphql({
|
|
868
|
-
query: result.query,
|
|
869
|
-
variables: result.variables, // ✅ Use variables (wrapped if fields pattern)
|
|
870
|
-
});
|
|
871
|
-
|
|
872
|
-
return {
|
|
873
|
-
success: true,
|
|
874
|
-
mutationId: response.data?.updateInventoryQuantity?.id,
|
|
875
|
-
};
|
|
876
|
-
} catch (error: any) {
|
|
877
|
-
log.error('❌ Dispatch mutation failed', {
|
|
878
|
-
error: error instanceof Error ? error.message : String(error),
|
|
879
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
880
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
881
|
-
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
882
|
-
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
883
|
-
: error.message?.includes('mapping') || error.message?.includes('field')
|
|
884
|
-
? 'Check dispatch mapping configuration and verify transfer request structure'
|
|
885
|
-
: error.message?.includes('inventory') || error.message?.includes('quantity')
|
|
886
|
-
? 'Check available inventory quantity at source location'
|
|
887
|
-
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
888
|
-
? 'Check network connectivity and Fluent Commerce API availability'
|
|
889
|
-
: 'Review error details and check dispatch mutation payload',
|
|
890
|
-
});
|
|
891
|
-
|
|
892
|
-
return {
|
|
893
|
-
success: false,
|
|
894
|
-
error: error.message,
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
/**
|
|
900
|
-
* Helper: Create receipt mutation (add to destination location)
|
|
901
|
-
*/
|
|
902
|
-
async function createReceiptMutation(
|
|
903
|
-
client: FluentClient,
|
|
904
|
-
transferId: string,
|
|
905
|
-
items: TransferItem[],
|
|
906
|
-
transferState: TransferState,
|
|
907
|
-
log: Logger
|
|
908
|
-
): Promise<{ success: boolean; mutationId?: string; error?: string }> {
|
|
909
|
-
try {
|
|
910
|
-
const receiptMapper = new GraphQLMutationMapper(
|
|
911
|
-
{
|
|
912
|
-
version: '1.0.0',
|
|
913
|
-
mutation: 'updateInventoryQuantity',
|
|
914
|
-
sourceFormat: 'json',
|
|
915
|
-
fields: {
|
|
916
|
-
'input.ref': {
|
|
917
|
-
value: `RECEIPT-${transferId}-${Date.now()}`,
|
|
918
|
-
},
|
|
919
|
-
'input.locationRef': {
|
|
920
|
-
source: 'destinationLocationRef',
|
|
921
|
-
},
|
|
922
|
-
'input.items[]': {
|
|
923
|
-
source: 'items',
|
|
924
|
-
fields: {
|
|
925
|
-
skuRef: { source: 'skuRef' },
|
|
926
|
-
qty: {
|
|
927
|
-
source: 'actualQty',
|
|
928
|
-
defaultValue: { source: 'requestedQty' }, // Use requested if actual not provided
|
|
929
|
-
},
|
|
930
|
-
type: { value: 'RECEIPT' },
|
|
931
|
-
status: { value: 'AVAILABLE' },
|
|
932
|
-
},
|
|
933
|
-
},
|
|
934
|
-
},
|
|
935
|
-
returnFields: ['id', 'ref'],
|
|
936
|
-
} as any,
|
|
937
|
-
client,
|
|
938
|
-
log
|
|
939
|
-
);
|
|
940
|
-
|
|
941
|
-
const sourceData = {
|
|
942
|
-
destinationLocationRef: transferState.originalRequest?.destinationLocationRef,
|
|
943
|
-
items,
|
|
944
|
-
};
|
|
945
|
-
|
|
946
|
-
const result = await receiptMapper.mapWithNodes(sourceData, {}, { config: {}, helpers: {} });
|
|
947
|
-
|
|
948
|
-
if (!result.success) {
|
|
949
|
-
throw new Error(`Mapping failed: ${result.errors?.join(', ')}`);
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
// Execute mutation (query is auto-generated in result)
|
|
953
|
-
const response = await client.graphql({
|
|
954
|
-
query: result.query,
|
|
955
|
-
variables: result.variables, // ✅ Use variables (wrapped if fields pattern)
|
|
956
|
-
});
|
|
957
|
-
|
|
958
|
-
return {
|
|
959
|
-
success: true,
|
|
960
|
-
mutationId: response.data?.updateInventoryQuantity?.id,
|
|
961
|
-
};
|
|
962
|
-
} catch (error: any) {
|
|
963
|
-
log.error('❌ Receipt mutation failed', {
|
|
964
|
-
error: error instanceof Error ? error.message : String(error),
|
|
965
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
966
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
967
|
-
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
968
|
-
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
969
|
-
: error.message?.includes('mapping') || error.message?.includes('field')
|
|
970
|
-
? 'Check receipt mapping configuration and verify receipt data structure'
|
|
971
|
-
: error.message?.includes('inventory') || error.message?.includes('quantity')
|
|
972
|
-
? 'Check actual quantity values and variance calculation'
|
|
973
|
-
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
974
|
-
? 'Check network connectivity and Fluent Commerce API availability'
|
|
975
|
-
: 'Review error details and check receipt mutation payload',
|
|
976
|
-
});
|
|
977
|
-
|
|
978
|
-
return {
|
|
979
|
-
success: false,
|
|
980
|
-
error: error.message,
|
|
981
|
-
};
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
/**
|
|
986
|
-
* Helper: Rollback transfer (reverse dispatch)
|
|
987
|
-
*/
|
|
988
|
-
async function rollbackTransfer(
|
|
989
|
-
client: FluentClient,
|
|
990
|
-
transferState: TransferState,
|
|
991
|
-
log: Logger
|
|
992
|
-
): Promise<{ success: boolean; error?: string }> {
|
|
993
|
-
try {
|
|
994
|
-
const originalRequest = transferState.originalRequest;
|
|
995
|
-
|
|
996
|
-
if (!originalRequest) {
|
|
997
|
-
throw new Error('Cannot rollback: original request data missing');
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
const rollbackMapper = new GraphQLMutationMapper(
|
|
1001
|
-
{
|
|
1002
|
-
version: '1.0.0',
|
|
1003
|
-
mutation: 'updateInventoryQuantity',
|
|
1004
|
-
sourceFormat: 'json',
|
|
1005
|
-
fields: {
|
|
1006
|
-
'input.ref': {
|
|
1007
|
-
value: `ROLLBACK-${transferState.transferId}-${Date.now()}`,
|
|
1008
|
-
},
|
|
1009
|
-
'input.locationRef': {
|
|
1010
|
-
source: 'sourceLocationRef',
|
|
1011
|
-
},
|
|
1012
|
-
'input.items[]': {
|
|
1013
|
-
source: 'items',
|
|
1014
|
-
fields: {
|
|
1015
|
-
skuRef: { source: 'skuRef' },
|
|
1016
|
-
qty: { source: 'requestedQty' }, // Add back to source
|
|
1017
|
-
type: { value: 'ROLLBACK' },
|
|
1018
|
-
status: { value: 'AVAILABLE' },
|
|
1019
|
-
},
|
|
1020
|
-
},
|
|
1021
|
-
},
|
|
1022
|
-
returnFields: ['id', 'ref'],
|
|
1023
|
-
} as any,
|
|
1024
|
-
client,
|
|
1025
|
-
log
|
|
1026
|
-
);
|
|
1027
|
-
|
|
1028
|
-
const result = await rollbackMapper.mapWithNodes(
|
|
1029
|
-
originalRequest,
|
|
1030
|
-
{},
|
|
1031
|
-
{ config: {}, helpers: {} }
|
|
1032
|
-
);
|
|
1033
|
-
|
|
1034
|
-
if (!result.success) {
|
|
1035
|
-
throw new Error(`Mapping failed: ${result.errors?.join(', ')}`);
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
// Execute mutation (query is auto-generated in result)
|
|
1039
|
-
await client.graphql({
|
|
1040
|
-
query: result.query,
|
|
1041
|
-
variables: result.variables, // ✅ Use variables (wrapped if fields pattern)
|
|
1042
|
-
});
|
|
1043
|
-
|
|
1044
|
-
return { success: true };
|
|
1045
|
-
} catch (error: any) {
|
|
1046
|
-
log.error('❌ Rollback failed', {
|
|
1047
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1048
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
1049
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1050
|
-
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
1051
|
-
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
1052
|
-
: error.message?.includes('mapping') || error.message?.includes('field')
|
|
1053
|
-
? 'Check rollback mapping configuration and verify transfer state structure'
|
|
1054
|
-
: error.message?.includes('original') || error.message?.includes('missing')
|
|
1055
|
-
? 'Original transfer request data missing - verify transfer state in KV store'
|
|
1056
|
-
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
1057
|
-
? 'Check network connectivity and Fluent Commerce API availability'
|
|
1058
|
-
: 'Review error details and check rollback mutation payload',
|
|
1059
|
-
});
|
|
1060
|
-
|
|
1061
|
-
return {
|
|
1062
|
-
success: false,
|
|
1063
|
-
error: error.message,
|
|
1064
|
-
};
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
/**
|
|
1069
|
-
* Helper: Calculate variance between expected and actual quantities
|
|
1070
|
-
*/
|
|
1071
|
-
function calculateVariance(
|
|
1072
|
-
expectedItems: TransferItem[],
|
|
1073
|
-
actualItems: TransferItem[]
|
|
1074
|
-
): VarianceDetails {
|
|
1075
|
-
const varianceItems = [];
|
|
1076
|
-
let totalExpected = 0;
|
|
1077
|
-
let totalActual = 0;
|
|
1078
|
-
|
|
1079
|
-
for (const expectedItem of expectedItems) {
|
|
1080
|
-
const actualItem = actualItems.find(i => i.skuRef === expectedItem.skuRef);
|
|
1081
|
-
const expectedQty = expectedItem.requestedQty;
|
|
1082
|
-
const actualQty = actualItem?.actualQty ?? expectedQty;
|
|
1083
|
-
|
|
1084
|
-
totalExpected += expectedQty;
|
|
1085
|
-
totalActual += actualQty;
|
|
1086
|
-
|
|
1087
|
-
if (expectedQty !== actualQty) {
|
|
1088
|
-
varianceItems.push({
|
|
1089
|
-
skuRef: expectedItem.skuRef,
|
|
1090
|
-
expected: expectedQty,
|
|
1091
|
-
actual: actualQty,
|
|
1092
|
-
difference: actualQty - expectedQty,
|
|
1093
|
-
reason: actualItem?.notes,
|
|
1094
|
-
});
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
const variancePercentage =
|
|
1099
|
-
totalExpected > 0 ? Math.abs((totalActual - totalExpected) / totalExpected) * 100 : 0;
|
|
1100
|
-
|
|
1101
|
-
return {
|
|
1102
|
-
hasVariance: varianceItems.length > 0,
|
|
1103
|
-
variancePercentage,
|
|
1104
|
-
requiresApproval: variancePercentage > 5, // > 5% requires approval
|
|
1105
|
-
items: varianceItems,
|
|
1106
|
-
};
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
/**
|
|
1110
|
-
* Helper: Get transfer state from KV
|
|
1111
|
-
*/
|
|
1112
|
-
async function getTransferState(
|
|
1113
|
-
kvAdapter: VersoriKVAdapter,
|
|
1114
|
-
transferId: string
|
|
1115
|
-
): Promise<TransferState | null> {
|
|
1116
|
-
const stateKey = ['transfer-state', transferId];
|
|
1117
|
-
const result = await kvAdapter.get(stateKey);
|
|
1118
|
-
return result?.value as TransferState | null;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
/**
|
|
1122
|
-
* Helper: Save transfer state to KV
|
|
1123
|
-
*/
|
|
1124
|
-
async function saveTransferState(
|
|
1125
|
-
kvAdapter: VersoriKVAdapter,
|
|
1126
|
-
transferState: TransferState
|
|
1127
|
-
): Promise<void> {
|
|
1128
|
-
const stateKey = ['transfer-state', transferState.transferId];
|
|
1129
|
-
await kvAdapter.set(stateKey, transferState);
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
/**
|
|
1133
|
-
* Helper: Get all transfer states (for reconciliation)
|
|
1134
|
-
*/
|
|
1135
|
-
async function getAllTransferStates(
|
|
1136
|
-
kvAdapter: VersoriKVAdapter
|
|
1137
|
-
): Promise<TransferState[]> {
|
|
1138
|
-
// Note: This is a simplified implementation
|
|
1139
|
-
// In production, you'd use KV list operations with proper pagination
|
|
1140
|
-
const transfers: TransferState[] = [];
|
|
1141
|
-
|
|
1142
|
-
// Implementation would depend on your KV structure
|
|
1143
|
-
// This is a placeholder showing the expected return type
|
|
1144
|
-
|
|
1145
|
-
return transfers;
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
/**
|
|
1149
|
-
* Helper: Send variance notification email
|
|
1150
|
-
*/
|
|
1151
|
-
async function sendVarianceNotification(
|
|
1152
|
-
transferId: string,
|
|
1153
|
-
variance: VarianceDetails,
|
|
1154
|
-
activation: any,
|
|
1155
|
-
log: any
|
|
1156
|
-
): Promise<void> {
|
|
1157
|
-
const emailTo = activation.getVariable('varianceNotificationEmail') as string;
|
|
1158
|
-
if (!emailTo) {
|
|
1159
|
-
log.warn('⚠️ Variance notification email not configured', { transferId });
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
// Email sending implementation
|
|
1164
|
-
// Use Versori email connector or external service
|
|
1165
|
-
log.info('📧 Sending variance notification', {
|
|
1166
|
-
transferId,
|
|
1167
|
-
emailTo,
|
|
1168
|
-
variancePercentage: variance.variancePercentage.toFixed(2) + '%',
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
/**
|
|
1173
|
-
* Helper: Send completion notification
|
|
1174
|
-
*/
|
|
1175
|
-
async function sendCompletionNotification(
|
|
1176
|
-
transferId: string,
|
|
1177
|
-
transferState: TransferState,
|
|
1178
|
-
activation: any,
|
|
1179
|
-
log: any
|
|
1180
|
-
): Promise<void> {
|
|
1181
|
-
const emailTo = activation.getVariable('completionNotificationEmail') as string;
|
|
1182
|
-
if (!emailTo) return;
|
|
1183
|
-
|
|
1184
|
-
log.info('📧 Sending completion notification', {
|
|
1185
|
-
transferId,
|
|
1186
|
-
emailTo,
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
|
-
```
|
|
1190
|
-
|
|
1191
|
-
---
|
|
1192
|
-
|
|
1193
|
-
### 3. Workflow: Initiate Transfer (`src/workflows/webhook/initiate-transfer.ts`)
|
|
1194
|
-
|
|
1195
|
-
```typescript
|
|
1196
|
-
/**
|
|
1197
|
-
* Webhook: Initiate Transfer
|
|
1198
|
-
*
|
|
1199
|
-
* Endpoint: POST https://{workspace}.versori.run/initiate-transfer
|
|
1200
|
-
* Request body: TransferRequest (JSON)
|
|
1201
|
-
*
|
|
1202
|
-
* Pattern: webhook().then(http()) - needs Fluent API access
|
|
1203
|
-
* Uses shared service: transfer.service.ts
|
|
1204
|
-
*/
|
|
1205
|
-
|
|
1206
|
-
import { webhook, http } from '@versori/run';
|
|
1207
|
-
import { processTransferRequest } from '../../services/transfer.service';
|
|
1208
|
-
|
|
1209
|
-
export const initiateTransfer = webhook('initiate-transfer', {
|
|
1210
|
-
response: { mode: 'sync' },
|
|
1211
|
-
connection: 'initiate-transfer', // Versori validates API key
|
|
1212
|
-
}).then(
|
|
1213
|
-
http('process-transfer-request', { connection: 'fluent_commerce' }, async ctx => {
|
|
1214
|
-
const { log, data } = ctx;
|
|
1215
|
-
const jobId = `transfer-${Date.now()}`;
|
|
1216
|
-
|
|
1217
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
1218
|
-
log.info('📦 TRANSFER REQUEST RECEIVED', { jobId });
|
|
1219
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
1220
|
-
|
|
1221
|
-
try {
|
|
1222
|
-
const result = await processTransferRequest(ctx, data);
|
|
1223
|
-
|
|
1224
|
-
if (result.success) {
|
|
1225
|
-
log.info('✅ TRANSFER REQUEST COMPLETED', { jobId, result });
|
|
1226
|
-
} else {
|
|
1227
|
-
log.error('❌ TRANSFER REQUEST FAILED', { jobId, error: result.error });
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
return { success: result.success, jobId, ...result };
|
|
1231
|
-
} catch (e: any) {
|
|
1232
|
-
log.error('❌ TRANSFER REQUEST FAILED', { jobId, error: e.message });
|
|
1233
|
-
return { success: false, jobId, error: e.message };
|
|
1234
|
-
}
|
|
1235
|
-
})
|
|
1236
|
-
);
|
|
1237
|
-
```
|
|
1238
|
-
|
|
1239
|
-
---
|
|
1240
|
-
|
|
1241
|
-
### 4. Workflow: Receive Transfer (`src/workflows/webhook/receive-transfer.ts`)
|
|
1242
|
-
|
|
1243
|
-
```typescript
|
|
1244
|
-
/**
|
|
1245
|
-
* Webhook: Receive Transfer (Receipt Confirmation)
|
|
1246
|
-
*
|
|
1247
|
-
* Endpoint: POST https://{workspace}.versori.run/receive-transfer
|
|
1248
|
-
* Request body: { transferId, receivedDate, items[] }
|
|
1249
|
-
*
|
|
1250
|
-
* Pattern: webhook().then(http()) - needs Fluent API access
|
|
1251
|
-
* Uses shared service: transfer.service.ts
|
|
1252
|
-
*/
|
|
1253
|
-
|
|
1254
|
-
import { webhook, http } from '@versori/run';
|
|
1255
|
-
import { processReceiptConfirmation } from '../../services/transfer.service';
|
|
1256
|
-
|
|
1257
|
-
export const receiveTransfer = webhook('receive-transfer', {
|
|
1258
|
-
response: { mode: 'sync' },
|
|
1259
|
-
connection: 'receive-transfer',
|
|
1260
|
-
}).then(
|
|
1261
|
-
http('process-receipt-confirmation', { connection: 'fluent_commerce' }, async ctx => {
|
|
1262
|
-
const { log, data } = ctx;
|
|
1263
|
-
const jobId = `receipt-${Date.now()}`;
|
|
1264
|
-
|
|
1265
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
1266
|
-
log.info('📦 RECEIPT CONFIRMATION RECEIVED', { jobId, transferId: data.transferId });
|
|
1267
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
1268
|
-
|
|
1269
|
-
try {
|
|
1270
|
-
const result = await processReceiptConfirmation(ctx, data);
|
|
1271
|
-
|
|
1272
|
-
if (result.success) {
|
|
1273
|
-
log.info('✅ RECEIPT CONFIRMATION COMPLETED', { jobId, result });
|
|
1274
|
-
} else {
|
|
1275
|
-
log.error('❌ RECEIPT CONFIRMATION FAILED', { jobId, error: result.error });
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
return { success: result.success, jobId, ...result };
|
|
1279
|
-
} catch (e: any) {
|
|
1280
|
-
log.error('❌ RECEIPT CONFIRMATION FAILED', { jobId, error: e.message });
|
|
1281
|
-
return { success: false, jobId, error: e.message };
|
|
1282
|
-
}
|
|
1283
|
-
})
|
|
1284
|
-
);
|
|
1285
|
-
```
|
|
1286
|
-
|
|
1287
|
-
---
|
|
1288
|
-
|
|
1289
|
-
### 5. Workflow: Reconcile Transfers (`src/workflows/scheduled/reconcile-transfers.ts`)
|
|
1290
|
-
|
|
1291
|
-
```typescript
|
|
1292
|
-
/**
|
|
1293
|
-
* Scheduled Workflow: Transfer Reconciliation
|
|
1294
|
-
*
|
|
1295
|
-
* Runs automatically daily at 1 AM UTC
|
|
1296
|
-
* NOT exposed as HTTP endpoint - Versori executes on schedule
|
|
1297
|
-
*
|
|
1298
|
-
* Finds stale in-transit transfers (> 24 hours) and generates alerts
|
|
1299
|
-
* Uses JobTracker for status tracking
|
|
1300
|
-
*/
|
|
1301
|
-
|
|
1302
|
-
import { schedule, http } from '@versori/run';
|
|
1303
|
-
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
1304
|
-
import { runReconciliation } from '../../services/transfer.service';
|
|
1305
|
-
|
|
1306
|
-
export const reconcileTransfers = schedule(
|
|
1307
|
-
'reconcile-transfers',
|
|
1308
|
-
'0 1 * * *' // Daily at 1 AM UTC
|
|
1309
|
-
).then(
|
|
1310
|
-
http('run-reconciliation', { connection: 'fluent_commerce' }, async ctx => {
|
|
1311
|
-
const { log, openKv } = ctx;
|
|
1312
|
-
const jobId = `reconcile-${Date.now()}`;
|
|
1313
|
-
|
|
1314
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
1315
|
-
log.info('🔄 RECONCILIATION STARTED', { jobId });
|
|
1316
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
1317
|
-
|
|
1318
|
-
const tracker = new JobTracker(openKv(':project:'), log);
|
|
1319
|
-
await tracker.createJob(jobId, { triggeredBy: 'schedule' });
|
|
1320
|
-
|
|
1321
|
-
try {
|
|
1322
|
-
const result = await runReconciliation(ctx, jobId, tracker);
|
|
1323
|
-
await tracker.markCompleted(jobId, result);
|
|
1324
|
-
|
|
1325
|
-
log.info('✅ RECONCILIATION COMPLETED', { jobId, result });
|
|
1326
|
-
return { success: true, jobId, ...result };
|
|
1327
|
-
} catch (e: any) {
|
|
1328
|
-
await tracker.markFailed(jobId, e);
|
|
1329
|
-
|
|
1330
|
-
log.error('❌ RECONCILIATION FAILED', { jobId, error: e.message });
|
|
1331
|
-
return { success: false, jobId, error: e.message };
|
|
1332
|
-
}
|
|
1333
|
-
})
|
|
1334
|
-
);
|
|
1335
|
-
```
|
|
1336
|
-
|
|
1337
|
-
---
|
|
1338
|
-
|
|
1339
|
-
### 6. Workflow: Export Transfer Report (`src/workflows/webhook/export-transfer-report.ts`)
|
|
1340
|
-
|
|
1341
|
-
```typescript
|
|
1342
|
-
/**
|
|
1343
|
-
* Webhook: Export Transfer Report
|
|
1344
|
-
*
|
|
1345
|
-
* Endpoint: POST https://{workspace}.versori.run/export-transfer-report
|
|
1346
|
-
* Request body: { startDate, endDate }
|
|
1347
|
-
*
|
|
1348
|
-
* Pattern: webhook().then(fn()) - no external API needed, only KV storage
|
|
1349
|
-
* Queries KV store for transfer states and generates report
|
|
1350
|
-
*/
|
|
1351
|
-
|
|
1352
|
-
import { webhook, fn } from '@versori/run';
|
|
1353
|
-
import { VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
1354
|
-
|
|
1355
|
-
export const exportTransferReport = webhook('export-transfer-report', {
|
|
1356
|
-
response: { mode: 'sync' },
|
|
1357
|
-
connection: 'export-transfer-report',
|
|
1358
|
-
}).then(
|
|
1359
|
-
fn('generate-report', async ctx => {
|
|
1360
|
-
const { log, openKv, data } = ctx;
|
|
1361
|
-
const { startDate, endDate } = data;
|
|
1362
|
-
|
|
1363
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
1364
|
-
log.info('📊 TRANSFER REPORT EXPORT', { startDate, endDate });
|
|
1365
|
-
log.info('═══════════════════════════════════════════════════════');
|
|
1366
|
-
|
|
1367
|
-
try {
|
|
1368
|
-
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
1369
|
-
|
|
1370
|
-
// Query transfer states (simplified - implement proper date filtering)
|
|
1371
|
-
const transfers: any[] = []; // getAllTransferStates(kvAdapter, startDate, endDate);
|
|
1372
|
-
|
|
1373
|
-
const report = {
|
|
1374
|
-
period: { startDate, endDate },
|
|
1375
|
-
totalTransfers: transfers.length,
|
|
1376
|
-
byStatus: {
|
|
1377
|
-
pending: transfers.filter(t => t.status === 'PENDING').length,
|
|
1378
|
-
dispatched: transfers.filter(t => t.status === 'DISPATCHED').length,
|
|
1379
|
-
inTransit: transfers.filter(t => t.status === 'IN_TRANSIT').length,
|
|
1380
|
-
received: transfers.filter(t => t.status === 'RECEIVED').length,
|
|
1381
|
-
failed: transfers.filter(t => t.status === 'FAILED').length,
|
|
1382
|
-
rolledBack: transfers.filter(t => t.status === 'ROLLED_BACK').length,
|
|
1383
|
-
},
|
|
1384
|
-
transfers: transfers.map(t => ({
|
|
1385
|
-
transferId: t.transferId,
|
|
1386
|
-
status: t.status,
|
|
1387
|
-
sourceLocation: t.originalRequest?.sourceLocationRef,
|
|
1388
|
-
destinationLocation: t.originalRequest?.destinationLocationRef,
|
|
1389
|
-
itemCount: t.originalRequest?.items?.length || 0,
|
|
1390
|
-
createdAt: t.createdAt,
|
|
1391
|
-
completedAt: t.receivedAt || t.updatedAt,
|
|
1392
|
-
})),
|
|
1393
|
-
};
|
|
1394
|
-
|
|
1395
|
-
log.info('✅ REPORT GENERATED', {
|
|
1396
|
-
totalTransfers: report.totalTransfers,
|
|
1397
|
-
byStatus: report.byStatus,
|
|
1398
|
-
});
|
|
1399
|
-
|
|
1400
|
-
return { success: true, report };
|
|
1401
|
-
} catch (e: any) {
|
|
1402
|
-
log.error('❌ REPORT EXPORT FAILED', { error: e.message });
|
|
1403
|
-
return { success: false, error: e.message };
|
|
1404
|
-
}
|
|
1405
|
-
})
|
|
1406
|
-
);
|
|
1407
|
-
```
|
|
1408
|
-
|
|
1409
|
-
---
|
|
1410
|
-
|
|
1411
|
-
## Configuration
|
|
1412
|
-
|
|
1413
|
-
### Activation Variables Configuration
|
|
1414
|
-
|
|
1415
|
-
```bash
|
|
1416
|
-
# Required Variables
|
|
1417
|
-
fluentRetailerId=my-retailer-id
|
|
1418
|
-
|
|
1419
|
-
# S3 Configuration (for CSV-based transfers)
|
|
1420
|
-
s3TransferBucket=my-transfers-bucket
|
|
1421
|
-
awsAccessKeyId=AKIAXXXXXXXXXXXX
|
|
1422
|
-
awsSecretAccessKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
1423
|
-
awsRegion=us-east-1
|
|
1424
|
-
s3TransferPrefix=transfers/
|
|
1425
|
-
|
|
1426
|
-
# Email Notifications
|
|
1427
|
-
varianceNotificationEmail=warehouse-manager@company.com
|
|
1428
|
-
completionNotificationEmail=logistics@company.com
|
|
1429
|
-
```
|
|
1430
|
-
|
|
1431
|
-
### GraphQL Mutation Mapping Configuration: `config/transfer-mappings.json`
|
|
1432
|
-
|
|
1433
|
-
```json
|
|
1434
|
-
{
|
|
1435
|
-
"dispatch": {
|
|
1436
|
-
"version": "1.0.0",
|
|
1437
|
-
"mutation": "updateInventoryQuantity",
|
|
1438
|
-
"sourceFormat": "json",
|
|
1439
|
-
"fields": {
|
|
1440
|
-
"input.ref": {
|
|
1441
|
-
"resolver": "custom.generateDispatchRef"
|
|
1442
|
-
},
|
|
1443
|
-
"input.locationRef": {
|
|
1444
|
-
"source": "sourceLocationRef",
|
|
1445
|
-
"required": true
|
|
1446
|
-
},
|
|
1447
|
-
"input.items": {
|
|
1448
|
-
"source": "items",
|
|
1449
|
-
"isArray": true,
|
|
1450
|
-
"fields": {
|
|
1451
|
-
"skuRef": {
|
|
1452
|
-
"source": "skuRef",
|
|
1453
|
-
"required": true
|
|
1454
|
-
},
|
|
1455
|
-
"qty": {
|
|
1456
|
-
"source": "requestedQty",
|
|
1457
|
-
"resolver": "custom.negateQty",
|
|
1458
|
-
"required": true
|
|
1459
|
-
},
|
|
1460
|
-
"type": {
|
|
1461
|
-
"value": "DISPATCH"
|
|
1462
|
-
},
|
|
1463
|
-
"status": {
|
|
1464
|
-
"value": "AVAILABLE"
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
},
|
|
1469
|
-
"returnFields": ["id", "ref"]
|
|
1470
|
-
},
|
|
1471
|
-
"receipt": {
|
|
1472
|
-
"version": "1.0.0",
|
|
1473
|
-
"mutation": "updateInventoryQuantity",
|
|
1474
|
-
"sourceFormat": "json",
|
|
1475
|
-
"fields": {
|
|
1476
|
-
"input.ref": {
|
|
1477
|
-
"resolver": "custom.generateReceiptRef"
|
|
1478
|
-
},
|
|
1479
|
-
"input.locationRef": {
|
|
1480
|
-
"source": "destinationLocationRef",
|
|
1481
|
-
"required": true
|
|
1482
|
-
},
|
|
1483
|
-
"input.items": {
|
|
1484
|
-
"source": "items",
|
|
1485
|
-
"isArray": true,
|
|
1486
|
-
"fields": {
|
|
1487
|
-
"skuRef": {
|
|
1488
|
-
"source": "skuRef",
|
|
1489
|
-
"required": true
|
|
1490
|
-
},
|
|
1491
|
-
"qty": {
|
|
1492
|
-
"source": "actualQty",
|
|
1493
|
-
"required": true
|
|
1494
|
-
},
|
|
1495
|
-
"type": {
|
|
1496
|
-
"value": "RECEIPT"
|
|
1497
|
-
},
|
|
1498
|
-
"status": {
|
|
1499
|
-
"value": "AVAILABLE"
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
},
|
|
1504
|
-
"returnFields": ["id", "ref"]
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
```
|
|
1508
|
-
|
|
1509
|
-
---
|
|
1510
|
-
|
|
1511
|
-
## Input/Output Examples
|
|
1512
|
-
|
|
1513
|
-
### CSV Input Format Example: `sample-transfer.csv`
|
|
1514
|
-
|
|
1515
|
-
```csv
|
|
1516
|
-
transfer_id,source_location,destination_location,requested_date,expected_delivery,items
|
|
1517
|
-
TRF-001,WH-EAST,STORE-NYC,2025-01-15,2025-01-17,"[{""sku"":""SKU-001"",""qty"":100},{""sku"":""SKU-002"",""qty"":50}]"
|
|
1518
|
-
TRF-002,WH-WEST,STORE-LA,2025-01-15,2025-01-18,"[{""sku"":""SKU-003"",""qty"":200}]"
|
|
1519
|
-
```
|
|
1520
|
-
|
|
1521
|
-
### JSON Input Format Example
|
|
1522
|
-
|
|
1523
|
-
```json
|
|
1524
|
-
{
|
|
1525
|
-
"transferId": "TRF-001",
|
|
1526
|
-
"sourceLocationRef": "WH-EAST",
|
|
1527
|
-
"destinationLocationRef": "STORE-NYC",
|
|
1528
|
-
"requestedDate": "2025-01-15T10:00:00Z",
|
|
1529
|
-
"expectedDeliveryDate": "2025-01-17T10:00:00Z",
|
|
1530
|
-
"items": [
|
|
1531
|
-
{
|
|
1532
|
-
"skuRef": "SKU-001",
|
|
1533
|
-
"requestedQty": 100,
|
|
1534
|
-
"condition": "NEW"
|
|
1535
|
-
},
|
|
1536
|
-
{
|
|
1537
|
-
"skuRef": "SKU-002",
|
|
1538
|
-
"requestedQty": 50,
|
|
1539
|
-
"condition": "NEW"
|
|
1540
|
-
}
|
|
1541
|
-
],
|
|
1542
|
-
"metadata": {
|
|
1543
|
-
"reason": "Store restocking",
|
|
1544
|
-
"priority": "HIGH",
|
|
1545
|
-
"requestedBy": "manager@store.com"
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
```
|
|
1549
|
-
|
|
1550
|
-
### Receipt Confirmation Format
|
|
1551
|
-
|
|
1552
|
-
```json
|
|
1553
|
-
{
|
|
1554
|
-
"transferId": "TRF-001",
|
|
1555
|
-
"receivedDate": "2025-01-17T14:30:00Z",
|
|
1556
|
-
"items": [
|
|
1557
|
-
{
|
|
1558
|
-
"skuRef": "SKU-001",
|
|
1559
|
-
"requestedQty": 100,
|
|
1560
|
-
"actualQty": 98,
|
|
1561
|
-
"notes": "2 units damaged in transit"
|
|
1562
|
-
},
|
|
1563
|
-
{
|
|
1564
|
-
"skuRef": "SKU-002",
|
|
1565
|
-
"requestedQty": 50,
|
|
1566
|
-
"actualQty": 50
|
|
1567
|
-
}
|
|
1568
|
-
]
|
|
1569
|
-
}
|
|
1570
|
-
```
|
|
1571
|
-
|
|
1572
|
-
---
|
|
1573
|
-
|
|
1574
|
-
## Versori Workflow Structure
|
|
1575
|
-
|
|
1576
|
-
### Workflow Flow Diagram
|
|
1577
|
-
|
|
1578
|
-
```
|
|
1579
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
1580
|
-
│ Transfer Request │
|
|
1581
|
-
│ (Webhook JSON or Scheduled CSV) │
|
|
1582
|
-
└──────────────────────┬──────────────────────────────────────┘
|
|
1583
|
-
│
|
|
1584
|
-
├─ Acquire Lock
|
|
1585
|
-
│
|
|
1586
|
-
├─ Initialize Transfer State (PENDING)
|
|
1587
|
-
│
|
|
1588
|
-
├─ Create Dispatch Mutation
|
|
1589
|
-
│ └─ Remove from Source Location
|
|
1590
|
-
│
|
|
1591
|
-
├─ Update State (IN_TRANSIT)
|
|
1592
|
-
│
|
|
1593
|
-
└─ Release Lock
|
|
1594
|
-
│
|
|
1595
|
-
┌──────────────────────┴──────────────────────────────────────┐
|
|
1596
|
-
│ Receipt Confirmation │
|
|
1597
|
-
│ (Webhook JSON) │
|
|
1598
|
-
└──────────────────────┬──────────────────────────────────────┘
|
|
1599
|
-
│
|
|
1600
|
-
├─ Acquire Lock
|
|
1601
|
-
│
|
|
1602
|
-
├─ Calculate Variance
|
|
1603
|
-
│ ├─ If > 5% → Require Approval
|
|
1604
|
-
│ │ └─ Send Email Notification
|
|
1605
|
-
│ │
|
|
1606
|
-
│ └─ If <= 5% → Auto-Approve
|
|
1607
|
-
│
|
|
1608
|
-
├─ Create Receipt Mutation
|
|
1609
|
-
│ └─ Add to Destination Location
|
|
1610
|
-
│
|
|
1611
|
-
├─ Update State (RECEIVED)
|
|
1612
|
-
│
|
|
1613
|
-
├─ Send Completion Notification
|
|
1614
|
-
│
|
|
1615
|
-
└─ Release Lock
|
|
1616
|
-
```
|
|
1617
|
-
|
|
1618
|
-
---
|
|
1619
|
-
|
|
1620
|
-
## Key Patterns Explained
|
|
1621
|
-
|
|
1622
|
-
### Pattern 1: Paired Mutations (Dispatch + Receipt)
|
|
1623
|
-
|
|
1624
|
-
**Dispatch Mutation** (Remove from source):
|
|
1625
|
-
|
|
1626
|
-
```typescript
|
|
1627
|
-
const dispatchPayload = {
|
|
1628
|
-
input: {
|
|
1629
|
-
ref: 'DISPATCH-TRF-001-1234567890',
|
|
1630
|
-
locationRef: 'WH-EAST',
|
|
1631
|
-
items: [
|
|
1632
|
-
{
|
|
1633
|
-
skuRef: 'SKU-001',
|
|
1634
|
-
qty: -100, // Negative quantity for removal
|
|
1635
|
-
type: 'DISPATCH',
|
|
1636
|
-
status: 'AVAILABLE',
|
|
1637
|
-
},
|
|
1638
|
-
],
|
|
1639
|
-
},
|
|
1640
|
-
};
|
|
1641
|
-
```
|
|
1642
|
-
|
|
1643
|
-
**Receipt Mutation** (Add to destination):
|
|
1644
|
-
|
|
1645
|
-
```typescript
|
|
1646
|
-
const receiptPayload = {
|
|
1647
|
-
input: {
|
|
1648
|
-
ref: 'RECEIPT-TRF-001-1234567891',
|
|
1649
|
-
locationRef: 'STORE-NYC',
|
|
1650
|
-
items: [
|
|
1651
|
-
{
|
|
1652
|
-
skuRef: 'SKU-001',
|
|
1653
|
-
qty: 98, // Actual quantity received
|
|
1654
|
-
type: 'RECEIPT',
|
|
1655
|
-
status: 'AVAILABLE',
|
|
1656
|
-
},
|
|
1657
|
-
],
|
|
1658
|
-
},
|
|
1659
|
-
};
|
|
1660
|
-
```
|
|
1661
|
-
|
|
1662
|
-
**Why Paired Mutations?**
|
|
1663
|
-
|
|
1664
|
-
- **Atomicity**: Each mutation is atomic (all-or-nothing)
|
|
1665
|
-
- **Audit Trail**: Separate records for dispatch and receipt
|
|
1666
|
-
- **Rollback**: Can reverse dispatch if receipt fails
|
|
1667
|
-
- **Variance Tracking**: Compare dispatch vs receipt quantities
|
|
1668
|
-
|
|
1669
|
-
### Pattern 2: In-Transit Inventory Tracking
|
|
1670
|
-
|
|
1671
|
-
**VersoriKV State Schema**:
|
|
1672
|
-
|
|
1673
|
-
```typescript
|
|
1674
|
-
interface TransferState {
|
|
1675
|
-
transferId: string;
|
|
1676
|
-
status: 'PENDING' | 'DISPATCHED' | 'IN_TRANSIT' | 'RECEIVED' | 'FAILED' | 'ROLLED_BACK';
|
|
1677
|
-
dispatchMutationId?: string;
|
|
1678
|
-
receiptMutationId?: string;
|
|
1679
|
-
dispatchedAt?: string;
|
|
1680
|
-
receivedAt?: string;
|
|
1681
|
-
variance?: VarianceDetails;
|
|
1682
|
-
createdAt: string;
|
|
1683
|
-
updatedAt: string;
|
|
1684
|
-
}
|
|
1685
|
-
```
|
|
1686
|
-
|
|
1687
|
-
**State Transitions**:
|
|
1688
|
-
|
|
1689
|
-
```
|
|
1690
|
-
PENDING → DISPATCHED → IN_TRANSIT → RECEIVED
|
|
1691
|
-
↓
|
|
1692
|
-
FAILED → ROLLED_BACK
|
|
1693
|
-
```
|
|
1694
|
-
|
|
1695
|
-
**Tracking Benefits**:
|
|
1696
|
-
|
|
1697
|
-
- Know what's in transit at any time
|
|
1698
|
-
- Query transfer status via API
|
|
1699
|
-
- Handle partial receipts
|
|
1700
|
-
- Track transit times for analytics
|
|
1701
|
-
|
|
1702
|
-
### Pattern 3: Variance Handling
|
|
1703
|
-
|
|
1704
|
-
**Variance Calculation**:
|
|
1705
|
-
|
|
1706
|
-
```typescript
|
|
1707
|
-
function calculateVariance(expected: TransferItem[], actual: TransferItem[]): VarianceDetails {
|
|
1708
|
-
let totalExpected = 0;
|
|
1709
|
-
let totalActual = 0;
|
|
1710
|
-
const varianceItems = [];
|
|
1711
|
-
|
|
1712
|
-
for (const expectedItem of expected) {
|
|
1713
|
-
const actualItem = actual.find(i => i.skuRef === expectedItem.skuRef);
|
|
1714
|
-
const expectedQty = expectedItem.requestedQty;
|
|
1715
|
-
const actualQty = actualItem?.actualQty ?? expectedQty;
|
|
1716
|
-
|
|
1717
|
-
totalExpected += expectedQty;
|
|
1718
|
-
totalActual += actualQty;
|
|
1719
|
-
|
|
1720
|
-
if (expectedQty !== actualQty) {
|
|
1721
|
-
varianceItems.push({
|
|
1722
|
-
skuRef: expectedItem.skuRef,
|
|
1723
|
-
expected: expectedQty,
|
|
1724
|
-
actual: actualQty,
|
|
1725
|
-
difference: actualQty - expectedQty,
|
|
1726
|
-
reason: actualItem?.notes,
|
|
1727
|
-
});
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
const variancePercentage = Math.abs((totalActual - totalExpected) / totalExpected) * 100;
|
|
1732
|
-
|
|
1733
|
-
return {
|
|
1734
|
-
hasVariance: varianceItems.length > 0,
|
|
1735
|
-
variancePercentage,
|
|
1736
|
-
requiresApproval: variancePercentage > 5,
|
|
1737
|
-
items: varianceItems,
|
|
1738
|
-
};
|
|
1739
|
-
}
|
|
1740
|
-
```
|
|
1741
|
-
|
|
1742
|
-
**Approval Thresholds**:
|
|
1743
|
-
|
|
1744
|
-
- **< 5% variance**: Auto-approve (minor discrepancies expected)
|
|
1745
|
-
- **> 5% variance**: Require manual approval
|
|
1746
|
-
- **Email notification**: Sent to warehouse manager
|
|
1747
|
-
- **Approval webhook**: `/approve-variance` endpoint
|
|
1748
|
-
|
|
1749
|
-
### Pattern 4: Rollback on Failure
|
|
1750
|
-
|
|
1751
|
-
**When to Rollback**:
|
|
1752
|
-
|
|
1753
|
-
- Receipt mutation fails
|
|
1754
|
-
- Variance rejected by manager
|
|
1755
|
-
- Destination location unavailable
|
|
1756
|
-
- Critical validation errors
|
|
1757
|
-
|
|
1758
|
-
**Rollback Implementation**:
|
|
1759
|
-
|
|
1760
|
-
```typescript
|
|
1761
|
-
async function rollbackTransfer(
|
|
1762
|
-
client: any,
|
|
1763
|
-
transferState: TransferState,
|
|
1764
|
-
log: Logger
|
|
1765
|
-
): Promise<void> {
|
|
1766
|
-
// Create reverse dispatch mutation (add back to source)
|
|
1767
|
-
const rollbackPayload = {
|
|
1768
|
-
input: {
|
|
1769
|
-
ref: `ROLLBACK-${transferState.transferId}`,
|
|
1770
|
-
locationRef: originalSourceLocation,
|
|
1771
|
-
items: originalItems.map(item => ({
|
|
1772
|
-
skuRef: item.skuRef,
|
|
1773
|
-
qty: item.requestedQty, // Positive quantity (add back)
|
|
1774
|
-
type: 'ROLLBACK',
|
|
1775
|
-
status: 'AVAILABLE',
|
|
1776
|
-
})),
|
|
1777
|
-
},
|
|
1778
|
-
};
|
|
1779
|
-
|
|
1780
|
-
await client.graphql({
|
|
1781
|
-
query: rollbackMutation,
|
|
1782
|
-
variables: rollbackPayload,
|
|
1783
|
-
});
|
|
1784
|
-
|
|
1785
|
-
// Update state
|
|
1786
|
-
transferState.status = 'ROLLED_BACK';
|
|
1787
|
-
await saveTransferState(kvAdapter, transferState);
|
|
1788
|
-
}
|
|
1789
|
-
```
|
|
1790
|
-
|
|
1791
|
-
**Rollback Guarantees**:
|
|
1792
|
-
|
|
1793
|
-
- Inventory returned to source location
|
|
1794
|
-
- Audit trail preserved (ROLLBACK type)
|
|
1795
|
-
- State updated to ROLLED_BACK
|
|
1796
|
-
- Prevents inventory loss
|
|
1797
|
-
|
|
1798
|
-
### Pattern 5: Multi-Leg Transfers (A → B → C)
|
|
1799
|
-
|
|
1800
|
-
For complex transfers through intermediate locations:
|
|
1801
|
-
|
|
1802
|
-
```typescript
|
|
1803
|
-
interface MultiLegTransfer {
|
|
1804
|
-
transferId: string;
|
|
1805
|
-
legs: TransferLeg[];
|
|
1806
|
-
currentLeg: number;
|
|
1807
|
-
overallStatus: string;
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
interface TransferLeg {
|
|
1811
|
-
legId: string;
|
|
1812
|
-
sourceLocationRef: string;
|
|
1813
|
-
destinationLocationRef: string;
|
|
1814
|
-
status: string;
|
|
1815
|
-
dispatchedAt?: string;
|
|
1816
|
-
receivedAt?: string;
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
async function processMultiLegTransfer(
|
|
1820
|
-
transfer: MultiLegTransfer,
|
|
1821
|
-
client: any,
|
|
1822
|
-
log: Logger
|
|
1823
|
-
): Promise<void> {
|
|
1824
|
-
for (let i = transfer.currentLeg; i < transfer.legs.length; i++) {
|
|
1825
|
-
const leg = transfer.legs[i];
|
|
1826
|
-
|
|
1827
|
-
// Dispatch from current location
|
|
1828
|
-
await createDispatchMutation(
|
|
1829
|
-
client,
|
|
1830
|
-
{
|
|
1831
|
-
...transfer,
|
|
1832
|
-
sourceLocationRef: leg.sourceLocationRef,
|
|
1833
|
-
destinationLocationRef: leg.destinationLocationRef,
|
|
1834
|
-
},
|
|
1835
|
-
log
|
|
1836
|
-
);
|
|
1837
|
-
|
|
1838
|
-
// Wait for receipt confirmation (via webhook)
|
|
1839
|
-
// State management tracks current leg
|
|
1840
|
-
transfer.currentLeg = i;
|
|
1841
|
-
await saveTransferState(kvAdapter, transfer);
|
|
1842
|
-
|
|
1843
|
-
// Receipt confirmation advances to next leg
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
transfer.overallStatus = 'COMPLETED';
|
|
1847
|
-
await saveTransferState(kvAdapter, transfer);
|
|
1848
|
-
}
|
|
1849
|
-
```
|
|
1850
|
-
|
|
1851
|
-
---
|
|
1852
|
-
|
|
1853
|
-
## Testing
|
|
1854
|
-
|
|
1855
|
-
### 1. Test Dispatch Request
|
|
1856
|
-
|
|
1857
|
-
```bash
|
|
1858
|
-
curl -X POST https://your-workspace.versori.run/initiate-transfer \
|
|
1859
|
-
-H "Content-Type: application/json" \
|
|
1860
|
-
-d '{
|
|
1861
|
-
"transferId": "TEST-001",
|
|
1862
|
-
"sourceLocationRef": "WH-TEST",
|
|
1863
|
-
"destinationLocationRef": "STORE-TEST",
|
|
1864
|
-
"requestedDate": "2025-01-15T10:00:00Z",
|
|
1865
|
-
"items": [
|
|
1866
|
-
{
|
|
1867
|
-
"skuRef": "TEST-SKU-001",
|
|
1868
|
-
"requestedQty": 10
|
|
1869
|
-
}
|
|
1870
|
-
]
|
|
1871
|
-
}'
|
|
1872
|
-
```
|
|
1873
|
-
|
|
1874
|
-
Expected response:
|
|
1875
|
-
|
|
1876
|
-
```json
|
|
1877
|
-
{
|
|
1878
|
-
"success": true,
|
|
1879
|
-
"transferId": "TEST-001",
|
|
1880
|
-
"status": "IN_TRANSIT",
|
|
1881
|
-
"dispatchMutationId": "12345",
|
|
1882
|
-
"message": "Transfer dispatched successfully, awaiting receipt confirmation"
|
|
1883
|
-
}
|
|
1884
|
-
```
|
|
1885
|
-
|
|
1886
|
-
### 2. Test Receipt Confirmation (No Variance)
|
|
1887
|
-
|
|
1888
|
-
```bash
|
|
1889
|
-
curl -X POST https://your-workspace.versori.run/receive-transfer \
|
|
1890
|
-
-H "Content-Type: application/json" \
|
|
1891
|
-
-d '{
|
|
1892
|
-
"transferId": "TEST-001",
|
|
1893
|
-
"receivedDate": "2025-01-17T14:00:00Z",
|
|
1894
|
-
"items": [
|
|
1895
|
-
{
|
|
1896
|
-
"skuRef": "TEST-SKU-001",
|
|
1897
|
-
"requestedQty": 10,
|
|
1898
|
-
"actualQty": 10
|
|
1899
|
-
}
|
|
1900
|
-
]
|
|
1901
|
-
}'
|
|
1902
|
-
```
|
|
1903
|
-
|
|
1904
|
-
Expected response:
|
|
1905
|
-
|
|
1906
|
-
```json
|
|
1907
|
-
{
|
|
1908
|
-
"success": true,
|
|
1909
|
-
"status": "RECEIVED",
|
|
1910
|
-
"transferId": "TEST-001",
|
|
1911
|
-
"receiptMutationId": "12346"
|
|
1912
|
-
}
|
|
1913
|
-
```
|
|
1914
|
-
|
|
1915
|
-
### 3. Test Receipt with Variance
|
|
1916
|
-
|
|
1917
|
-
```bash
|
|
1918
|
-
curl -X POST https://your-workspace.versori.run/receive-transfer \
|
|
1919
|
-
-H "Content-Type: application/json" \
|
|
1920
|
-
-d '{
|
|
1921
|
-
"transferId": "TEST-002",
|
|
1922
|
-
"receivedDate": "2025-01-17T14:00:00Z",
|
|
1923
|
-
"items": [
|
|
1924
|
-
{
|
|
1925
|
-
"skuRef": "TEST-SKU-001",
|
|
1926
|
-
"requestedQty": 100,
|
|
1927
|
-
"actualQty": 90,
|
|
1928
|
-
"notes": "10 units damaged"
|
|
1929
|
-
}
|
|
1930
|
-
]
|
|
1931
|
-
}'
|
|
1932
|
-
```
|
|
1933
|
-
|
|
1934
|
-
Expected response (variance > 5%):
|
|
1935
|
-
|
|
1936
|
-
```json
|
|
1937
|
-
{
|
|
1938
|
-
"success": true,
|
|
1939
|
-
"status": "PENDING_APPROVAL",
|
|
1940
|
-
"transferId": "TEST-002",
|
|
1941
|
-
"variance": {
|
|
1942
|
-
"hasVariance": true,
|
|
1943
|
-
"variancePercentage": 10,
|
|
1944
|
-
"requiresApproval": true,
|
|
1945
|
-
"items": [
|
|
1946
|
-
{
|
|
1947
|
-
"skuRef": "TEST-SKU-001",
|
|
1948
|
-
"expected": 100,
|
|
1949
|
-
"actual": 90,
|
|
1950
|
-
"difference": -10,
|
|
1951
|
-
"reason": "10 units damaged"
|
|
1952
|
-
}
|
|
1953
|
-
]
|
|
1954
|
-
},
|
|
1955
|
-
"message": "Variance requires manual approval"
|
|
1956
|
-
}
|
|
1957
|
-
```
|
|
1958
|
-
|
|
1959
|
-
---
|
|
1960
|
-
|
|
1961
|
-
## Common Issues & Solutions
|
|
1962
|
-
|
|
1963
|
-
### Issue 1: Duplicate Transfer Processing
|
|
1964
|
-
|
|
1965
|
-
**Symptoms:**
|
|
1966
|
-
|
|
1967
|
-
- Same transfer processed multiple times
|
|
1968
|
-
- Double dispatch/receipt
|
|
1969
|
-
|
|
1970
|
-
**Root Cause:**
|
|
1971
|
-
|
|
1972
|
-
- Lock not acquired
|
|
1973
|
-
- Transfer state not checked
|
|
1974
|
-
|
|
1975
|
-
**Solution:**
|
|
1976
|
-
|
|
1977
|
-
```typescript
|
|
1978
|
-
// Always check existing state before processing
|
|
1979
|
-
const existingState = await getTransferState(kvAdapter, transferId);
|
|
1980
|
-
if (existingState && existingState.status !== 'FAILED') {
|
|
1981
|
-
log.warn('Transfer already processed', { transferId, status: existingState.status });
|
|
1982
|
-
return { success: false, error: 'Already processed' };
|
|
1983
|
-
}
|
|
1984
|
-
|
|
1985
|
-
// Always acquire lock
|
|
1986
|
-
const lockAcquired = await stateService.acquireLock(lockName, kvAdapter, 15);
|
|
1987
|
-
if (!lockAcquired) {
|
|
1988
|
-
throw new Error('Could not acquire lock');
|
|
1989
|
-
}
|
|
1990
|
-
```
|
|
1991
|
-
|
|
1992
|
-
### Issue 2: Variance Not Detected
|
|
1993
|
-
|
|
1994
|
-
**Symptoms:**
|
|
1995
|
-
|
|
1996
|
-
- Receipts with different quantities not flagged
|
|
1997
|
-
- No approval required
|
|
1998
|
-
|
|
1999
|
-
**Root Cause:**
|
|
2000
|
-
|
|
2001
|
-
- Variance calculation logic incorrect
|
|
2002
|
-
- Percentage threshold too high
|
|
2003
|
-
|
|
2004
|
-
**Solution:**
|
|
2005
|
-
|
|
2006
|
-
```typescript
|
|
2007
|
-
// Ensure variance calculation includes all items
|
|
2008
|
-
function calculateVariance(expected: TransferItem[], actual: TransferItem[]): VarianceDetails {
|
|
2009
|
-
// Check EVERY item, not just first mismatch
|
|
2010
|
-
for (const expectedItem of expected) {
|
|
2011
|
-
const actualItem = actual.find(i => i.skuRef === expectedItem.skuRef);
|
|
2012
|
-
if (!actualItem) {
|
|
2013
|
-
// Item completely missing
|
|
2014
|
-
varianceItems.push({
|
|
2015
|
-
skuRef: expectedItem.skuRef,
|
|
2016
|
-
expected: expectedItem.requestedQty,
|
|
2017
|
-
actual: 0,
|
|
2018
|
-
difference: -expectedItem.requestedQty,
|
|
2019
|
-
reason: 'Item not received',
|
|
2020
|
-
});
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
// Calculate percentage correctly
|
|
2025
|
-
const variancePercentage =
|
|
2026
|
-
totalExpected > 0 ? Math.abs((totalActual - totalExpected) / totalExpected) * 100 : 0;
|
|
2027
|
-
|
|
2028
|
-
return {
|
|
2029
|
-
hasVariance: varianceItems.length > 0,
|
|
2030
|
-
variancePercentage,
|
|
2031
|
-
requiresApproval: variancePercentage > 5, // Adjust threshold as needed
|
|
2032
|
-
items: varianceItems,
|
|
2033
|
-
};
|
|
2034
|
-
}
|
|
2035
|
-
```
|
|
2036
|
-
|
|
2037
|
-
### Issue 3: Rollback Fails
|
|
2038
|
-
|
|
2039
|
-
**Symptoms:**
|
|
2040
|
-
|
|
2041
|
-
- Transfer marked as ROLLED_BACK but inventory not restored
|
|
2042
|
-
- GraphQL mutation errors
|
|
2043
|
-
|
|
2044
|
-
**Root Cause:**
|
|
2045
|
-
|
|
2046
|
-
- Original request data not stored in state
|
|
2047
|
-
- Rollback mutation invalid
|
|
2048
|
-
|
|
2049
|
-
**Solution:**
|
|
2050
|
-
|
|
2051
|
-
```typescript
|
|
2052
|
-
// Store original request when creating transfer state
|
|
2053
|
-
const transferState: TransferState = {
|
|
2054
|
-
transferId,
|
|
2055
|
-
status: 'PENDING',
|
|
2056
|
-
originalRequest: transferRequest, // Store for rollback
|
|
2057
|
-
// ... other fields
|
|
2058
|
-
};
|
|
2059
|
-
await saveTransferState(kvAdapter, transferState);
|
|
2060
|
-
|
|
2061
|
-
// Validate rollback payload before sending
|
|
2062
|
-
async function rollbackTransfer(client: any, transferState: TransferState, log: Logger) {
|
|
2063
|
-
const originalRequest = transferState.originalRequest;
|
|
2064
|
-
if (!originalRequest) {
|
|
2065
|
-
throw new Error('Cannot rollback: original request data missing');
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
// Validate items exist
|
|
2069
|
-
if (!originalRequest.items || originalRequest.items.length === 0) {
|
|
2070
|
-
throw new Error('Cannot rollback: no items in original request');
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
// Create rollback mutation...
|
|
2074
|
-
}
|
|
2075
|
-
```
|
|
2076
|
-
|
|
2077
|
-
### Issue 4: Email Notifications Not Sent
|
|
2078
|
-
|
|
2079
|
-
**Symptoms:**
|
|
2080
|
-
|
|
2081
|
-
- No emails for variance or completion
|
|
2082
|
-
- Silent failures
|
|
2083
|
-
|
|
2084
|
-
**Root Cause:**
|
|
2085
|
-
|
|
2086
|
-
- Email service not configured
|
|
2087
|
-
- Missing activation variables
|
|
2088
|
-
- Email connector not connected
|
|
2089
|
-
|
|
2090
|
-
**Solution:**
|
|
2091
|
-
|
|
2092
|
-
```typescript
|
|
2093
|
-
async function sendVarianceNotification(
|
|
2094
|
-
transferId: string,
|
|
2095
|
-
variance: VarianceDetails,
|
|
2096
|
-
activation: any,
|
|
2097
|
-
log: any
|
|
2098
|
-
): Promise<void> {
|
|
2099
|
-
const emailTo = activation.getVariable('varianceNotificationEmail') as string;
|
|
2100
|
-
|
|
2101
|
-
if (!emailTo) {
|
|
2102
|
-
log.warn('Variance notification email not configured', { transferId });
|
|
2103
|
-
return; // Don't fail transfer if email not configured
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
try {
|
|
2107
|
-
// Send email via Versori connector or external service
|
|
2108
|
-
// Example: SendGrid, AWS SES, etc.
|
|
2109
|
-
await sendEmail({
|
|
2110
|
-
to: emailTo,
|
|
2111
|
-
subject: `Transfer Variance Requires Approval: ${transferId}`,
|
|
2112
|
-
body: formatVarianceEmail(transferId, variance),
|
|
2113
|
-
});
|
|
2114
|
-
|
|
2115
|
-
log.info('Variance notification sent', { transferId, emailTo });
|
|
2116
|
-
} catch (error: any) {
|
|
2117
|
-
// Log error but don't fail transfer
|
|
2118
|
-
log.error('Failed to send variance notification', error, { transferId });
|
|
2119
|
-
}
|
|
2120
|
-
}
|
|
2121
|
-
```
|
|
2122
|
-
|
|
2123
|
-
---
|
|
2124
|
-
|
|
2125
|
-
## Related Guides
|
|
2126
|
-
|
|
2127
|
-
- **02-scheduled-csv-inventory.md** - CSV processing patterns
|
|
2128
|
-
- **03-kv-state-management.md** - State tracking and locking
|
|
2129
|
-
- **04-webhook-xml-response.md** - Webhook response patterns
|
|
2130
|
-
- **GraphQL Mutation Mapping**: `../../02-CORE-GUIDES/mapping/graphql-mutation-mapping/`
|
|
2131
|
-
- **Universal Mapping**: `../../02-CORE-GUIDES/mapping/readme.md`
|
|
2132
|
-
|
|
2133
|
-
---
|
|
2134
|
-
|
|
2135
|
-
## Summary
|
|
2136
|
-
|
|
2137
|
-
This guide demonstrated inter-location transfer processing with:
|
|
2138
|
-
|
|
2139
|
-
1. **Paired Mutations**: Dispatch and receipt for audit trail
|
|
2140
|
-
2. **In-Transit Tracking**: VersoriKV state management
|
|
2141
|
-
3. **Variance Handling**: Automatic approval thresholds and manual review
|
|
2142
|
-
4. **Rollback Logic**: Restore inventory on failure
|
|
2143
|
-
5. **Multi-Format Support**: JSON webhooks and CSV scheduled imports
|
|
2144
|
-
6. **Email Notifications**: Variance alerts and completion notices
|
|
2145
|
-
|
|
2146
|
-
**Key Takeaways**:
|
|
2147
|
-
|
|
2148
|
-
- Use paired mutations for complete audit trail
|
|
2149
|
-
- Track transfer state in VersoriKV for visibility
|
|
2150
|
-
- Implement variance thresholds (e.g., 5%) for auto-approval
|
|
2151
|
-
- Always store original request for rollback capability
|
|
2152
|
-
- Use distributed locking to prevent duplicate processing
|
|
2153
|
-
- Handle both webhook and scheduled triggers
|
|
2154
|
-
|
|
2155
|
-
**Production Considerations**:
|
|
2156
|
-
|
|
2157
|
-
- Monitor variance approval queue
|
|
2158
|
-
- Set up alerts for stuck transfers
|
|
2159
|
-
- Track average transit times
|
|
2160
|
-
- Implement retry logic for transient failures
|
|
2161
|
-
- Add comprehensive logging for debugging
|
|
2162
|
-
- Test rollback scenarios thoroughly
|
|
1
|
+
# Versori Inter-Location Transfer Processing
|
|
2
|
+
|
|
3
|
+
**FC Connect SDK Use Case Guide**
|
|
4
|
+
|
|
5
|
+
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
6
|
+
> **Version**: Use latest - `npm install @fluentcommerce/fc-connect-sdk@latest`
|
|
7
|
+
|
|
8
|
+
**Context**: Webhook-triggered or scheduled workflow for processing inventory transfers between warehouses/stores with in-transit tracking
|
|
9
|
+
|
|
10
|
+
**Complexity**: Medium
|
|
11
|
+
|
|
12
|
+
**Runtime**: Versori Platform (Webhook/Scheduled)
|
|
13
|
+
|
|
14
|
+
**Estimated Lines**: ~1,200 lines (modular, production-ready)
|
|
15
|
+
|
|
16
|
+
## What You'll Build
|
|
17
|
+
|
|
18
|
+
- Versori webhook/scheduled workflow for transfer requests
|
|
19
|
+
- CSV and JSON input format support
|
|
20
|
+
- Parse transfer data (source, destination, items, quantities)
|
|
21
|
+
- Create paired dispatch/receipt mutations (GraphQL)
|
|
22
|
+
- In-transit inventory tracking with VersoriKV
|
|
23
|
+
- Variance handling (damaged items, shortages)
|
|
24
|
+
- Rollback logic on failure
|
|
25
|
+
- Email notifications on completion
|
|
26
|
+
- Approval workflow for variances > 5%
|
|
27
|
+
|
|
28
|
+
## SDK Methods Used
|
|
29
|
+
|
|
30
|
+
- `createClient(ctx)` - Auto-detects Versori context
|
|
31
|
+
- `GraphQLMutationMapper(config, logger, { fluentClient, customResolvers })` - Map transfers to GraphQL mutations
|
|
32
|
+
- `mapper.mapWithNodes(sourceData)` - Transform transfer data (resolvers from constructor)
|
|
33
|
+
- `client.graphql(payload)` - Execute mutations
|
|
34
|
+
- `VersoriKVAdapter(openKv())` - State management
|
|
35
|
+
- `StateService(logger)` - Distributed locking
|
|
36
|
+
- `UniversalMapper(mappingConfig)` - CSV field mapping
|
|
37
|
+
- `CSVParserService()` - Parse CSV transfer files
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Versori Workflows Structure
|
|
42
|
+
|
|
43
|
+
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
44
|
+
|
|
45
|
+
**Trigger Types:**
|
|
46
|
+
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
47
|
+
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
48
|
+
- **`http()`** → External API calls (chained from webhook/schedule)
|
|
49
|
+
- **`fn()`** → Internal processing (chained from webhook/schedule)
|
|
50
|
+
|
|
51
|
+
### Recommended Project Structure
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
inter-location-transfers/
|
|
55
|
+
├── index.ts # Entry point - registers all workflows
|
|
56
|
+
└── src/
|
|
57
|
+
├── workflows/
|
|
58
|
+
│ ├── webhook/
|
|
59
|
+
│ │ ├── initiate-transfer.ts # Webhook: Process transfer requests
|
|
60
|
+
│ │ ├── receive-transfer.ts # Webhook: Receipt confirmation
|
|
61
|
+
│ │ └── export-transfer-report.ts # Webhook: Export reports
|
|
62
|
+
│ │
|
|
63
|
+
│ └── scheduled/
|
|
64
|
+
│ └── reconcile-transfers.ts # Scheduled: Daily reconciliation
|
|
65
|
+
│
|
|
66
|
+
├── services/
|
|
67
|
+
│ └── transfer.service.ts # Shared orchestration logic (reusable)
|
|
68
|
+
│
|
|
69
|
+
└── config/
|
|
70
|
+
└── transfer-mapping.json # Mapping configuration
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Benefits:**
|
|
74
|
+
- ✅ Clear trigger separation (`webhook/` vs `scheduled/`)
|
|
75
|
+
- ✅ Descriptive file names (easy to browse and understand)
|
|
76
|
+
- ✅ Scalable (add new workflows without cluttering)
|
|
77
|
+
- ✅ Reusable code in `services/` (DRY principle)
|
|
78
|
+
- ✅ Easy to modify individual workflows without affecting others
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Use Case Profile
|
|
83
|
+
|
|
84
|
+
- **Volume**: Medium (50-500 transfers/day)
|
|
85
|
+
- **Latency**: Near real-time (< 5 minutes)
|
|
86
|
+
- **Complexity**: Medium (paired mutations, state tracking, variance handling)
|
|
87
|
+
- **Pattern**: Webhook/scheduled → parse → dispatch mutation → receipt mutation → state tracking
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Complete Working Code
|
|
92
|
+
|
|
93
|
+
### 1. Entry Point: `index.ts`
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
/**
|
|
97
|
+
* Entry Point - Registers all workflows with Versori platform
|
|
98
|
+
*
|
|
99
|
+
* ⚠️ CRITICAL PATTERN: MemoryInterpreter Setup
|
|
100
|
+
* This pattern ensures workflows execute in the correct Versori runtime context
|
|
101
|
+
* and prevents "unexpected export" errors during deployment.
|
|
102
|
+
*
|
|
103
|
+
* COMPONENTS:
|
|
104
|
+
* 1. Import ALL workflows (scheduled, webhook, etc.)
|
|
105
|
+
* 2. Create MemoryInterpreter with workflow array
|
|
106
|
+
* 3. NO direct exports - interpreter handles workflow registration
|
|
107
|
+
*
|
|
108
|
+
* File Structure:
|
|
109
|
+
* - src/workflows/scheduled/ → Time-based triggers (cron)
|
|
110
|
+
* - src/workflows/webhook/ → HTTP-based triggers (webhooks)
|
|
111
|
+
*/
|
|
112
|
+
|
|
113
|
+
import { MemoryInterpreter } from '@versori/run';
|
|
114
|
+
|
|
115
|
+
// Import webhook workflows
|
|
116
|
+
import { initiateTransfer } from './src/workflows/webhook/initiate-transfer';
|
|
117
|
+
import { receiveTransfer } from './src/workflows/webhook/receive-transfer';
|
|
118
|
+
import { exportTransferReport } from './src/workflows/webhook/export-transfer-report';
|
|
119
|
+
|
|
120
|
+
// Import scheduled workflows
|
|
121
|
+
import { reconcileTransfers } from './src/workflows/scheduled/reconcile-transfers';
|
|
122
|
+
|
|
123
|
+
// ✅ CORRECT: Register via MemoryInterpreter (Versori standard)
|
|
124
|
+
new MemoryInterpreter([
|
|
125
|
+
// Webhooks (HTTP-based triggers)
|
|
126
|
+
initiateTransfer,
|
|
127
|
+
receiveTransfer,
|
|
128
|
+
exportTransferReport,
|
|
129
|
+
|
|
130
|
+
// Scheduled (time-based triggers)
|
|
131
|
+
reconcileTransfers,
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
// ❌ WRONG: Direct exports cause "unexpected export" errors
|
|
135
|
+
// export { initiateTransfer, receiveTransfer, reconcileTransfers };
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* What Gets Exposed:
|
|
139
|
+
* ✅ initiateTransfer → https://{workspace}.versori.run/initiate-transfer
|
|
140
|
+
* ✅ receiveTransfer → https://{workspace}.versori.run/receive-transfer
|
|
141
|
+
* ✅ exportTransferReport → https://{workspace}.versori.run/export-transfer-report
|
|
142
|
+
* ❌ reconcileTransfers → NOT exposed (runs automatically on cron)
|
|
143
|
+
*/
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### 2. Service File: `src/services/transfer.service.ts`
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
/**
|
|
152
|
+
* Transfer Service - Shared orchestration logic
|
|
153
|
+
*
|
|
154
|
+
* This service provides reusable business logic for transfer processing:
|
|
155
|
+
* - Dispatch mutations (remove from source)
|
|
156
|
+
* - Receipt mutations (add to destination)
|
|
157
|
+
* - Variance calculation and approval
|
|
158
|
+
* - Rollback logic on failure
|
|
159
|
+
* - State management with VersoriKV
|
|
160
|
+
* - Distributed locking for concurrency
|
|
161
|
+
*
|
|
162
|
+
* All business logic is centralized here - workflows just delegate to service functions.
|
|
163
|
+
*/
|
|
164
|
+
|
|
165
|
+
import { Buffer } from 'node:buffer'; // Required for Versori/Deno
|
|
166
|
+
import {
|
|
167
|
+
createClient,
|
|
168
|
+
GraphQLMutationMapper,
|
|
169
|
+
UniversalMapper,
|
|
170
|
+
VersoriKVAdapter,
|
|
171
|
+
StateService,
|
|
172
|
+
CSVParserService,
|
|
173
|
+
S3DataSource,
|
|
174
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
175
|
+
|
|
176
|
+
import type {
|
|
177
|
+
FluentClient,
|
|
178
|
+
Logger,
|
|
179
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Transfer data interfaces
|
|
183
|
+
*/
|
|
184
|
+
export interface TransferRequest {
|
|
185
|
+
transferId: string;
|
|
186
|
+
sourceLocationRef: string;
|
|
187
|
+
destinationLocationRef: string;
|
|
188
|
+
requestedDate: string;
|
|
189
|
+
expectedDeliveryDate?: string;
|
|
190
|
+
items: TransferItem[];
|
|
191
|
+
metadata?: {
|
|
192
|
+
reason?: string;
|
|
193
|
+
priority?: string;
|
|
194
|
+
requestedBy?: string;
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface TransferItem {
|
|
199
|
+
skuRef: string;
|
|
200
|
+
requestedQty: number;
|
|
201
|
+
actualQty?: number; // For receipt confirmation
|
|
202
|
+
condition?: string;
|
|
203
|
+
notes?: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface TransferState {
|
|
207
|
+
transferId: string;
|
|
208
|
+
status: 'PENDING' | 'DISPATCHED' | 'IN_TRANSIT' | 'RECEIVED' | 'FAILED' | 'ROLLED_BACK';
|
|
209
|
+
dispatchMutationId?: string;
|
|
210
|
+
receiptMutationId?: string;
|
|
211
|
+
dispatchedAt?: string;
|
|
212
|
+
receivedAt?: string;
|
|
213
|
+
variance?: VarianceDetails;
|
|
214
|
+
originalRequest?: TransferRequest;
|
|
215
|
+
receiptData?: any;
|
|
216
|
+
createdAt: string;
|
|
217
|
+
updatedAt: string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface VarianceDetails {
|
|
221
|
+
hasVariance: boolean;
|
|
222
|
+
variancePercentage: number;
|
|
223
|
+
requiresApproval: boolean;
|
|
224
|
+
items: {
|
|
225
|
+
skuRef: string;
|
|
226
|
+
expected: number;
|
|
227
|
+
actual: number;
|
|
228
|
+
difference: number;
|
|
229
|
+
reason?: string;
|
|
230
|
+
}[];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface TransferResult {
|
|
234
|
+
success: boolean;
|
|
235
|
+
transferId?: string;
|
|
236
|
+
status?: string;
|
|
237
|
+
dispatchMutationId?: string;
|
|
238
|
+
receiptMutationId?: string;
|
|
239
|
+
variance?: VarianceDetails;
|
|
240
|
+
error?: string;
|
|
241
|
+
currentStatus?: string;
|
|
242
|
+
message?: string;
|
|
243
|
+
recommendation?: string;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Process transfer request (dispatch from source)
|
|
248
|
+
*
|
|
249
|
+
* This function orchestrates the complete transfer dispatch:
|
|
250
|
+
* 1. Client initialization with retailerId
|
|
251
|
+
* 2. Lock acquisition for concurrency control
|
|
252
|
+
* 3. Transfer state initialization
|
|
253
|
+
* 4. Dispatch mutation creation
|
|
254
|
+
* 5. State updates and cleanup
|
|
255
|
+
*
|
|
256
|
+
* @param ctx - Versori context object
|
|
257
|
+
* @param transferRequest - Transfer request data
|
|
258
|
+
*/
|
|
259
|
+
export async function processTransferRequest(
|
|
260
|
+
ctx: any,
|
|
261
|
+
transferRequest: TransferRequest
|
|
262
|
+
): Promise<TransferResult> {
|
|
263
|
+
const { log, activation, openKv } = ctx;
|
|
264
|
+
const startTime = Date.now();
|
|
265
|
+
|
|
266
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
267
|
+
log.info('🚀 TRANSFER REQUEST PROCESSING STARTED', {
|
|
268
|
+
transferId: transferRequest.transferId,
|
|
269
|
+
sourceLocation: transferRequest.sourceLocationRef,
|
|
270
|
+
destinationLocation: transferRequest.destinationLocationRef,
|
|
271
|
+
itemCount: transferRequest.items.length,
|
|
272
|
+
});
|
|
273
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
// Initialize client with retailerId and validateConnection
|
|
277
|
+
log.info('🔧 Initializing Fluent Commerce client...');
|
|
278
|
+
const client = await createClient({ ...ctx, validateConnection: true });
|
|
279
|
+
const fluentRetailerId = activation.getVariable('fluentRetailerId');
|
|
280
|
+
|
|
281
|
+
if (!fluentRetailerId) {
|
|
282
|
+
log.error('❌ Missing required fluentRetailerId activation variable');
|
|
283
|
+
return {
|
|
284
|
+
success: false,
|
|
285
|
+
error: 'fluentRetailerId activation variable is required',
|
|
286
|
+
recommendation: 'Please configure fluentRetailerId in the Activation Variables section',
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
client.setRetailerId(fluentRetailerId);
|
|
291
|
+
log.info('✅ Client initialized', { retailerId: fluentRetailerId });
|
|
292
|
+
|
|
293
|
+
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
294
|
+
const stateService = new StateService(log);
|
|
295
|
+
|
|
296
|
+
// Check if transfer already processed
|
|
297
|
+
log.info('🔍 Checking transfer state...');
|
|
298
|
+
const existingState = await getTransferState(kvAdapter, transferRequest.transferId);
|
|
299
|
+
|
|
300
|
+
if (existingState && existingState.status !== 'FAILED') {
|
|
301
|
+
log.warn('⚠️ Transfer already processed', {
|
|
302
|
+
transferId: transferRequest.transferId,
|
|
303
|
+
status: existingState.status,
|
|
304
|
+
});
|
|
305
|
+
return {
|
|
306
|
+
success: false,
|
|
307
|
+
error: 'Transfer already processed',
|
|
308
|
+
transferId: transferRequest.transferId,
|
|
309
|
+
currentStatus: existingState.status,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Acquire lock to prevent concurrent processing
|
|
314
|
+
log.info('🔒 Acquiring transfer lock...');
|
|
315
|
+
const lockName = `transfer-lock:${transferRequest.transferId}`;
|
|
316
|
+
const lockAcquired = await stateService.acquireLock(lockName, kvAdapter, 15);
|
|
317
|
+
|
|
318
|
+
if (!lockAcquired) {
|
|
319
|
+
log.warn('⚠️ Could not acquire lock - transfer already being processed');
|
|
320
|
+
return {
|
|
321
|
+
success: false,
|
|
322
|
+
error: 'Transfer is already being processed',
|
|
323
|
+
transferId: transferRequest.transferId,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
// Create initial transfer state
|
|
329
|
+
log.info('📝 Initializing transfer state...');
|
|
330
|
+
const transferState: TransferState = {
|
|
331
|
+
transferId: transferRequest.transferId,
|
|
332
|
+
status: 'PENDING',
|
|
333
|
+
originalRequest: transferRequest,
|
|
334
|
+
createdAt: new Date().toISOString(),
|
|
335
|
+
updatedAt: new Date().toISOString(),
|
|
336
|
+
};
|
|
337
|
+
await saveTransferState(kvAdapter, transferState);
|
|
338
|
+
log.info('✅ Transfer state initialized');
|
|
339
|
+
|
|
340
|
+
// Step 1: Create dispatch mutation (remove from source)
|
|
341
|
+
log.info('📦 Creating dispatch mutation...');
|
|
342
|
+
const dispatchStartTime = Date.now();
|
|
343
|
+
const dispatchResult = await createDispatchMutation(client, transferRequest, log);
|
|
344
|
+
|
|
345
|
+
if (!dispatchResult.success) {
|
|
346
|
+
throw new Error(`Dispatch failed: ${dispatchResult.error}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const dispatchDuration = Date.now() - dispatchStartTime;
|
|
350
|
+
log.info('✅ Dispatch mutation created', {
|
|
351
|
+
mutationId: dispatchResult.mutationId,
|
|
352
|
+
duration: `${dispatchDuration}ms`,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Update state with dispatch info
|
|
356
|
+
transferState.status = 'DISPATCHED';
|
|
357
|
+
transferState.dispatchMutationId = dispatchResult.mutationId;
|
|
358
|
+
transferState.dispatchedAt = new Date().toISOString();
|
|
359
|
+
transferState.updatedAt = new Date().toISOString();
|
|
360
|
+
await saveTransferState(kvAdapter, transferState);
|
|
361
|
+
|
|
362
|
+
// Step 2: Mark as in-transit
|
|
363
|
+
transferState.status = 'IN_TRANSIT';
|
|
364
|
+
transferState.updatedAt = new Date().toISOString();
|
|
365
|
+
await saveTransferState(kvAdapter, transferState);
|
|
366
|
+
log.info('✅ Transfer marked as in-transit');
|
|
367
|
+
|
|
368
|
+
const totalDuration = Date.now() - startTime;
|
|
369
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
370
|
+
log.info('✅ TRANSFER REQUEST COMPLETED', {
|
|
371
|
+
transferId: transferRequest.transferId,
|
|
372
|
+
status: 'IN_TRANSIT',
|
|
373
|
+
duration: `${totalDuration}ms`,
|
|
374
|
+
});
|
|
375
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
376
|
+
|
|
377
|
+
// For webhook trigger, return success after dispatch
|
|
378
|
+
// Receipt will be confirmed via separate webhook
|
|
379
|
+
return {
|
|
380
|
+
success: true,
|
|
381
|
+
transferId: transferRequest.transferId,
|
|
382
|
+
status: 'IN_TRANSIT',
|
|
383
|
+
dispatchMutationId: dispatchResult.mutationId,
|
|
384
|
+
message: 'Transfer dispatched successfully, awaiting receipt confirmation',
|
|
385
|
+
};
|
|
386
|
+
} finally {
|
|
387
|
+
// Always release lock
|
|
388
|
+
log.info('🔓 Releasing transfer lock...');
|
|
389
|
+
await stateService.releaseLock(lockName, kvAdapter);
|
|
390
|
+
}
|
|
391
|
+
} catch (error: any) {
|
|
392
|
+
const duration = Date.now() - startTime;
|
|
393
|
+
log.error('═══════════════════════════════════════════════════════');
|
|
394
|
+
log.error('❌ TRANSFER REQUEST FAILED', {
|
|
395
|
+
transferId: transferRequest.transferId,
|
|
396
|
+
error: error instanceof Error ? error.message : String(error),
|
|
397
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
398
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
399
|
+
duration: `${duration}ms`,
|
|
400
|
+
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
401
|
+
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
402
|
+
: error.message?.includes('mapping') || error.message?.includes('field')
|
|
403
|
+
? 'Check mapping configuration and verify transfer request payload structure'
|
|
404
|
+
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
405
|
+
? 'Check network connectivity and Fluent Commerce API availability'
|
|
406
|
+
: error.message?.includes('lock') || error.message?.includes('concurrent')
|
|
407
|
+
? 'Concurrent transfer processing detected - retry after lock release'
|
|
408
|
+
: 'Review error details and check transfer request payload structure',
|
|
409
|
+
});
|
|
410
|
+
log.error('═══════════════════════════════════════════════════════');
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
success: false,
|
|
414
|
+
error: error.message,
|
|
415
|
+
transferId: transferRequest.transferId,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Process receipt confirmation (add to destination)
|
|
422
|
+
*/
|
|
423
|
+
export async function processReceiptConfirmation(
|
|
424
|
+
ctx: any,
|
|
425
|
+
receiptData: { transferId: string; receivedDate: string; items: TransferItem[] }
|
|
426
|
+
): Promise<TransferResult> {
|
|
427
|
+
const { log, activation, openKv } = ctx;
|
|
428
|
+
const startTime = Date.now();
|
|
429
|
+
|
|
430
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
431
|
+
log.info('📦 RECEIPT CONFIRMATION STARTED', {
|
|
432
|
+
transferId: receiptData.transferId,
|
|
433
|
+
itemCount: receiptData.items.length,
|
|
434
|
+
});
|
|
435
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
const client = await createClient({ ...ctx, validateConnection: true });
|
|
439
|
+
const fluentRetailerId = activation.getVariable('fluentRetailerId');
|
|
440
|
+
|
|
441
|
+
if (fluentRetailerId) {
|
|
442
|
+
client.setRetailerId(fluentRetailerId);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
446
|
+
const stateService = new StateService(log);
|
|
447
|
+
|
|
448
|
+
// Get transfer state
|
|
449
|
+
log.info('🔍 Retrieving transfer state...');
|
|
450
|
+
const transferState = await getTransferState(kvAdapter, receiptData.transferId);
|
|
451
|
+
|
|
452
|
+
if (!transferState) {
|
|
453
|
+
throw new Error('Transfer not found');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (transferState.status !== 'IN_TRANSIT') {
|
|
457
|
+
throw new Error(`Invalid transfer status: ${transferState.status}`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Acquire lock
|
|
461
|
+
log.info('🔒 Acquiring transfer lock...');
|
|
462
|
+
const lockName = `transfer-lock:${receiptData.transferId}`;
|
|
463
|
+
const lockAcquired = await stateService.acquireLock(lockName, kvAdapter, 15);
|
|
464
|
+
|
|
465
|
+
if (!lockAcquired) {
|
|
466
|
+
throw new Error('Could not acquire lock');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
// Calculate variance
|
|
471
|
+
log.info('🔍 Calculating variance...');
|
|
472
|
+
const variance = calculateVariance(
|
|
473
|
+
transferState.originalRequest?.items || receiptData.items,
|
|
474
|
+
receiptData.items
|
|
475
|
+
);
|
|
476
|
+
transferState.variance = variance;
|
|
477
|
+
transferState.receiptData = receiptData;
|
|
478
|
+
|
|
479
|
+
log.info('📊 Variance calculated', {
|
|
480
|
+
hasVariance: variance.hasVariance,
|
|
481
|
+
variancePercentage: variance.variancePercentage.toFixed(2) + '%',
|
|
482
|
+
requiresApproval: variance.requiresApproval,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Check if variance requires approval (> 5%)
|
|
486
|
+
if (variance.requiresApproval) {
|
|
487
|
+
transferState.status = 'PENDING';
|
|
488
|
+
transferState.updatedAt = new Date().toISOString();
|
|
489
|
+
await saveTransferState(kvAdapter, transferState);
|
|
490
|
+
|
|
491
|
+
log.warn('⚠️ Variance requires approval', {
|
|
492
|
+
variancePercentage: variance.variancePercentage.toFixed(2) + '%',
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// Send email notification
|
|
496
|
+
await sendVarianceNotification(receiptData.transferId, variance, activation, log);
|
|
497
|
+
|
|
498
|
+
const duration = Date.now() - startTime;
|
|
499
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
500
|
+
log.info('⚠️ RECEIPT PENDING APPROVAL', {
|
|
501
|
+
transferId: receiptData.transferId,
|
|
502
|
+
duration: `${duration}ms`,
|
|
503
|
+
});
|
|
504
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
success: true,
|
|
508
|
+
status: 'PENDING_APPROVAL',
|
|
509
|
+
transferId: receiptData.transferId,
|
|
510
|
+
variance,
|
|
511
|
+
message: 'Variance requires manual approval',
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Create receipt mutation (add to destination)
|
|
516
|
+
log.info('📦 Creating receipt mutation...');
|
|
517
|
+
const receiptStartTime = Date.now();
|
|
518
|
+
const receiptResult = await createReceiptMutation(
|
|
519
|
+
client,
|
|
520
|
+
receiptData.transferId,
|
|
521
|
+
receiptData.items,
|
|
522
|
+
transferState,
|
|
523
|
+
log
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
if (!receiptResult.success) {
|
|
527
|
+
throw new Error(`Receipt failed: ${receiptResult.error}`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const receiptDuration = Date.now() - receiptStartTime;
|
|
531
|
+
log.info('✅ Receipt mutation created', {
|
|
532
|
+
mutationId: receiptResult.mutationId,
|
|
533
|
+
duration: `${receiptDuration}ms`,
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// Update state
|
|
537
|
+
transferState.status = 'RECEIVED';
|
|
538
|
+
transferState.receiptMutationId = receiptResult.mutationId;
|
|
539
|
+
transferState.receivedAt = new Date().toISOString();
|
|
540
|
+
transferState.updatedAt = new Date().toISOString();
|
|
541
|
+
await saveTransferState(kvAdapter, transferState);
|
|
542
|
+
|
|
543
|
+
// Send completion notification
|
|
544
|
+
await sendCompletionNotification(receiptData.transferId, transferState, activation, log);
|
|
545
|
+
|
|
546
|
+
const totalDuration = Date.now() - startTime;
|
|
547
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
548
|
+
log.info('✅ RECEIPT CONFIRMATION COMPLETED', {
|
|
549
|
+
transferId: receiptData.transferId,
|
|
550
|
+
duration: `${totalDuration}ms`,
|
|
551
|
+
});
|
|
552
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
success: true,
|
|
556
|
+
status: 'RECEIVED',
|
|
557
|
+
transferId: receiptData.transferId,
|
|
558
|
+
receiptMutationId: receiptResult.mutationId,
|
|
559
|
+
variance: variance.hasVariance ? variance : undefined,
|
|
560
|
+
};
|
|
561
|
+
} finally {
|
|
562
|
+
log.info('🔓 Releasing transfer lock...');
|
|
563
|
+
await stateService.releaseLock(lockName, kvAdapter);
|
|
564
|
+
}
|
|
565
|
+
} catch (error: any) {
|
|
566
|
+
const duration = Date.now() - startTime;
|
|
567
|
+
log.error('═══════════════════════════════════════════════════════');
|
|
568
|
+
log.error('❌ RECEIPT CONFIRMATION FAILED', {
|
|
569
|
+
transferId: receiptData.transferId,
|
|
570
|
+
error: error instanceof Error ? error.message : String(error),
|
|
571
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
572
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
573
|
+
duration: `${duration}ms`,
|
|
574
|
+
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
575
|
+
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
576
|
+
: error.message?.includes('not found') || error.message?.includes('missing')
|
|
577
|
+
? 'Transfer not found - verify transferId and check transfer state in KV store'
|
|
578
|
+
: error.message?.includes('status') || error.message?.includes('invalid')
|
|
579
|
+
? 'Invalid transfer status - ensure transfer is in IN_TRANSIT status'
|
|
580
|
+
: error.message?.includes('lock') || error.message?.includes('concurrent')
|
|
581
|
+
? 'Concurrent receipt processing detected - retry after lock release'
|
|
582
|
+
: error.message?.includes('variance') || error.message?.includes('approval')
|
|
583
|
+
? 'Variance requires approval - check variance calculation and approval workflow'
|
|
584
|
+
: 'Review error details and check receipt confirmation payload structure',
|
|
585
|
+
});
|
|
586
|
+
log.error('═══════════════════════════════════════════════════════');
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
success: false,
|
|
590
|
+
error: error.message,
|
|
591
|
+
transferId: receiptData.transferId,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Process variance approval/rejection
|
|
598
|
+
*/
|
|
599
|
+
export async function processVarianceApproval(
|
|
600
|
+
ctx: any,
|
|
601
|
+
approvalData: { transferId: string; approved: boolean; notes?: string }
|
|
602
|
+
): Promise<TransferResult> {
|
|
603
|
+
const { log, activation, openKv } = ctx;
|
|
604
|
+
const { transferId, approved, notes } = approvalData;
|
|
605
|
+
const startTime = Date.now();
|
|
606
|
+
|
|
607
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
608
|
+
log.info('📝 VARIANCE APPROVAL PROCESSING', {
|
|
609
|
+
transferId,
|
|
610
|
+
approved,
|
|
611
|
+
});
|
|
612
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
613
|
+
|
|
614
|
+
try {
|
|
615
|
+
const client = await createClient({ ...ctx, validateConnection: true });
|
|
616
|
+
const fluentRetailerId = activation.getVariable('fluentRetailerId');
|
|
617
|
+
|
|
618
|
+
if (fluentRetailerId) {
|
|
619
|
+
client.setRetailerId(fluentRetailerId);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
623
|
+
const stateService = new StateService(log);
|
|
624
|
+
|
|
625
|
+
const transferState = await getTransferState(kvAdapter, transferId);
|
|
626
|
+
if (!transferState) {
|
|
627
|
+
throw new Error('Transfer not found');
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const lockName = `transfer-lock:${transferId}`;
|
|
631
|
+
const lockAcquired = await stateService.acquireLock(lockName, kvAdapter, 15);
|
|
632
|
+
if (!lockAcquired) {
|
|
633
|
+
throw new Error('Could not acquire lock');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
if (!approved) {
|
|
638
|
+
// Rollback: reverse the dispatch
|
|
639
|
+
log.info('❌ Variance rejected - rolling back transfer...');
|
|
640
|
+
const rollbackResult = await rollbackTransfer(client, transferState, log);
|
|
641
|
+
|
|
642
|
+
transferState.status = 'ROLLED_BACK';
|
|
643
|
+
transferState.updatedAt = new Date().toISOString();
|
|
644
|
+
await saveTransferState(kvAdapter, transferState);
|
|
645
|
+
|
|
646
|
+
const duration = Date.now() - startTime;
|
|
647
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
648
|
+
log.info('✅ TRANSFER ROLLED BACK', {
|
|
649
|
+
transferId,
|
|
650
|
+
duration: `${duration}ms`,
|
|
651
|
+
});
|
|
652
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
653
|
+
|
|
654
|
+
return {
|
|
655
|
+
success: true,
|
|
656
|
+
status: 'ROLLED_BACK',
|
|
657
|
+
transferId,
|
|
658
|
+
message: 'Transfer rolled back due to variance rejection',
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Approved: complete receipt with actual quantities
|
|
663
|
+
log.info('✅ Variance approved - completing receipt...');
|
|
664
|
+
const receiptData = transferState.receiptData;
|
|
665
|
+
if (!receiptData) {
|
|
666
|
+
throw new Error('Receipt data not found in transfer state');
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const receiptResult = await createReceiptMutation(
|
|
670
|
+
client,
|
|
671
|
+
transferId,
|
|
672
|
+
receiptData.items,
|
|
673
|
+
transferState,
|
|
674
|
+
log
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
if (!receiptResult.success) {
|
|
678
|
+
throw new Error(`Receipt failed: ${receiptResult.error}`);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
transferState.status = 'RECEIVED';
|
|
682
|
+
transferState.receiptMutationId = receiptResult.mutationId;
|
|
683
|
+
transferState.receivedAt = new Date().toISOString();
|
|
684
|
+
transferState.updatedAt = new Date().toISOString();
|
|
685
|
+
await saveTransferState(kvAdapter, transferState);
|
|
686
|
+
|
|
687
|
+
const duration = Date.now() - startTime;
|
|
688
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
689
|
+
log.info('✅ VARIANCE APPROVAL COMPLETED', {
|
|
690
|
+
transferId,
|
|
691
|
+
duration: `${duration}ms`,
|
|
692
|
+
});
|
|
693
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
694
|
+
|
|
695
|
+
return {
|
|
696
|
+
success: true,
|
|
697
|
+
status: 'RECEIVED',
|
|
698
|
+
transferId,
|
|
699
|
+
receiptMutationId: receiptResult.mutationId,
|
|
700
|
+
};
|
|
701
|
+
} finally {
|
|
702
|
+
await stateService.releaseLock(lockName, kvAdapter);
|
|
703
|
+
}
|
|
704
|
+
} catch (error: any) {
|
|
705
|
+
const duration = Date.now() - startTime;
|
|
706
|
+
log.error('═══════════════════════════════════════════════════════');
|
|
707
|
+
log.error('❌ VARIANCE APPROVAL FAILED', {
|
|
708
|
+
transferId,
|
|
709
|
+
error: error instanceof Error ? error.message : String(error),
|
|
710
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
711
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
712
|
+
duration: `${duration}ms`,
|
|
713
|
+
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
714
|
+
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
715
|
+
: error.message?.includes('not found') || error.message?.includes('missing')
|
|
716
|
+
? 'Transfer not found or receipt data missing - verify transferId and transfer state'
|
|
717
|
+
: error.message?.includes('lock') || error.message?.includes('concurrent')
|
|
718
|
+
? 'Concurrent approval processing detected - retry after lock release'
|
|
719
|
+
: error.message?.includes('receipt') || error.message?.includes('mutation')
|
|
720
|
+
? 'Check receipt mutation payload and inventory availability at destination'
|
|
721
|
+
: 'Review error details and check variance approval payload structure',
|
|
722
|
+
});
|
|
723
|
+
log.error('═══════════════════════════════════════════════════════');
|
|
724
|
+
|
|
725
|
+
return {
|
|
726
|
+
success: false,
|
|
727
|
+
error: error.message,
|
|
728
|
+
transferId,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Run reconciliation (scheduled)
|
|
735
|
+
*/
|
|
736
|
+
export async function runReconciliation(
|
|
737
|
+
ctx: any,
|
|
738
|
+
jobId: string,
|
|
739
|
+
tracker: any
|
|
740
|
+
): Promise<any> {
|
|
741
|
+
const { log, openKv } = ctx;
|
|
742
|
+
const startTime = Date.now();
|
|
743
|
+
|
|
744
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
745
|
+
log.info('🔄 RECONCILIATION STARTED', { jobId });
|
|
746
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
747
|
+
|
|
748
|
+
try {
|
|
749
|
+
await tracker.updateJob(jobId, { stage: 'discovery', status: 'processing' });
|
|
750
|
+
|
|
751
|
+
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
752
|
+
|
|
753
|
+
// Find all in-transit transfers older than 24 hours
|
|
754
|
+
log.info('🔍 Discovering stale transfers...');
|
|
755
|
+
const allTransfers = await getAllTransferStates(kvAdapter);
|
|
756
|
+
const staleTransfers = allTransfers.filter(t => {
|
|
757
|
+
if (t.status !== 'IN_TRANSIT') return false;
|
|
758
|
+
const dispatchedAt = new Date(t.dispatchedAt || t.createdAt);
|
|
759
|
+
const hoursSinceDispatch = (Date.now() - dispatchedAt.getTime()) / (1000 * 60 * 60);
|
|
760
|
+
return hoursSinceDispatch > 24;
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
log.info('📊 Stale transfers found', { count: staleTransfers.length });
|
|
764
|
+
|
|
765
|
+
await tracker.updateJob(jobId, {
|
|
766
|
+
stage: 'processing',
|
|
767
|
+
metadata: { staleCount: staleTransfers.length },
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// Process each stale transfer (generate alerts, etc.)
|
|
771
|
+
let alertsSent = 0;
|
|
772
|
+
for (const transfer of staleTransfers) {
|
|
773
|
+
log.warn('⚠️ Stale transfer detected', {
|
|
774
|
+
transferId: transfer.transferId,
|
|
775
|
+
status: transfer.status,
|
|
776
|
+
age: Math.floor((Date.now() - new Date(transfer.dispatchedAt || transfer.createdAt).getTime()) / (1000 * 60 * 60)) + ' hours',
|
|
777
|
+
});
|
|
778
|
+
// Send alert email (implement as needed)
|
|
779
|
+
alertsSent++;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const duration = Date.now() - startTime;
|
|
783
|
+
const result = {
|
|
784
|
+
staleTransfersFound: staleTransfers.length,
|
|
785
|
+
alertsSent,
|
|
786
|
+
duration: `${duration}ms`,
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
790
|
+
log.info('✅ RECONCILIATION COMPLETED', result);
|
|
791
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
792
|
+
|
|
793
|
+
return result;
|
|
794
|
+
} catch (error: any) {
|
|
795
|
+
const duration = Date.now() - startTime;
|
|
796
|
+
log.error('═══════════════════════════════════════════════════════');
|
|
797
|
+
log.error('❌ RECONCILIATION FAILED', {
|
|
798
|
+
jobId,
|
|
799
|
+
error: error instanceof Error ? error.message : String(error),
|
|
800
|
+
duration: `${duration}ms`,
|
|
801
|
+
});
|
|
802
|
+
log.error('═══════════════════════════════════════════════════════');
|
|
803
|
+
throw error;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Helper: Create dispatch mutation (remove from source location)
|
|
809
|
+
*/
|
|
810
|
+
async function createDispatchMutation(
|
|
811
|
+
client: FluentClient,
|
|
812
|
+
transferRequest: TransferRequest,
|
|
813
|
+
log: Logger
|
|
814
|
+
): Promise<{ success: boolean; mutationId?: string; error?: string }> {
|
|
815
|
+
try {
|
|
816
|
+
const dispatchMapper = new GraphQLMutationMapper(
|
|
817
|
+
{
|
|
818
|
+
version: '1.0.0',
|
|
819
|
+
mutation: 'updateInventoryQuantity',
|
|
820
|
+
sourceFormat: 'json',
|
|
821
|
+
fields: {
|
|
822
|
+
'input.ref': {
|
|
823
|
+
resolver: 'custom.generateDispatchRef',
|
|
824
|
+
},
|
|
825
|
+
'input.locationRef': {
|
|
826
|
+
source: 'sourceLocationRef',
|
|
827
|
+
},
|
|
828
|
+
'input.items[]': {
|
|
829
|
+
source: 'items',
|
|
830
|
+
fields: {
|
|
831
|
+
skuRef: { source: 'skuRef' },
|
|
832
|
+
qty: {
|
|
833
|
+
source: 'requestedQty',
|
|
834
|
+
resolver: 'custom.negateQty', // Make negative for dispatch
|
|
835
|
+
},
|
|
836
|
+
type: { value: 'DISPATCH' },
|
|
837
|
+
status: { value: 'AVAILABLE' },
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
},
|
|
841
|
+
returnFields: ['id', 'ref'],
|
|
842
|
+
} as any,
|
|
843
|
+
client,
|
|
844
|
+
log
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
// Custom resolvers
|
|
848
|
+
const resolvers = {
|
|
849
|
+
'custom.generateDispatchRef': () => {
|
|
850
|
+
return `DISPATCH-${transferRequest.transferId}-${Date.now()}`;
|
|
851
|
+
},
|
|
852
|
+
'custom.negateQty': (qty: any) => {
|
|
853
|
+
return -Math.abs(qty);
|
|
854
|
+
},
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
const result = await dispatchMapper.mapWithNodes(transferRequest, resolvers, {
|
|
858
|
+
config: {},
|
|
859
|
+
helpers: {},
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
if (!result.success) {
|
|
863
|
+
throw new Error(`Mapping failed: ${result.errors?.join(', ')}`);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Execute mutation (query is auto-generated in result)
|
|
867
|
+
const response = await client.graphql({
|
|
868
|
+
query: result.query,
|
|
869
|
+
variables: result.variables, // ✅ Use variables (wrapped if fields pattern)
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
return {
|
|
873
|
+
success: true,
|
|
874
|
+
mutationId: response.data?.updateInventoryQuantity?.id,
|
|
875
|
+
};
|
|
876
|
+
} catch (error: any) {
|
|
877
|
+
log.error('❌ Dispatch mutation failed', {
|
|
878
|
+
error: error instanceof Error ? error.message : String(error),
|
|
879
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
880
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
881
|
+
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
882
|
+
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
883
|
+
: error.message?.includes('mapping') || error.message?.includes('field')
|
|
884
|
+
? 'Check dispatch mapping configuration and verify transfer request structure'
|
|
885
|
+
: error.message?.includes('inventory') || error.message?.includes('quantity')
|
|
886
|
+
? 'Check available inventory quantity at source location'
|
|
887
|
+
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
888
|
+
? 'Check network connectivity and Fluent Commerce API availability'
|
|
889
|
+
: 'Review error details and check dispatch mutation payload',
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
return {
|
|
893
|
+
success: false,
|
|
894
|
+
error: error.message,
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Helper: Create receipt mutation (add to destination location)
|
|
901
|
+
*/
|
|
902
|
+
async function createReceiptMutation(
|
|
903
|
+
client: FluentClient,
|
|
904
|
+
transferId: string,
|
|
905
|
+
items: TransferItem[],
|
|
906
|
+
transferState: TransferState,
|
|
907
|
+
log: Logger
|
|
908
|
+
): Promise<{ success: boolean; mutationId?: string; error?: string }> {
|
|
909
|
+
try {
|
|
910
|
+
const receiptMapper = new GraphQLMutationMapper(
|
|
911
|
+
{
|
|
912
|
+
version: '1.0.0',
|
|
913
|
+
mutation: 'updateInventoryQuantity',
|
|
914
|
+
sourceFormat: 'json',
|
|
915
|
+
fields: {
|
|
916
|
+
'input.ref': {
|
|
917
|
+
value: `RECEIPT-${transferId}-${Date.now()}`,
|
|
918
|
+
},
|
|
919
|
+
'input.locationRef': {
|
|
920
|
+
source: 'destinationLocationRef',
|
|
921
|
+
},
|
|
922
|
+
'input.items[]': {
|
|
923
|
+
source: 'items',
|
|
924
|
+
fields: {
|
|
925
|
+
skuRef: { source: 'skuRef' },
|
|
926
|
+
qty: {
|
|
927
|
+
source: 'actualQty',
|
|
928
|
+
defaultValue: { source: 'requestedQty' }, // Use requested if actual not provided
|
|
929
|
+
},
|
|
930
|
+
type: { value: 'RECEIPT' },
|
|
931
|
+
status: { value: 'AVAILABLE' },
|
|
932
|
+
},
|
|
933
|
+
},
|
|
934
|
+
},
|
|
935
|
+
returnFields: ['id', 'ref'],
|
|
936
|
+
} as any,
|
|
937
|
+
client,
|
|
938
|
+
log
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
const sourceData = {
|
|
942
|
+
destinationLocationRef: transferState.originalRequest?.destinationLocationRef,
|
|
943
|
+
items,
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
const result = await receiptMapper.mapWithNodes(sourceData, {}, { config: {}, helpers: {} });
|
|
947
|
+
|
|
948
|
+
if (!result.success) {
|
|
949
|
+
throw new Error(`Mapping failed: ${result.errors?.join(', ')}`);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Execute mutation (query is auto-generated in result)
|
|
953
|
+
const response = await client.graphql({
|
|
954
|
+
query: result.query,
|
|
955
|
+
variables: result.variables, // ✅ Use variables (wrapped if fields pattern)
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
return {
|
|
959
|
+
success: true,
|
|
960
|
+
mutationId: response.data?.updateInventoryQuantity?.id,
|
|
961
|
+
};
|
|
962
|
+
} catch (error: any) {
|
|
963
|
+
log.error('❌ Receipt mutation failed', {
|
|
964
|
+
error: error instanceof Error ? error.message : String(error),
|
|
965
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
966
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
967
|
+
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
968
|
+
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
969
|
+
: error.message?.includes('mapping') || error.message?.includes('field')
|
|
970
|
+
? 'Check receipt mapping configuration and verify receipt data structure'
|
|
971
|
+
: error.message?.includes('inventory') || error.message?.includes('quantity')
|
|
972
|
+
? 'Check actual quantity values and variance calculation'
|
|
973
|
+
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
974
|
+
? 'Check network connectivity and Fluent Commerce API availability'
|
|
975
|
+
: 'Review error details and check receipt mutation payload',
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
return {
|
|
979
|
+
success: false,
|
|
980
|
+
error: error.message,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Helper: Rollback transfer (reverse dispatch)
|
|
987
|
+
*/
|
|
988
|
+
async function rollbackTransfer(
|
|
989
|
+
client: FluentClient,
|
|
990
|
+
transferState: TransferState,
|
|
991
|
+
log: Logger
|
|
992
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
993
|
+
try {
|
|
994
|
+
const originalRequest = transferState.originalRequest;
|
|
995
|
+
|
|
996
|
+
if (!originalRequest) {
|
|
997
|
+
throw new Error('Cannot rollback: original request data missing');
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const rollbackMapper = new GraphQLMutationMapper(
|
|
1001
|
+
{
|
|
1002
|
+
version: '1.0.0',
|
|
1003
|
+
mutation: 'updateInventoryQuantity',
|
|
1004
|
+
sourceFormat: 'json',
|
|
1005
|
+
fields: {
|
|
1006
|
+
'input.ref': {
|
|
1007
|
+
value: `ROLLBACK-${transferState.transferId}-${Date.now()}`,
|
|
1008
|
+
},
|
|
1009
|
+
'input.locationRef': {
|
|
1010
|
+
source: 'sourceLocationRef',
|
|
1011
|
+
},
|
|
1012
|
+
'input.items[]': {
|
|
1013
|
+
source: 'items',
|
|
1014
|
+
fields: {
|
|
1015
|
+
skuRef: { source: 'skuRef' },
|
|
1016
|
+
qty: { source: 'requestedQty' }, // Add back to source
|
|
1017
|
+
type: { value: 'ROLLBACK' },
|
|
1018
|
+
status: { value: 'AVAILABLE' },
|
|
1019
|
+
},
|
|
1020
|
+
},
|
|
1021
|
+
},
|
|
1022
|
+
returnFields: ['id', 'ref'],
|
|
1023
|
+
} as any,
|
|
1024
|
+
client,
|
|
1025
|
+
log
|
|
1026
|
+
);
|
|
1027
|
+
|
|
1028
|
+
const result = await rollbackMapper.mapWithNodes(
|
|
1029
|
+
originalRequest,
|
|
1030
|
+
{},
|
|
1031
|
+
{ config: {}, helpers: {} }
|
|
1032
|
+
);
|
|
1033
|
+
|
|
1034
|
+
if (!result.success) {
|
|
1035
|
+
throw new Error(`Mapping failed: ${result.errors?.join(', ')}`);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Execute mutation (query is auto-generated in result)
|
|
1039
|
+
await client.graphql({
|
|
1040
|
+
query: result.query,
|
|
1041
|
+
variables: result.variables, // ✅ Use variables (wrapped if fields pattern)
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
return { success: true };
|
|
1045
|
+
} catch (error: any) {
|
|
1046
|
+
log.error('❌ Rollback failed', {
|
|
1047
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1048
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
1049
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1050
|
+
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
1051
|
+
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
1052
|
+
: error.message?.includes('mapping') || error.message?.includes('field')
|
|
1053
|
+
? 'Check rollback mapping configuration and verify transfer state structure'
|
|
1054
|
+
: error.message?.includes('original') || error.message?.includes('missing')
|
|
1055
|
+
? 'Original transfer request data missing - verify transfer state in KV store'
|
|
1056
|
+
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
1057
|
+
? 'Check network connectivity and Fluent Commerce API availability'
|
|
1058
|
+
: 'Review error details and check rollback mutation payload',
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
return {
|
|
1062
|
+
success: false,
|
|
1063
|
+
error: error.message,
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Helper: Calculate variance between expected and actual quantities
|
|
1070
|
+
*/
|
|
1071
|
+
function calculateVariance(
|
|
1072
|
+
expectedItems: TransferItem[],
|
|
1073
|
+
actualItems: TransferItem[]
|
|
1074
|
+
): VarianceDetails {
|
|
1075
|
+
const varianceItems = [];
|
|
1076
|
+
let totalExpected = 0;
|
|
1077
|
+
let totalActual = 0;
|
|
1078
|
+
|
|
1079
|
+
for (const expectedItem of expectedItems) {
|
|
1080
|
+
const actualItem = actualItems.find(i => i.skuRef === expectedItem.skuRef);
|
|
1081
|
+
const expectedQty = expectedItem.requestedQty;
|
|
1082
|
+
const actualQty = actualItem?.actualQty ?? expectedQty;
|
|
1083
|
+
|
|
1084
|
+
totalExpected += expectedQty;
|
|
1085
|
+
totalActual += actualQty;
|
|
1086
|
+
|
|
1087
|
+
if (expectedQty !== actualQty) {
|
|
1088
|
+
varianceItems.push({
|
|
1089
|
+
skuRef: expectedItem.skuRef,
|
|
1090
|
+
expected: expectedQty,
|
|
1091
|
+
actual: actualQty,
|
|
1092
|
+
difference: actualQty - expectedQty,
|
|
1093
|
+
reason: actualItem?.notes,
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
const variancePercentage =
|
|
1099
|
+
totalExpected > 0 ? Math.abs((totalActual - totalExpected) / totalExpected) * 100 : 0;
|
|
1100
|
+
|
|
1101
|
+
return {
|
|
1102
|
+
hasVariance: varianceItems.length > 0,
|
|
1103
|
+
variancePercentage,
|
|
1104
|
+
requiresApproval: variancePercentage > 5, // > 5% requires approval
|
|
1105
|
+
items: varianceItems,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Helper: Get transfer state from KV
|
|
1111
|
+
*/
|
|
1112
|
+
async function getTransferState(
|
|
1113
|
+
kvAdapter: VersoriKVAdapter,
|
|
1114
|
+
transferId: string
|
|
1115
|
+
): Promise<TransferState | null> {
|
|
1116
|
+
const stateKey = ['transfer-state', transferId];
|
|
1117
|
+
const result = await kvAdapter.get(stateKey);
|
|
1118
|
+
return result?.value as TransferState | null;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Helper: Save transfer state to KV
|
|
1123
|
+
*/
|
|
1124
|
+
async function saveTransferState(
|
|
1125
|
+
kvAdapter: VersoriKVAdapter,
|
|
1126
|
+
transferState: TransferState
|
|
1127
|
+
): Promise<void> {
|
|
1128
|
+
const stateKey = ['transfer-state', transferState.transferId];
|
|
1129
|
+
await kvAdapter.set(stateKey, transferState);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Helper: Get all transfer states (for reconciliation)
|
|
1134
|
+
*/
|
|
1135
|
+
async function getAllTransferStates(
|
|
1136
|
+
kvAdapter: VersoriKVAdapter
|
|
1137
|
+
): Promise<TransferState[]> {
|
|
1138
|
+
// Note: This is a simplified implementation
|
|
1139
|
+
// In production, you'd use KV list operations with proper pagination
|
|
1140
|
+
const transfers: TransferState[] = [];
|
|
1141
|
+
|
|
1142
|
+
// Implementation would depend on your KV structure
|
|
1143
|
+
// This is a placeholder showing the expected return type
|
|
1144
|
+
|
|
1145
|
+
return transfers;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Helper: Send variance notification email
|
|
1150
|
+
*/
|
|
1151
|
+
async function sendVarianceNotification(
|
|
1152
|
+
transferId: string,
|
|
1153
|
+
variance: VarianceDetails,
|
|
1154
|
+
activation: any,
|
|
1155
|
+
log: any
|
|
1156
|
+
): Promise<void> {
|
|
1157
|
+
const emailTo = activation.getVariable('varianceNotificationEmail') as string;
|
|
1158
|
+
if (!emailTo) {
|
|
1159
|
+
log.warn('⚠️ Variance notification email not configured', { transferId });
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Email sending implementation
|
|
1164
|
+
// Use Versori email connector or external service
|
|
1165
|
+
log.info('📧 Sending variance notification', {
|
|
1166
|
+
transferId,
|
|
1167
|
+
emailTo,
|
|
1168
|
+
variancePercentage: variance.variancePercentage.toFixed(2) + '%',
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Helper: Send completion notification
|
|
1174
|
+
*/
|
|
1175
|
+
async function sendCompletionNotification(
|
|
1176
|
+
transferId: string,
|
|
1177
|
+
transferState: TransferState,
|
|
1178
|
+
activation: any,
|
|
1179
|
+
log: any
|
|
1180
|
+
): Promise<void> {
|
|
1181
|
+
const emailTo = activation.getVariable('completionNotificationEmail') as string;
|
|
1182
|
+
if (!emailTo) return;
|
|
1183
|
+
|
|
1184
|
+
log.info('📧 Sending completion notification', {
|
|
1185
|
+
transferId,
|
|
1186
|
+
emailTo,
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
---
|
|
1192
|
+
|
|
1193
|
+
### 3. Workflow: Initiate Transfer (`src/workflows/webhook/initiate-transfer.ts`)
|
|
1194
|
+
|
|
1195
|
+
```typescript
|
|
1196
|
+
/**
|
|
1197
|
+
* Webhook: Initiate Transfer
|
|
1198
|
+
*
|
|
1199
|
+
* Endpoint: POST https://{workspace}.versori.run/initiate-transfer
|
|
1200
|
+
* Request body: TransferRequest (JSON)
|
|
1201
|
+
*
|
|
1202
|
+
* Pattern: webhook().then(http()) - needs Fluent API access
|
|
1203
|
+
* Uses shared service: transfer.service.ts
|
|
1204
|
+
*/
|
|
1205
|
+
|
|
1206
|
+
import { webhook, http } from '@versori/run';
|
|
1207
|
+
import { processTransferRequest } from '../../services/transfer.service';
|
|
1208
|
+
|
|
1209
|
+
export const initiateTransfer = webhook('initiate-transfer', {
|
|
1210
|
+
response: { mode: 'sync' },
|
|
1211
|
+
connection: 'initiate-transfer', // Versori validates API key
|
|
1212
|
+
}).then(
|
|
1213
|
+
http('process-transfer-request', { connection: 'fluent_commerce' }, async ctx => {
|
|
1214
|
+
const { log, data } = ctx;
|
|
1215
|
+
const jobId = `transfer-${Date.now()}`;
|
|
1216
|
+
|
|
1217
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
1218
|
+
log.info('📦 TRANSFER REQUEST RECEIVED', { jobId });
|
|
1219
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
1220
|
+
|
|
1221
|
+
try {
|
|
1222
|
+
const result = await processTransferRequest(ctx, data);
|
|
1223
|
+
|
|
1224
|
+
if (result.success) {
|
|
1225
|
+
log.info('✅ TRANSFER REQUEST COMPLETED', { jobId, result });
|
|
1226
|
+
} else {
|
|
1227
|
+
log.error('❌ TRANSFER REQUEST FAILED', { jobId, error: result.error });
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
return { success: result.success, jobId, ...result };
|
|
1231
|
+
} catch (e: any) {
|
|
1232
|
+
log.error('❌ TRANSFER REQUEST FAILED', { jobId, error: e.message });
|
|
1233
|
+
return { success: false, jobId, error: e.message };
|
|
1234
|
+
}
|
|
1235
|
+
})
|
|
1236
|
+
);
|
|
1237
|
+
```
|
|
1238
|
+
|
|
1239
|
+
---
|
|
1240
|
+
|
|
1241
|
+
### 4. Workflow: Receive Transfer (`src/workflows/webhook/receive-transfer.ts`)
|
|
1242
|
+
|
|
1243
|
+
```typescript
|
|
1244
|
+
/**
|
|
1245
|
+
* Webhook: Receive Transfer (Receipt Confirmation)
|
|
1246
|
+
*
|
|
1247
|
+
* Endpoint: POST https://{workspace}.versori.run/receive-transfer
|
|
1248
|
+
* Request body: { transferId, receivedDate, items[] }
|
|
1249
|
+
*
|
|
1250
|
+
* Pattern: webhook().then(http()) - needs Fluent API access
|
|
1251
|
+
* Uses shared service: transfer.service.ts
|
|
1252
|
+
*/
|
|
1253
|
+
|
|
1254
|
+
import { webhook, http } from '@versori/run';
|
|
1255
|
+
import { processReceiptConfirmation } from '../../services/transfer.service';
|
|
1256
|
+
|
|
1257
|
+
export const receiveTransfer = webhook('receive-transfer', {
|
|
1258
|
+
response: { mode: 'sync' },
|
|
1259
|
+
connection: 'receive-transfer',
|
|
1260
|
+
}).then(
|
|
1261
|
+
http('process-receipt-confirmation', { connection: 'fluent_commerce' }, async ctx => {
|
|
1262
|
+
const { log, data } = ctx;
|
|
1263
|
+
const jobId = `receipt-${Date.now()}`;
|
|
1264
|
+
|
|
1265
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
1266
|
+
log.info('📦 RECEIPT CONFIRMATION RECEIVED', { jobId, transferId: data.transferId });
|
|
1267
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
1268
|
+
|
|
1269
|
+
try {
|
|
1270
|
+
const result = await processReceiptConfirmation(ctx, data);
|
|
1271
|
+
|
|
1272
|
+
if (result.success) {
|
|
1273
|
+
log.info('✅ RECEIPT CONFIRMATION COMPLETED', { jobId, result });
|
|
1274
|
+
} else {
|
|
1275
|
+
log.error('❌ RECEIPT CONFIRMATION FAILED', { jobId, error: result.error });
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
return { success: result.success, jobId, ...result };
|
|
1279
|
+
} catch (e: any) {
|
|
1280
|
+
log.error('❌ RECEIPT CONFIRMATION FAILED', { jobId, error: e.message });
|
|
1281
|
+
return { success: false, jobId, error: e.message };
|
|
1282
|
+
}
|
|
1283
|
+
})
|
|
1284
|
+
);
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
---
|
|
1288
|
+
|
|
1289
|
+
### 5. Workflow: Reconcile Transfers (`src/workflows/scheduled/reconcile-transfers.ts`)
|
|
1290
|
+
|
|
1291
|
+
```typescript
|
|
1292
|
+
/**
|
|
1293
|
+
* Scheduled Workflow: Transfer Reconciliation
|
|
1294
|
+
*
|
|
1295
|
+
* Runs automatically daily at 1 AM UTC
|
|
1296
|
+
* NOT exposed as HTTP endpoint - Versori executes on schedule
|
|
1297
|
+
*
|
|
1298
|
+
* Finds stale in-transit transfers (> 24 hours) and generates alerts
|
|
1299
|
+
* Uses JobTracker for status tracking
|
|
1300
|
+
*/
|
|
1301
|
+
|
|
1302
|
+
import { schedule, http } from '@versori/run';
|
|
1303
|
+
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
1304
|
+
import { runReconciliation } from '../../services/transfer.service';
|
|
1305
|
+
|
|
1306
|
+
export const reconcileTransfers = schedule(
|
|
1307
|
+
'reconcile-transfers',
|
|
1308
|
+
'0 1 * * *' // Daily at 1 AM UTC
|
|
1309
|
+
).then(
|
|
1310
|
+
http('run-reconciliation', { connection: 'fluent_commerce' }, async ctx => {
|
|
1311
|
+
const { log, openKv } = ctx;
|
|
1312
|
+
const jobId = `reconcile-${Date.now()}`;
|
|
1313
|
+
|
|
1314
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
1315
|
+
log.info('🔄 RECONCILIATION STARTED', { jobId });
|
|
1316
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
1317
|
+
|
|
1318
|
+
const tracker = new JobTracker(openKv(':project:'), log);
|
|
1319
|
+
await tracker.createJob(jobId, { triggeredBy: 'schedule' });
|
|
1320
|
+
|
|
1321
|
+
try {
|
|
1322
|
+
const result = await runReconciliation(ctx, jobId, tracker);
|
|
1323
|
+
await tracker.markCompleted(jobId, result);
|
|
1324
|
+
|
|
1325
|
+
log.info('✅ RECONCILIATION COMPLETED', { jobId, result });
|
|
1326
|
+
return { success: true, jobId, ...result };
|
|
1327
|
+
} catch (e: any) {
|
|
1328
|
+
await tracker.markFailed(jobId, e);
|
|
1329
|
+
|
|
1330
|
+
log.error('❌ RECONCILIATION FAILED', { jobId, error: e.message });
|
|
1331
|
+
return { success: false, jobId, error: e.message };
|
|
1332
|
+
}
|
|
1333
|
+
})
|
|
1334
|
+
);
|
|
1335
|
+
```
|
|
1336
|
+
|
|
1337
|
+
---
|
|
1338
|
+
|
|
1339
|
+
### 6. Workflow: Export Transfer Report (`src/workflows/webhook/export-transfer-report.ts`)
|
|
1340
|
+
|
|
1341
|
+
```typescript
|
|
1342
|
+
/**
|
|
1343
|
+
* Webhook: Export Transfer Report
|
|
1344
|
+
*
|
|
1345
|
+
* Endpoint: POST https://{workspace}.versori.run/export-transfer-report
|
|
1346
|
+
* Request body: { startDate, endDate }
|
|
1347
|
+
*
|
|
1348
|
+
* Pattern: webhook().then(fn()) - no external API needed, only KV storage
|
|
1349
|
+
* Queries KV store for transfer states and generates report
|
|
1350
|
+
*/
|
|
1351
|
+
|
|
1352
|
+
import { webhook, fn } from '@versori/run';
|
|
1353
|
+
import { VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
1354
|
+
|
|
1355
|
+
export const exportTransferReport = webhook('export-transfer-report', {
|
|
1356
|
+
response: { mode: 'sync' },
|
|
1357
|
+
connection: 'export-transfer-report',
|
|
1358
|
+
}).then(
|
|
1359
|
+
fn('generate-report', async ctx => {
|
|
1360
|
+
const { log, openKv, data } = ctx;
|
|
1361
|
+
const { startDate, endDate } = data;
|
|
1362
|
+
|
|
1363
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
1364
|
+
log.info('📊 TRANSFER REPORT EXPORT', { startDate, endDate });
|
|
1365
|
+
log.info('═══════════════════════════════════════════════════════');
|
|
1366
|
+
|
|
1367
|
+
try {
|
|
1368
|
+
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
1369
|
+
|
|
1370
|
+
// Query transfer states (simplified - implement proper date filtering)
|
|
1371
|
+
const transfers: any[] = []; // getAllTransferStates(kvAdapter, startDate, endDate);
|
|
1372
|
+
|
|
1373
|
+
const report = {
|
|
1374
|
+
period: { startDate, endDate },
|
|
1375
|
+
totalTransfers: transfers.length,
|
|
1376
|
+
byStatus: {
|
|
1377
|
+
pending: transfers.filter(t => t.status === 'PENDING').length,
|
|
1378
|
+
dispatched: transfers.filter(t => t.status === 'DISPATCHED').length,
|
|
1379
|
+
inTransit: transfers.filter(t => t.status === 'IN_TRANSIT').length,
|
|
1380
|
+
received: transfers.filter(t => t.status === 'RECEIVED').length,
|
|
1381
|
+
failed: transfers.filter(t => t.status === 'FAILED').length,
|
|
1382
|
+
rolledBack: transfers.filter(t => t.status === 'ROLLED_BACK').length,
|
|
1383
|
+
},
|
|
1384
|
+
transfers: transfers.map(t => ({
|
|
1385
|
+
transferId: t.transferId,
|
|
1386
|
+
status: t.status,
|
|
1387
|
+
sourceLocation: t.originalRequest?.sourceLocationRef,
|
|
1388
|
+
destinationLocation: t.originalRequest?.destinationLocationRef,
|
|
1389
|
+
itemCount: t.originalRequest?.items?.length || 0,
|
|
1390
|
+
createdAt: t.createdAt,
|
|
1391
|
+
completedAt: t.receivedAt || t.updatedAt,
|
|
1392
|
+
})),
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
log.info('✅ REPORT GENERATED', {
|
|
1396
|
+
totalTransfers: report.totalTransfers,
|
|
1397
|
+
byStatus: report.byStatus,
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
return { success: true, report };
|
|
1401
|
+
} catch (e: any) {
|
|
1402
|
+
log.error('❌ REPORT EXPORT FAILED', { error: e.message });
|
|
1403
|
+
return { success: false, error: e.message };
|
|
1404
|
+
}
|
|
1405
|
+
})
|
|
1406
|
+
);
|
|
1407
|
+
```
|
|
1408
|
+
|
|
1409
|
+
---
|
|
1410
|
+
|
|
1411
|
+
## Configuration
|
|
1412
|
+
|
|
1413
|
+
### Activation Variables Configuration
|
|
1414
|
+
|
|
1415
|
+
```bash
|
|
1416
|
+
# Required Variables
|
|
1417
|
+
fluentRetailerId=my-retailer-id
|
|
1418
|
+
|
|
1419
|
+
# S3 Configuration (for CSV-based transfers)
|
|
1420
|
+
s3TransferBucket=my-transfers-bucket
|
|
1421
|
+
awsAccessKeyId=AKIAXXXXXXXXXXXX
|
|
1422
|
+
awsSecretAccessKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
1423
|
+
awsRegion=us-east-1
|
|
1424
|
+
s3TransferPrefix=transfers/
|
|
1425
|
+
|
|
1426
|
+
# Email Notifications
|
|
1427
|
+
varianceNotificationEmail=warehouse-manager@company.com
|
|
1428
|
+
completionNotificationEmail=logistics@company.com
|
|
1429
|
+
```
|
|
1430
|
+
|
|
1431
|
+
### GraphQL Mutation Mapping Configuration: `config/transfer-mappings.json`
|
|
1432
|
+
|
|
1433
|
+
```json
|
|
1434
|
+
{
|
|
1435
|
+
"dispatch": {
|
|
1436
|
+
"version": "1.0.0",
|
|
1437
|
+
"mutation": "updateInventoryQuantity",
|
|
1438
|
+
"sourceFormat": "json",
|
|
1439
|
+
"fields": {
|
|
1440
|
+
"input.ref": {
|
|
1441
|
+
"resolver": "custom.generateDispatchRef"
|
|
1442
|
+
},
|
|
1443
|
+
"input.locationRef": {
|
|
1444
|
+
"source": "sourceLocationRef",
|
|
1445
|
+
"required": true
|
|
1446
|
+
},
|
|
1447
|
+
"input.items": {
|
|
1448
|
+
"source": "items",
|
|
1449
|
+
"isArray": true,
|
|
1450
|
+
"fields": {
|
|
1451
|
+
"skuRef": {
|
|
1452
|
+
"source": "skuRef",
|
|
1453
|
+
"required": true
|
|
1454
|
+
},
|
|
1455
|
+
"qty": {
|
|
1456
|
+
"source": "requestedQty",
|
|
1457
|
+
"resolver": "custom.negateQty",
|
|
1458
|
+
"required": true
|
|
1459
|
+
},
|
|
1460
|
+
"type": {
|
|
1461
|
+
"value": "DISPATCH"
|
|
1462
|
+
},
|
|
1463
|
+
"status": {
|
|
1464
|
+
"value": "AVAILABLE"
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
},
|
|
1469
|
+
"returnFields": ["id", "ref"]
|
|
1470
|
+
},
|
|
1471
|
+
"receipt": {
|
|
1472
|
+
"version": "1.0.0",
|
|
1473
|
+
"mutation": "updateInventoryQuantity",
|
|
1474
|
+
"sourceFormat": "json",
|
|
1475
|
+
"fields": {
|
|
1476
|
+
"input.ref": {
|
|
1477
|
+
"resolver": "custom.generateReceiptRef"
|
|
1478
|
+
},
|
|
1479
|
+
"input.locationRef": {
|
|
1480
|
+
"source": "destinationLocationRef",
|
|
1481
|
+
"required": true
|
|
1482
|
+
},
|
|
1483
|
+
"input.items": {
|
|
1484
|
+
"source": "items",
|
|
1485
|
+
"isArray": true,
|
|
1486
|
+
"fields": {
|
|
1487
|
+
"skuRef": {
|
|
1488
|
+
"source": "skuRef",
|
|
1489
|
+
"required": true
|
|
1490
|
+
},
|
|
1491
|
+
"qty": {
|
|
1492
|
+
"source": "actualQty",
|
|
1493
|
+
"required": true
|
|
1494
|
+
},
|
|
1495
|
+
"type": {
|
|
1496
|
+
"value": "RECEIPT"
|
|
1497
|
+
},
|
|
1498
|
+
"status": {
|
|
1499
|
+
"value": "AVAILABLE"
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
},
|
|
1504
|
+
"returnFields": ["id", "ref"]
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
```
|
|
1508
|
+
|
|
1509
|
+
---
|
|
1510
|
+
|
|
1511
|
+
## Input/Output Examples
|
|
1512
|
+
|
|
1513
|
+
### CSV Input Format Example: `sample-transfer.csv`
|
|
1514
|
+
|
|
1515
|
+
```csv
|
|
1516
|
+
transfer_id,source_location,destination_location,requested_date,expected_delivery,items
|
|
1517
|
+
TRF-001,WH-EAST,STORE-NYC,2025-01-15,2025-01-17,"[{""sku"":""SKU-001"",""qty"":100},{""sku"":""SKU-002"",""qty"":50}]"
|
|
1518
|
+
TRF-002,WH-WEST,STORE-LA,2025-01-15,2025-01-18,"[{""sku"":""SKU-003"",""qty"":200}]"
|
|
1519
|
+
```
|
|
1520
|
+
|
|
1521
|
+
### JSON Input Format Example
|
|
1522
|
+
|
|
1523
|
+
```json
|
|
1524
|
+
{
|
|
1525
|
+
"transferId": "TRF-001",
|
|
1526
|
+
"sourceLocationRef": "WH-EAST",
|
|
1527
|
+
"destinationLocationRef": "STORE-NYC",
|
|
1528
|
+
"requestedDate": "2025-01-15T10:00:00Z",
|
|
1529
|
+
"expectedDeliveryDate": "2025-01-17T10:00:00Z",
|
|
1530
|
+
"items": [
|
|
1531
|
+
{
|
|
1532
|
+
"skuRef": "SKU-001",
|
|
1533
|
+
"requestedQty": 100,
|
|
1534
|
+
"condition": "NEW"
|
|
1535
|
+
},
|
|
1536
|
+
{
|
|
1537
|
+
"skuRef": "SKU-002",
|
|
1538
|
+
"requestedQty": 50,
|
|
1539
|
+
"condition": "NEW"
|
|
1540
|
+
}
|
|
1541
|
+
],
|
|
1542
|
+
"metadata": {
|
|
1543
|
+
"reason": "Store restocking",
|
|
1544
|
+
"priority": "HIGH",
|
|
1545
|
+
"requestedBy": "manager@store.com"
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
```
|
|
1549
|
+
|
|
1550
|
+
### Receipt Confirmation Format
|
|
1551
|
+
|
|
1552
|
+
```json
|
|
1553
|
+
{
|
|
1554
|
+
"transferId": "TRF-001",
|
|
1555
|
+
"receivedDate": "2025-01-17T14:30:00Z",
|
|
1556
|
+
"items": [
|
|
1557
|
+
{
|
|
1558
|
+
"skuRef": "SKU-001",
|
|
1559
|
+
"requestedQty": 100,
|
|
1560
|
+
"actualQty": 98,
|
|
1561
|
+
"notes": "2 units damaged in transit"
|
|
1562
|
+
},
|
|
1563
|
+
{
|
|
1564
|
+
"skuRef": "SKU-002",
|
|
1565
|
+
"requestedQty": 50,
|
|
1566
|
+
"actualQty": 50
|
|
1567
|
+
}
|
|
1568
|
+
]
|
|
1569
|
+
}
|
|
1570
|
+
```
|
|
1571
|
+
|
|
1572
|
+
---
|
|
1573
|
+
|
|
1574
|
+
## Versori Workflow Structure
|
|
1575
|
+
|
|
1576
|
+
### Workflow Flow Diagram
|
|
1577
|
+
|
|
1578
|
+
```
|
|
1579
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
1580
|
+
│ Transfer Request │
|
|
1581
|
+
│ (Webhook JSON or Scheduled CSV) │
|
|
1582
|
+
└──────────────────────┬──────────────────────────────────────┘
|
|
1583
|
+
│
|
|
1584
|
+
├─ Acquire Lock
|
|
1585
|
+
│
|
|
1586
|
+
├─ Initialize Transfer State (PENDING)
|
|
1587
|
+
│
|
|
1588
|
+
├─ Create Dispatch Mutation
|
|
1589
|
+
│ └─ Remove from Source Location
|
|
1590
|
+
│
|
|
1591
|
+
├─ Update State (IN_TRANSIT)
|
|
1592
|
+
│
|
|
1593
|
+
└─ Release Lock
|
|
1594
|
+
│
|
|
1595
|
+
┌──────────────────────┴──────────────────────────────────────┐
|
|
1596
|
+
│ Receipt Confirmation │
|
|
1597
|
+
│ (Webhook JSON) │
|
|
1598
|
+
└──────────────────────┬──────────────────────────────────────┘
|
|
1599
|
+
│
|
|
1600
|
+
├─ Acquire Lock
|
|
1601
|
+
│
|
|
1602
|
+
├─ Calculate Variance
|
|
1603
|
+
│ ├─ If > 5% → Require Approval
|
|
1604
|
+
│ │ └─ Send Email Notification
|
|
1605
|
+
│ │
|
|
1606
|
+
│ └─ If <= 5% → Auto-Approve
|
|
1607
|
+
│
|
|
1608
|
+
├─ Create Receipt Mutation
|
|
1609
|
+
│ └─ Add to Destination Location
|
|
1610
|
+
│
|
|
1611
|
+
├─ Update State (RECEIVED)
|
|
1612
|
+
│
|
|
1613
|
+
├─ Send Completion Notification
|
|
1614
|
+
│
|
|
1615
|
+
└─ Release Lock
|
|
1616
|
+
```
|
|
1617
|
+
|
|
1618
|
+
---
|
|
1619
|
+
|
|
1620
|
+
## Key Patterns Explained
|
|
1621
|
+
|
|
1622
|
+
### Pattern 1: Paired Mutations (Dispatch + Receipt)
|
|
1623
|
+
|
|
1624
|
+
**Dispatch Mutation** (Remove from source):
|
|
1625
|
+
|
|
1626
|
+
```typescript
|
|
1627
|
+
const dispatchPayload = {
|
|
1628
|
+
input: {
|
|
1629
|
+
ref: 'DISPATCH-TRF-001-1234567890',
|
|
1630
|
+
locationRef: 'WH-EAST',
|
|
1631
|
+
items: [
|
|
1632
|
+
{
|
|
1633
|
+
skuRef: 'SKU-001',
|
|
1634
|
+
qty: -100, // Negative quantity for removal
|
|
1635
|
+
type: 'DISPATCH',
|
|
1636
|
+
status: 'AVAILABLE',
|
|
1637
|
+
},
|
|
1638
|
+
],
|
|
1639
|
+
},
|
|
1640
|
+
};
|
|
1641
|
+
```
|
|
1642
|
+
|
|
1643
|
+
**Receipt Mutation** (Add to destination):
|
|
1644
|
+
|
|
1645
|
+
```typescript
|
|
1646
|
+
const receiptPayload = {
|
|
1647
|
+
input: {
|
|
1648
|
+
ref: 'RECEIPT-TRF-001-1234567891',
|
|
1649
|
+
locationRef: 'STORE-NYC',
|
|
1650
|
+
items: [
|
|
1651
|
+
{
|
|
1652
|
+
skuRef: 'SKU-001',
|
|
1653
|
+
qty: 98, // Actual quantity received
|
|
1654
|
+
type: 'RECEIPT',
|
|
1655
|
+
status: 'AVAILABLE',
|
|
1656
|
+
},
|
|
1657
|
+
],
|
|
1658
|
+
},
|
|
1659
|
+
};
|
|
1660
|
+
```
|
|
1661
|
+
|
|
1662
|
+
**Why Paired Mutations?**
|
|
1663
|
+
|
|
1664
|
+
- **Atomicity**: Each mutation is atomic (all-or-nothing)
|
|
1665
|
+
- **Audit Trail**: Separate records for dispatch and receipt
|
|
1666
|
+
- **Rollback**: Can reverse dispatch if receipt fails
|
|
1667
|
+
- **Variance Tracking**: Compare dispatch vs receipt quantities
|
|
1668
|
+
|
|
1669
|
+
### Pattern 2: In-Transit Inventory Tracking
|
|
1670
|
+
|
|
1671
|
+
**VersoriKV State Schema**:
|
|
1672
|
+
|
|
1673
|
+
```typescript
|
|
1674
|
+
interface TransferState {
|
|
1675
|
+
transferId: string;
|
|
1676
|
+
status: 'PENDING' | 'DISPATCHED' | 'IN_TRANSIT' | 'RECEIVED' | 'FAILED' | 'ROLLED_BACK';
|
|
1677
|
+
dispatchMutationId?: string;
|
|
1678
|
+
receiptMutationId?: string;
|
|
1679
|
+
dispatchedAt?: string;
|
|
1680
|
+
receivedAt?: string;
|
|
1681
|
+
variance?: VarianceDetails;
|
|
1682
|
+
createdAt: string;
|
|
1683
|
+
updatedAt: string;
|
|
1684
|
+
}
|
|
1685
|
+
```
|
|
1686
|
+
|
|
1687
|
+
**State Transitions**:
|
|
1688
|
+
|
|
1689
|
+
```
|
|
1690
|
+
PENDING → DISPATCHED → IN_TRANSIT → RECEIVED
|
|
1691
|
+
↓
|
|
1692
|
+
FAILED → ROLLED_BACK
|
|
1693
|
+
```
|
|
1694
|
+
|
|
1695
|
+
**Tracking Benefits**:
|
|
1696
|
+
|
|
1697
|
+
- Know what's in transit at any time
|
|
1698
|
+
- Query transfer status via API
|
|
1699
|
+
- Handle partial receipts
|
|
1700
|
+
- Track transit times for analytics
|
|
1701
|
+
|
|
1702
|
+
### Pattern 3: Variance Handling
|
|
1703
|
+
|
|
1704
|
+
**Variance Calculation**:
|
|
1705
|
+
|
|
1706
|
+
```typescript
|
|
1707
|
+
function calculateVariance(expected: TransferItem[], actual: TransferItem[]): VarianceDetails {
|
|
1708
|
+
let totalExpected = 0;
|
|
1709
|
+
let totalActual = 0;
|
|
1710
|
+
const varianceItems = [];
|
|
1711
|
+
|
|
1712
|
+
for (const expectedItem of expected) {
|
|
1713
|
+
const actualItem = actual.find(i => i.skuRef === expectedItem.skuRef);
|
|
1714
|
+
const expectedQty = expectedItem.requestedQty;
|
|
1715
|
+
const actualQty = actualItem?.actualQty ?? expectedQty;
|
|
1716
|
+
|
|
1717
|
+
totalExpected += expectedQty;
|
|
1718
|
+
totalActual += actualQty;
|
|
1719
|
+
|
|
1720
|
+
if (expectedQty !== actualQty) {
|
|
1721
|
+
varianceItems.push({
|
|
1722
|
+
skuRef: expectedItem.skuRef,
|
|
1723
|
+
expected: expectedQty,
|
|
1724
|
+
actual: actualQty,
|
|
1725
|
+
difference: actualQty - expectedQty,
|
|
1726
|
+
reason: actualItem?.notes,
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
const variancePercentage = Math.abs((totalActual - totalExpected) / totalExpected) * 100;
|
|
1732
|
+
|
|
1733
|
+
return {
|
|
1734
|
+
hasVariance: varianceItems.length > 0,
|
|
1735
|
+
variancePercentage,
|
|
1736
|
+
requiresApproval: variancePercentage > 5,
|
|
1737
|
+
items: varianceItems,
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
```
|
|
1741
|
+
|
|
1742
|
+
**Approval Thresholds**:
|
|
1743
|
+
|
|
1744
|
+
- **< 5% variance**: Auto-approve (minor discrepancies expected)
|
|
1745
|
+
- **> 5% variance**: Require manual approval
|
|
1746
|
+
- **Email notification**: Sent to warehouse manager
|
|
1747
|
+
- **Approval webhook**: `/approve-variance` endpoint
|
|
1748
|
+
|
|
1749
|
+
### Pattern 4: Rollback on Failure
|
|
1750
|
+
|
|
1751
|
+
**When to Rollback**:
|
|
1752
|
+
|
|
1753
|
+
- Receipt mutation fails
|
|
1754
|
+
- Variance rejected by manager
|
|
1755
|
+
- Destination location unavailable
|
|
1756
|
+
- Critical validation errors
|
|
1757
|
+
|
|
1758
|
+
**Rollback Implementation**:
|
|
1759
|
+
|
|
1760
|
+
```typescript
|
|
1761
|
+
async function rollbackTransfer(
|
|
1762
|
+
client: any,
|
|
1763
|
+
transferState: TransferState,
|
|
1764
|
+
log: Logger
|
|
1765
|
+
): Promise<void> {
|
|
1766
|
+
// Create reverse dispatch mutation (add back to source)
|
|
1767
|
+
const rollbackPayload = {
|
|
1768
|
+
input: {
|
|
1769
|
+
ref: `ROLLBACK-${transferState.transferId}`,
|
|
1770
|
+
locationRef: originalSourceLocation,
|
|
1771
|
+
items: originalItems.map(item => ({
|
|
1772
|
+
skuRef: item.skuRef,
|
|
1773
|
+
qty: item.requestedQty, // Positive quantity (add back)
|
|
1774
|
+
type: 'ROLLBACK',
|
|
1775
|
+
status: 'AVAILABLE',
|
|
1776
|
+
})),
|
|
1777
|
+
},
|
|
1778
|
+
};
|
|
1779
|
+
|
|
1780
|
+
await client.graphql({
|
|
1781
|
+
query: rollbackMutation,
|
|
1782
|
+
variables: rollbackPayload,
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
// Update state
|
|
1786
|
+
transferState.status = 'ROLLED_BACK';
|
|
1787
|
+
await saveTransferState(kvAdapter, transferState);
|
|
1788
|
+
}
|
|
1789
|
+
```
|
|
1790
|
+
|
|
1791
|
+
**Rollback Guarantees**:
|
|
1792
|
+
|
|
1793
|
+
- Inventory returned to source location
|
|
1794
|
+
- Audit trail preserved (ROLLBACK type)
|
|
1795
|
+
- State updated to ROLLED_BACK
|
|
1796
|
+
- Prevents inventory loss
|
|
1797
|
+
|
|
1798
|
+
### Pattern 5: Multi-Leg Transfers (A → B → C)
|
|
1799
|
+
|
|
1800
|
+
For complex transfers through intermediate locations:
|
|
1801
|
+
|
|
1802
|
+
```typescript
|
|
1803
|
+
interface MultiLegTransfer {
|
|
1804
|
+
transferId: string;
|
|
1805
|
+
legs: TransferLeg[];
|
|
1806
|
+
currentLeg: number;
|
|
1807
|
+
overallStatus: string;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
interface TransferLeg {
|
|
1811
|
+
legId: string;
|
|
1812
|
+
sourceLocationRef: string;
|
|
1813
|
+
destinationLocationRef: string;
|
|
1814
|
+
status: string;
|
|
1815
|
+
dispatchedAt?: string;
|
|
1816
|
+
receivedAt?: string;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
async function processMultiLegTransfer(
|
|
1820
|
+
transfer: MultiLegTransfer,
|
|
1821
|
+
client: any,
|
|
1822
|
+
log: Logger
|
|
1823
|
+
): Promise<void> {
|
|
1824
|
+
for (let i = transfer.currentLeg; i < transfer.legs.length; i++) {
|
|
1825
|
+
const leg = transfer.legs[i];
|
|
1826
|
+
|
|
1827
|
+
// Dispatch from current location
|
|
1828
|
+
await createDispatchMutation(
|
|
1829
|
+
client,
|
|
1830
|
+
{
|
|
1831
|
+
...transfer,
|
|
1832
|
+
sourceLocationRef: leg.sourceLocationRef,
|
|
1833
|
+
destinationLocationRef: leg.destinationLocationRef,
|
|
1834
|
+
},
|
|
1835
|
+
log
|
|
1836
|
+
);
|
|
1837
|
+
|
|
1838
|
+
// Wait for receipt confirmation (via webhook)
|
|
1839
|
+
// State management tracks current leg
|
|
1840
|
+
transfer.currentLeg = i;
|
|
1841
|
+
await saveTransferState(kvAdapter, transfer);
|
|
1842
|
+
|
|
1843
|
+
// Receipt confirmation advances to next leg
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
transfer.overallStatus = 'COMPLETED';
|
|
1847
|
+
await saveTransferState(kvAdapter, transfer);
|
|
1848
|
+
}
|
|
1849
|
+
```
|
|
1850
|
+
|
|
1851
|
+
---
|
|
1852
|
+
|
|
1853
|
+
## Testing
|
|
1854
|
+
|
|
1855
|
+
### 1. Test Dispatch Request
|
|
1856
|
+
|
|
1857
|
+
```bash
|
|
1858
|
+
curl -X POST https://your-workspace.versori.run/initiate-transfer \
|
|
1859
|
+
-H "Content-Type: application/json" \
|
|
1860
|
+
-d '{
|
|
1861
|
+
"transferId": "TEST-001",
|
|
1862
|
+
"sourceLocationRef": "WH-TEST",
|
|
1863
|
+
"destinationLocationRef": "STORE-TEST",
|
|
1864
|
+
"requestedDate": "2025-01-15T10:00:00Z",
|
|
1865
|
+
"items": [
|
|
1866
|
+
{
|
|
1867
|
+
"skuRef": "TEST-SKU-001",
|
|
1868
|
+
"requestedQty": 10
|
|
1869
|
+
}
|
|
1870
|
+
]
|
|
1871
|
+
}'
|
|
1872
|
+
```
|
|
1873
|
+
|
|
1874
|
+
Expected response:
|
|
1875
|
+
|
|
1876
|
+
```json
|
|
1877
|
+
{
|
|
1878
|
+
"success": true,
|
|
1879
|
+
"transferId": "TEST-001",
|
|
1880
|
+
"status": "IN_TRANSIT",
|
|
1881
|
+
"dispatchMutationId": "12345",
|
|
1882
|
+
"message": "Transfer dispatched successfully, awaiting receipt confirmation"
|
|
1883
|
+
}
|
|
1884
|
+
```
|
|
1885
|
+
|
|
1886
|
+
### 2. Test Receipt Confirmation (No Variance)
|
|
1887
|
+
|
|
1888
|
+
```bash
|
|
1889
|
+
curl -X POST https://your-workspace.versori.run/receive-transfer \
|
|
1890
|
+
-H "Content-Type: application/json" \
|
|
1891
|
+
-d '{
|
|
1892
|
+
"transferId": "TEST-001",
|
|
1893
|
+
"receivedDate": "2025-01-17T14:00:00Z",
|
|
1894
|
+
"items": [
|
|
1895
|
+
{
|
|
1896
|
+
"skuRef": "TEST-SKU-001",
|
|
1897
|
+
"requestedQty": 10,
|
|
1898
|
+
"actualQty": 10
|
|
1899
|
+
}
|
|
1900
|
+
]
|
|
1901
|
+
}'
|
|
1902
|
+
```
|
|
1903
|
+
|
|
1904
|
+
Expected response:
|
|
1905
|
+
|
|
1906
|
+
```json
|
|
1907
|
+
{
|
|
1908
|
+
"success": true,
|
|
1909
|
+
"status": "RECEIVED",
|
|
1910
|
+
"transferId": "TEST-001",
|
|
1911
|
+
"receiptMutationId": "12346"
|
|
1912
|
+
}
|
|
1913
|
+
```
|
|
1914
|
+
|
|
1915
|
+
### 3. Test Receipt with Variance
|
|
1916
|
+
|
|
1917
|
+
```bash
|
|
1918
|
+
curl -X POST https://your-workspace.versori.run/receive-transfer \
|
|
1919
|
+
-H "Content-Type: application/json" \
|
|
1920
|
+
-d '{
|
|
1921
|
+
"transferId": "TEST-002",
|
|
1922
|
+
"receivedDate": "2025-01-17T14:00:00Z",
|
|
1923
|
+
"items": [
|
|
1924
|
+
{
|
|
1925
|
+
"skuRef": "TEST-SKU-001",
|
|
1926
|
+
"requestedQty": 100,
|
|
1927
|
+
"actualQty": 90,
|
|
1928
|
+
"notes": "10 units damaged"
|
|
1929
|
+
}
|
|
1930
|
+
]
|
|
1931
|
+
}'
|
|
1932
|
+
```
|
|
1933
|
+
|
|
1934
|
+
Expected response (variance > 5%):
|
|
1935
|
+
|
|
1936
|
+
```json
|
|
1937
|
+
{
|
|
1938
|
+
"success": true,
|
|
1939
|
+
"status": "PENDING_APPROVAL",
|
|
1940
|
+
"transferId": "TEST-002",
|
|
1941
|
+
"variance": {
|
|
1942
|
+
"hasVariance": true,
|
|
1943
|
+
"variancePercentage": 10,
|
|
1944
|
+
"requiresApproval": true,
|
|
1945
|
+
"items": [
|
|
1946
|
+
{
|
|
1947
|
+
"skuRef": "TEST-SKU-001",
|
|
1948
|
+
"expected": 100,
|
|
1949
|
+
"actual": 90,
|
|
1950
|
+
"difference": -10,
|
|
1951
|
+
"reason": "10 units damaged"
|
|
1952
|
+
}
|
|
1953
|
+
]
|
|
1954
|
+
},
|
|
1955
|
+
"message": "Variance requires manual approval"
|
|
1956
|
+
}
|
|
1957
|
+
```
|
|
1958
|
+
|
|
1959
|
+
---
|
|
1960
|
+
|
|
1961
|
+
## Common Issues & Solutions
|
|
1962
|
+
|
|
1963
|
+
### Issue 1: Duplicate Transfer Processing
|
|
1964
|
+
|
|
1965
|
+
**Symptoms:**
|
|
1966
|
+
|
|
1967
|
+
- Same transfer processed multiple times
|
|
1968
|
+
- Double dispatch/receipt
|
|
1969
|
+
|
|
1970
|
+
**Root Cause:**
|
|
1971
|
+
|
|
1972
|
+
- Lock not acquired
|
|
1973
|
+
- Transfer state not checked
|
|
1974
|
+
|
|
1975
|
+
**Solution:**
|
|
1976
|
+
|
|
1977
|
+
```typescript
|
|
1978
|
+
// Always check existing state before processing
|
|
1979
|
+
const existingState = await getTransferState(kvAdapter, transferId);
|
|
1980
|
+
if (existingState && existingState.status !== 'FAILED') {
|
|
1981
|
+
log.warn('Transfer already processed', { transferId, status: existingState.status });
|
|
1982
|
+
return { success: false, error: 'Already processed' };
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// Always acquire lock
|
|
1986
|
+
const lockAcquired = await stateService.acquireLock(lockName, kvAdapter, 15);
|
|
1987
|
+
if (!lockAcquired) {
|
|
1988
|
+
throw new Error('Could not acquire lock');
|
|
1989
|
+
}
|
|
1990
|
+
```
|
|
1991
|
+
|
|
1992
|
+
### Issue 2: Variance Not Detected
|
|
1993
|
+
|
|
1994
|
+
**Symptoms:**
|
|
1995
|
+
|
|
1996
|
+
- Receipts with different quantities not flagged
|
|
1997
|
+
- No approval required
|
|
1998
|
+
|
|
1999
|
+
**Root Cause:**
|
|
2000
|
+
|
|
2001
|
+
- Variance calculation logic incorrect
|
|
2002
|
+
- Percentage threshold too high
|
|
2003
|
+
|
|
2004
|
+
**Solution:**
|
|
2005
|
+
|
|
2006
|
+
```typescript
|
|
2007
|
+
// Ensure variance calculation includes all items
|
|
2008
|
+
function calculateVariance(expected: TransferItem[], actual: TransferItem[]): VarianceDetails {
|
|
2009
|
+
// Check EVERY item, not just first mismatch
|
|
2010
|
+
for (const expectedItem of expected) {
|
|
2011
|
+
const actualItem = actual.find(i => i.skuRef === expectedItem.skuRef);
|
|
2012
|
+
if (!actualItem) {
|
|
2013
|
+
// Item completely missing
|
|
2014
|
+
varianceItems.push({
|
|
2015
|
+
skuRef: expectedItem.skuRef,
|
|
2016
|
+
expected: expectedItem.requestedQty,
|
|
2017
|
+
actual: 0,
|
|
2018
|
+
difference: -expectedItem.requestedQty,
|
|
2019
|
+
reason: 'Item not received',
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
// Calculate percentage correctly
|
|
2025
|
+
const variancePercentage =
|
|
2026
|
+
totalExpected > 0 ? Math.abs((totalActual - totalExpected) / totalExpected) * 100 : 0;
|
|
2027
|
+
|
|
2028
|
+
return {
|
|
2029
|
+
hasVariance: varianceItems.length > 0,
|
|
2030
|
+
variancePercentage,
|
|
2031
|
+
requiresApproval: variancePercentage > 5, // Adjust threshold as needed
|
|
2032
|
+
items: varianceItems,
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
```
|
|
2036
|
+
|
|
2037
|
+
### Issue 3: Rollback Fails
|
|
2038
|
+
|
|
2039
|
+
**Symptoms:**
|
|
2040
|
+
|
|
2041
|
+
- Transfer marked as ROLLED_BACK but inventory not restored
|
|
2042
|
+
- GraphQL mutation errors
|
|
2043
|
+
|
|
2044
|
+
**Root Cause:**
|
|
2045
|
+
|
|
2046
|
+
- Original request data not stored in state
|
|
2047
|
+
- Rollback mutation invalid
|
|
2048
|
+
|
|
2049
|
+
**Solution:**
|
|
2050
|
+
|
|
2051
|
+
```typescript
|
|
2052
|
+
// Store original request when creating transfer state
|
|
2053
|
+
const transferState: TransferState = {
|
|
2054
|
+
transferId,
|
|
2055
|
+
status: 'PENDING',
|
|
2056
|
+
originalRequest: transferRequest, // Store for rollback
|
|
2057
|
+
// ... other fields
|
|
2058
|
+
};
|
|
2059
|
+
await saveTransferState(kvAdapter, transferState);
|
|
2060
|
+
|
|
2061
|
+
// Validate rollback payload before sending
|
|
2062
|
+
async function rollbackTransfer(client: any, transferState: TransferState, log: Logger) {
|
|
2063
|
+
const originalRequest = transferState.originalRequest;
|
|
2064
|
+
if (!originalRequest) {
|
|
2065
|
+
throw new Error('Cannot rollback: original request data missing');
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// Validate items exist
|
|
2069
|
+
if (!originalRequest.items || originalRequest.items.length === 0) {
|
|
2070
|
+
throw new Error('Cannot rollback: no items in original request');
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
// Create rollback mutation...
|
|
2074
|
+
}
|
|
2075
|
+
```
|
|
2076
|
+
|
|
2077
|
+
### Issue 4: Email Notifications Not Sent
|
|
2078
|
+
|
|
2079
|
+
**Symptoms:**
|
|
2080
|
+
|
|
2081
|
+
- No emails for variance or completion
|
|
2082
|
+
- Silent failures
|
|
2083
|
+
|
|
2084
|
+
**Root Cause:**
|
|
2085
|
+
|
|
2086
|
+
- Email service not configured
|
|
2087
|
+
- Missing activation variables
|
|
2088
|
+
- Email connector not connected
|
|
2089
|
+
|
|
2090
|
+
**Solution:**
|
|
2091
|
+
|
|
2092
|
+
```typescript
|
|
2093
|
+
async function sendVarianceNotification(
|
|
2094
|
+
transferId: string,
|
|
2095
|
+
variance: VarianceDetails,
|
|
2096
|
+
activation: any,
|
|
2097
|
+
log: any
|
|
2098
|
+
): Promise<void> {
|
|
2099
|
+
const emailTo = activation.getVariable('varianceNotificationEmail') as string;
|
|
2100
|
+
|
|
2101
|
+
if (!emailTo) {
|
|
2102
|
+
log.warn('Variance notification email not configured', { transferId });
|
|
2103
|
+
return; // Don't fail transfer if email not configured
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
try {
|
|
2107
|
+
// Send email via Versori connector or external service
|
|
2108
|
+
// Example: SendGrid, AWS SES, etc.
|
|
2109
|
+
await sendEmail({
|
|
2110
|
+
to: emailTo,
|
|
2111
|
+
subject: `Transfer Variance Requires Approval: ${transferId}`,
|
|
2112
|
+
body: formatVarianceEmail(transferId, variance),
|
|
2113
|
+
});
|
|
2114
|
+
|
|
2115
|
+
log.info('Variance notification sent', { transferId, emailTo });
|
|
2116
|
+
} catch (error: any) {
|
|
2117
|
+
// Log error but don't fail transfer
|
|
2118
|
+
log.error('Failed to send variance notification', error, { transferId });
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
```
|
|
2122
|
+
|
|
2123
|
+
---
|
|
2124
|
+
|
|
2125
|
+
## Related Guides
|
|
2126
|
+
|
|
2127
|
+
- **02-scheduled-csv-inventory.md** - CSV processing patterns
|
|
2128
|
+
- **03-kv-state-management.md** - State tracking and locking
|
|
2129
|
+
- **04-webhook-xml-response.md** - Webhook response patterns
|
|
2130
|
+
- **GraphQL Mutation Mapping**: `../../02-CORE-GUIDES/mapping/graphql-mutation-mapping/`
|
|
2131
|
+
- **Universal Mapping**: `../../02-CORE-GUIDES/mapping/readme.md`
|
|
2132
|
+
|
|
2133
|
+
---
|
|
2134
|
+
|
|
2135
|
+
## Summary
|
|
2136
|
+
|
|
2137
|
+
This guide demonstrated inter-location transfer processing with:
|
|
2138
|
+
|
|
2139
|
+
1. **Paired Mutations**: Dispatch and receipt for audit trail
|
|
2140
|
+
2. **In-Transit Tracking**: VersoriKV state management
|
|
2141
|
+
3. **Variance Handling**: Automatic approval thresholds and manual review
|
|
2142
|
+
4. **Rollback Logic**: Restore inventory on failure
|
|
2143
|
+
5. **Multi-Format Support**: JSON webhooks and CSV scheduled imports
|
|
2144
|
+
6. **Email Notifications**: Variance alerts and completion notices
|
|
2145
|
+
|
|
2146
|
+
**Key Takeaways**:
|
|
2147
|
+
|
|
2148
|
+
- Use paired mutations for complete audit trail
|
|
2149
|
+
- Track transfer state in VersoriKV for visibility
|
|
2150
|
+
- Implement variance thresholds (e.g., 5%) for auto-approval
|
|
2151
|
+
- Always store original request for rollback capability
|
|
2152
|
+
- Use distributed locking to prevent duplicate processing
|
|
2153
|
+
- Handle both webhook and scheduled triggers
|
|
2154
|
+
|
|
2155
|
+
**Production Considerations**:
|
|
2156
|
+
|
|
2157
|
+
- Monitor variance approval queue
|
|
2158
|
+
- Set up alerts for stuck transfers
|
|
2159
|
+
- Track average transit times
|
|
2160
|
+
- Implement retry logic for transient failures
|
|
2161
|
+
- Add comprehensive logging for debugging
|
|
2162
|
+
- Test rollback scenarios thoroughly
|