@fluentcommerce/fc-connect-sdk 0.1.53 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -2
- package/README.md +39 -0
- package/dist/cjs/auth/index.d.ts +3 -0
- package/dist/cjs/auth/index.js +13 -0
- package/dist/cjs/auth/profile-loader.d.ts +18 -0
- package/dist/cjs/auth/profile-loader.js +208 -0
- package/dist/cjs/client-factory.d.ts +4 -0
- package/dist/cjs/client-factory.js +10 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/index.d.ts +3 -1
- package/dist/cjs/index.js +8 -2
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/auth/index.d.ts +3 -0
- package/dist/esm/auth/index.js +2 -0
- package/dist/esm/auth/profile-loader.d.ts +18 -0
- package/dist/esm/auth/profile-loader.js +169 -0
- package/dist/esm/client-factory.d.ts +4 -0
- package/dist/esm/client-factory.js +9 -0
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/utils/pagination-helpers.js +38 -2
- package/dist/esm/versori/fluent-versori-client.js +11 -5
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/dist/types/auth/index.d.ts +3 -0
- package/dist/types/auth/profile-loader.d.ts +18 -0
- package/dist/types/client-factory.d.ts +4 -0
- package/dist/types/index.d.ts +3 -1
- package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
- package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
- package/docs/00-START-HERE/cli-documentation-index.md +202 -202
- package/docs/00-START-HERE/cli-quick-reference.md +252 -252
- package/docs/00-START-HERE/decision-tree.md +552 -552
- package/docs/00-START-HERE/getting-started.md +1070 -1070
- package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
- package/docs/00-START-HERE/readme.md +237 -237
- package/docs/00-START-HERE/retailerid-configuration.md +404 -404
- package/docs/00-START-HERE/sdk-philosophy.md +794 -794
- package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
- package/docs/01-TEMPLATES/faq.md +686 -686
- package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
- package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
- package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
- package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
- package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
- package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
- package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
- package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
- package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
- package/docs/01-TEMPLATES/readme.md +957 -957
- package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
- package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
- package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
- package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
- package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
- package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
- package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
- package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
- package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
- package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
- package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
- package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
- package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
- package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
- package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
- package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
- package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
- package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
- package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -482
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
- package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
- package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
- package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
- package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
- package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
- package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
- package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
- package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
- package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
- package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
- package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
- package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
- package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
- package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
- package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
- package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
- package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
- package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
- package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
- package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
- package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
- package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
- package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
- package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
- package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
- package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
- package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
- package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
- package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
- package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
- package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
- package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
- package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
- package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
- package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
- package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
- package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
- package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
- package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
- package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
- package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
- package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
- package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
- package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
- package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
- package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
- package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
- package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
- package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
- package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
- package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
- package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
- package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
- package/docs/02-CORE-GUIDES/readme.md +194 -194
- package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
- package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
- package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
- package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
- package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
- package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
- package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
- package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
- package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
- package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
- package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
- package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
- package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
- package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
- package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
- package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
- package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
- package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
- package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
- package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
- package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
- package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
- package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
- package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
- package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
- package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
- package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/readme.md +159 -159
- package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
- package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
- package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
- package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
- package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
- package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
- package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
- package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
- package/docs/04-REFERENCE/architecture/readme.md +279 -279
- package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
- package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
- package/docs/04-REFERENCE/platforms/readme.md +135 -135
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
- package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
- package/docs/04-REFERENCE/readme.md +148 -148
- package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
- package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
- package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
- package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
- package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
- package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
- package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
- package/docs/04-REFERENCE/schema/readme.md +141 -141
- package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
- package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
- package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
- package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
- package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
- package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
- package/docs/04-REFERENCE/testing/readme.md +86 -86
- package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
- package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
- package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
- package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
- package/docs/template-loading-matrix.md +242 -242
- package/package.json +5 -3
|
@@ -1,2030 +1,2030 @@
|
|
|
1
|
-
---
|
|
2
|
-
template_id: tpl-ingest-cycle-count-reconciliation
|
|
3
|
-
canonical_filename: template-ingestion-cycle-count-reconciliation.md
|
|
4
|
-
sdk_version: ^0.1.39
|
|
5
|
-
runtime: versori
|
|
6
|
-
direction: ingestion
|
|
7
|
-
source: s3-csv|sftp-csv
|
|
8
|
-
destination: fluent-batch-api
|
|
9
|
-
entity: inventory
|
|
10
|
-
format: csv
|
|
11
|
-
logging: versori
|
|
12
|
-
status: stable
|
|
13
|
-
complexity: medium-high
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
# Template: Ingestion - Cycle Count Reconciliation with Batch API
|
|
17
|
-
|
|
18
|
-
## STEP 1: Read Template Header
|
|
19
|
-
|
|
20
|
-
This template is now loaded into your context. Review the metadata above:
|
|
21
|
-
|
|
22
|
-
- **Template ID**: `tpl-ingest-cycle-count-reconciliation`
|
|
23
|
-
- **SDK Version**: `^0.1.30`
|
|
24
|
-
- **Runtime**: Versori Platform
|
|
25
|
-
- **Direction**: Ingestion (data → Fluent Commerce)
|
|
26
|
-
- **Source**: S3 or SFTP CSV files
|
|
27
|
-
- **Destination**: Fluent Batch API
|
|
28
|
-
- **Entity**: InventoryQuantity (inventory adjustments)
|
|
29
|
-
- **Complexity**: Medium-High
|
|
30
|
-
- **Status**: Stable (validated and production-ready)
|
|
31
|
-
|
|
32
|
-
**Before implementing:**
|
|
33
|
-
1. Verify SDK version compatibility: `npm list @fluentcommerce/fc-connect-sdk`
|
|
34
|
-
2. Ensure you have Fluent API credentials (OAuth2: clientId, clientSecret, username, password)
|
|
35
|
-
3. Have S3 or SFTP access credentials ready
|
|
36
|
-
4. Review the workflow overview and entity fields below
|
|
37
|
-
|
|
38
|
-
## STEP 2: Workflow Overview
|
|
39
|
-
|
|
40
|
-
### What This Template Does
|
|
41
|
-
|
|
42
|
-
This template implements a **specialized cycle count reconciliation workflow** for Versori Platform that:
|
|
43
|
-
|
|
44
|
-
1. **Reads physical cycle count files** from S3 or SFTP (CSV format with count data)
|
|
45
|
-
2. **Compares against current Fluent inventory** using bulk GraphQL query for performance
|
|
46
|
-
3. **Calculates variance** with dual threshold logic (percentage AND absolute variance)
|
|
47
|
-
4. **Sends inventory adjustments** via Batch API for items requiring correction
|
|
48
|
-
5. **Generates reconciliation reports** with auto-adjusted vs manual review items
|
|
49
|
-
6. **Tracks file processing** to prevent duplicate reconciliations
|
|
50
|
-
7. **Maintains audit trail** with full variance tracking and metadata
|
|
51
|
-
|
|
52
|
-
### Key SDK Methods Used
|
|
53
|
-
|
|
54
|
-
- **createClient()** - Auto-detect Versori context and create FluentClient
|
|
55
|
-
- **CSVParserService** - Parse cycle count CSV files
|
|
56
|
-
- **UniversalMapper** - Transform CSV records to reconciliation format
|
|
57
|
-
- **S3DataSource / SftpDataSource** - Read files with retry logic and archival
|
|
58
|
-
- **VersoriFileTracker** - Prevent duplicate file processing
|
|
59
|
-
- **JobTracker** - Track multi-step workflow lifecycle
|
|
60
|
-
- **client.graphql()** - Bulk inventory query with auto-pagination
|
|
61
|
-
- **client.createJob()** - Create Batch API job with BPP enabled
|
|
62
|
-
- **client.sendBatch()** - Send inventory adjustments (entityType: 'INVENTORY') - fire-and-forget
|
|
63
|
-
|
|
64
|
-
### Critical Entity Details
|
|
65
|
-
|
|
66
|
-
**Entity**: `InventoryQuantity` (NOT Product)
|
|
67
|
-
|
|
68
|
-
**Batch API EntityType**: `'INVENTORY'`
|
|
69
|
-
|
|
70
|
-
**Required Fields**:
|
|
71
|
-
- `ref` - Composite reference: `${locationRef}:${skuRef}`
|
|
72
|
-
- `locationRef` - Warehouse location identifier
|
|
73
|
-
- `skuRef` - SKU/article reference (NOTE: Field is `skuRef`, not `articleRef` in this context)
|
|
74
|
-
- `qty` - **NEW absolute quantity** (not delta/variance) - CRITICAL!
|
|
75
|
-
- `type` - Inventory type: `'CORRECTION'` for cycle count adjustments
|
|
76
|
-
- `status` - Inventory status: `'AVAILABLE'`
|
|
77
|
-
|
|
78
|
-
**Audit Trail Attributes**:
|
|
79
|
-
- `cycle_count_adjustment` - Flag indicating cycle count origin
|
|
80
|
-
- `original_qty` - System quantity before adjustment
|
|
81
|
-
- `variance_qty` - Calculated variance (counted - system)
|
|
82
|
-
- `count_date` - When physical count was performed
|
|
83
|
-
- `counter_name` - Person who performed count
|
|
84
|
-
- `source_file` - CSV filename for traceability
|
|
85
|
-
|
|
86
|
-
### Variance Calculation Logic
|
|
87
|
-
|
|
88
|
-
**Dual Threshold Approach** (both must be met for auto-adjustment):
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
const percentThreshold = 5; // 5% variance threshold
|
|
92
|
-
const absoluteThreshold = 10; // 10 units variance threshold
|
|
93
|
-
|
|
94
|
-
const varianceQty = countedQty - fluentQty;
|
|
95
|
-
const variancePercent = Math.abs((varianceQty / fluentQty) * 100);
|
|
96
|
-
|
|
97
|
-
// Auto-adjust only if BOTH thresholds are met
|
|
98
|
-
const isWithinThreshold =
|
|
99
|
-
(variancePercent < percentThreshold) &&
|
|
100
|
-
(Math.abs(varianceQty) < absoluteThreshold);
|
|
101
|
-
|
|
102
|
-
const action = isWithinThreshold ? 'AUTO_ADJUST' : 'MANUAL_REVIEW';
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
**Edge Cases Handled**:
|
|
106
|
-
- Zero to zero (0 → 0): No variance
|
|
107
|
-
- Zero to positive (0 → N): 100% variance → Manual review
|
|
108
|
-
- Divide by zero: Safe percentage calculation
|
|
109
|
-
|
|
110
|
-
### Workflow Steps
|
|
111
|
-
|
|
112
|
-
1. **File Discovery** - List CSV files from S3/SFTP with pattern matching
|
|
113
|
-
2. **Duplicate Prevention** - Check VersoriFileTracker for already-processed files
|
|
114
|
-
3. **CSV Parsing** - Parse cycle count data with validation
|
|
115
|
-
4. **Field Mapping** - Transform CSV to reconciliation records via UniversalMapper
|
|
116
|
-
5. **Bulk Inventory Query** - Single GraphQL query for all items (O(1) lookup)
|
|
117
|
-
6. **Variance Calculation** - Compare counted vs system quantities with dual thresholds
|
|
118
|
-
7. **Batch API Send** - Send auto-adjustments via createJob + sendBatch (BPP enabled)
|
|
119
|
-
8. **Status Polling** - Poll batch completion with timeout
|
|
120
|
-
9. **Report Generation** - Create reconciliation report with summary and details
|
|
121
|
-
10. **File Archival** - Move processed file to archive folder
|
|
122
|
-
11. **State Tracking** - Mark file as processed in KV store
|
|
123
|
-
|
|
124
|
-
### GraphQL Query (Bulk Inventory)
|
|
125
|
-
|
|
126
|
-
```graphql
|
|
127
|
-
query GetInventoryForReconciliation($first: Int, $after: String) {
|
|
128
|
-
inventoryQuantities(first: $first, after: $after) {
|
|
129
|
-
edges {
|
|
130
|
-
node {
|
|
131
|
-
id
|
|
132
|
-
locationRef
|
|
133
|
-
skuRef
|
|
134
|
-
qty
|
|
135
|
-
type
|
|
136
|
-
status
|
|
137
|
-
}
|
|
138
|
-
cursor
|
|
139
|
-
}
|
|
140
|
-
pageInfo {
|
|
141
|
-
hasNextPage
|
|
142
|
-
endCursor
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
**Performance**: Single query + Map lookup = O(1) per item (vs O(N) individual queries)
|
|
149
|
-
|
|
150
|
-
### Batch API Integration
|
|
151
|
-
|
|
152
|
-
**BPP (Batch Pre-Processing)**: **ENABLED** (default)
|
|
153
|
-
- Fluent's change detection filters unchanged inventory records
|
|
154
|
-
- Only actual changes trigger workflows
|
|
155
|
-
- Reduces downstream processing load
|
|
156
|
-
|
|
157
|
-
**When to Skip BPP**: Never for cycle count reconciliation (full accuracy required)
|
|
158
|
-
|
|
159
|
-
### No Hallucinated Methods
|
|
160
|
-
|
|
161
|
-
All methods used in this template are verified SDK methods:
|
|
162
|
-
- ✅ `createClient()` - Client factory
|
|
163
|
-
- ✅ `CSVParserService.parse()` - CSV parsing
|
|
164
|
-
- ✅ `UniversalMapper.mapBatch()` - Batch transformation
|
|
165
|
-
- ✅ `S3DataSource.listFiles()` / `readFile()` / `moveFile()` - S3 operations
|
|
166
|
-
- ✅ `SftpDataSource.listFiles()` / `downloadFile()` / `moveFile()` - SFTP operations
|
|
167
|
-
- ✅ `VersoriFileTracker.wasFileProcessed()` / `markFileProcessed()` - File tracking
|
|
168
|
-
- ✅ `JobTracker.createJob()` / `updateJob()` / `markCompleted()` - Job tracking
|
|
169
|
-
- ✅ `client.graphql()` - GraphQL query with auto-pagination
|
|
170
|
-
- ✅ `client.createJob()` - Batch job creation
|
|
171
|
-
- ✅ `client.sendBatch()` - Batch send (fire-and-forget)
|
|
172
|
-
|
|
173
|
-
### Workflows Provided
|
|
174
|
-
|
|
175
|
-
1. **Scheduled Reconciliation** (`scheduledReconciliation`)
|
|
176
|
-
- Cron: Daily at 2 AM UTC
|
|
177
|
-
- Auto-triggered with JobTracker integration
|
|
178
|
-
|
|
179
|
-
2. **Ad Hoc Reconciliation** (`adhocReconciliation`)
|
|
180
|
-
- Manual webhook trigger with API key authentication
|
|
181
|
-
- Supports `forceReprocess` flag to override file tracking
|
|
182
|
-
|
|
183
|
-
3. **Job Status Query** (`reconciliationJobStatus`)
|
|
184
|
-
- Webhook endpoint to query job status by `jobId`
|
|
185
|
-
- Returns job lifecycle state and results
|
|
186
|
-
|
|
187
|
-
---
|
|
188
|
-
|
|
189
|
-
**FC Connect SDK Use Case Guide**
|
|
190
|
-
|
|
191
|
-
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
192
|
-
> **Version**: `^0.1.30`
|
|
193
|
-
|
|
194
|
-
**Context**: Scheduled Versori workflow that reads physical cycle count CSV files from S3/SFTP, compares against current Fluent inventory, calculates variances with dual threshold logic, and sends automatic adjustments via Batch API with full audit trail.
|
|
195
|
-
|
|
196
|
-
**Complexity**: Medium-High
|
|
197
|
-
|
|
198
|
-
**Runtime**: Versori Platform (Scheduled + Manual Trigger + Status Query)
|
|
199
|
-
|
|
200
|
-
**Estimated Lines**: ~950
|
|
201
|
-
|
|
202
|
-
## Versori Workflows Structure
|
|
203
|
-
|
|
204
|
-
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
205
|
-
|
|
206
|
-
**Trigger Types:**
|
|
207
|
-
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
208
|
-
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
209
|
-
- **`workflow()`** → Durable workflows (advanced, rarely used)
|
|
210
|
-
|
|
211
|
-
**Execution Steps (chained to triggers):**
|
|
212
|
-
- **`http()`** → External API calls (chained from schedule/webhook)
|
|
213
|
-
- **`fn()`** → Internal processing (chained from schedule/webhook)
|
|
214
|
-
|
|
215
|
-
### Recommended Project Structure
|
|
216
|
-
|
|
217
|
-
```
|
|
218
|
-
data-batch-sync/
|
|
219
|
-
├── index.ts # Entry point - exports all workflows
|
|
220
|
-
└── src/
|
|
221
|
-
├── workflows/
|
|
222
|
-
│ ├── scheduled/
|
|
223
|
-
│ │ └── daily-data-sync.ts # Scheduled: Daily data sync
|
|
224
|
-
│ │
|
|
225
|
-
│ └── webhook/
|
|
226
|
-
│ ├── adhoc-data-sync.ts # Webhook: Manual trigger
|
|
227
|
-
│ └── job-status-check.ts # Webhook: Status query
|
|
228
|
-
│
|
|
229
|
-
├── services/
|
|
230
|
-
│ └── data-sync.service.ts # Shared orchestration logic (reusable)
|
|
231
|
-
│
|
|
232
|
-
└── types/
|
|
233
|
-
└── data.types.ts # Shared type definitions
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
**Benefits:**
|
|
237
|
-
- ✅ Clear trigger separation (`scheduled/` vs `webhook/`)
|
|
238
|
-
- ✅ Descriptive file names (easy to browse and understand)
|
|
239
|
-
- ✅ Scalable (add new workflows without cluttering)
|
|
240
|
-
- ✅ Reusable code in `services/` (DRY principle)
|
|
241
|
-
- ✅ Easy to modify individual workflows without affecting others
|
|
242
|
-
|
|
243
|
-
---
|
|
244
|
-
|
|
245
|
-
## Workflow Files
|
|
246
|
-
|
|
247
|
-
### 1. Scheduled Workflows (`src/workflows/scheduled/`)
|
|
248
|
-
|
|
249
|
-
All time-based triggers that run automatically on cron schedules.
|
|
250
|
-
|
|
251
|
-
#### `src/workflows/scheduled/daily-data-sync.ts`
|
|
252
|
-
|
|
253
|
-
**Purpose**: Automatic Daily data sync
|
|
254
|
-
**Trigger**: Cron schedule (`0 2 * * *`)
|
|
255
|
-
**Exposed as Endpoint**: ❌ NO - Runs automatically
|
|
256
|
-
|
|
257
|
-
```typescript
|
|
258
|
-
import { schedule, http } from '@versori/run';
|
|
259
|
-
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
260
|
-
import { runIngestion } from '../../services/data-sync.service.ts';
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Scheduled Workflow: Daily Data Sync
|
|
264
|
-
*
|
|
265
|
-
* Runs automatically daily at 2 AM UTC
|
|
266
|
-
* NOT exposed as HTTP endpoint - Versori executes on schedule
|
|
267
|
-
*
|
|
268
|
-
* Uses shared service: data-sync.service.ts
|
|
269
|
-
*/
|
|
270
|
-
export const daily_data_sync = schedule(
|
|
271
|
-
'data-batch-scheduled',
|
|
272
|
-
'0 2 * * *' // Daily at 2 AM UTC
|
|
273
|
-
).then(
|
|
274
|
-
http('run-data-batch', { connection: 'fluent_commerce' }, async ctx => {
|
|
275
|
-
const { log, openKv } = ctx;
|
|
276
|
-
const jobId = `data-batch-${Date.now()}`;
|
|
277
|
-
const tracker = new JobTracker(openKv(':project:'), log);
|
|
278
|
-
|
|
279
|
-
await tracker.createJob(jobId, { triggeredBy: 'schedule', stage: 'initialization' });
|
|
280
|
-
await tracker.updateJob(jobId, { status: 'processing' });
|
|
281
|
-
|
|
282
|
-
try {
|
|
283
|
-
// Reuse shared orchestration logic
|
|
284
|
-
const result = await runIngestion(ctx, jobId, tracker);
|
|
285
|
-
await tracker.markCompleted(jobId, result);
|
|
286
|
-
return { success: true, jobId, ...result };
|
|
287
|
-
} catch (e: any) {
|
|
288
|
-
await tracker.markFailed(jobId, e);
|
|
289
|
-
return { success: false, jobId, error: e?.message };
|
|
290
|
-
}
|
|
291
|
-
})
|
|
292
|
-
);
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
---
|
|
296
|
-
|
|
297
|
-
### 2. Webhook Workflows (`src/workflows/webhook/`)
|
|
298
|
-
|
|
299
|
-
All HTTP-based triggers that create webhook endpoints.
|
|
300
|
-
|
|
301
|
-
#### `src/workflows/webhook/adhoc-data-sync.ts`
|
|
302
|
-
|
|
303
|
-
**Purpose**: Manual data sync trigger (on-demand)
|
|
304
|
-
**Trigger**: HTTP POST
|
|
305
|
-
**Endpoint**: `POST https://{workspace}.versori.run/data-batch-adhoc`
|
|
306
|
-
**Use Cases**: Testing, priority processing, ad-hoc runs
|
|
307
|
-
|
|
308
|
-
```typescript
|
|
309
|
-
import { webhook, http } from '@versori/run';
|
|
310
|
-
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
311
|
-
import { runIngestion } from '../../services/data-sync.service.ts';
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Webhook: Manual Data Sync Trigger
|
|
315
|
-
*
|
|
316
|
-
* Endpoint: POST https://{workspace}.versori.run/data-batch-adhoc
|
|
317
|
-
* Request body (optional): { filePattern: "urgent_*.xml", maxFiles: 5 }
|
|
318
|
-
*
|
|
319
|
-
* Pattern: webhook().then(http()) - needs Fluent API access
|
|
320
|
-
* Uses shared service: data-sync.service.ts
|
|
321
|
-
*/
|
|
322
|
-
export const adhoc_data_sync = webhook('data-batch-adhoc', {
|
|
323
|
-
response: { mode: 'sync' },
|
|
324
|
-
connection: 'data-batch-adhoc', // Versori validates API key
|
|
325
|
-
}).then(
|
|
326
|
-
http('run-data-batch-adhoc', { connection: 'fluent_commerce' }, async ctx => {
|
|
327
|
-
const { log, openKv, data } = ctx;
|
|
328
|
-
const jobId = `data-batch-adhoc-${Date.now()}`;
|
|
329
|
-
const tracker = new JobTracker(openKv(':project:'), log);
|
|
330
|
-
|
|
331
|
-
await tracker.createJob(jobId, {
|
|
332
|
-
triggeredBy: 'manual',
|
|
333
|
-
stage: 'initialization',
|
|
334
|
-
options: data // Optional: filePattern, maxFiles, etc.
|
|
335
|
-
});
|
|
336
|
-
await tracker.updateJob(jobId, { status: 'processing' });
|
|
337
|
-
|
|
338
|
-
try {
|
|
339
|
-
// Same orchestration logic as scheduled workflow
|
|
340
|
-
const result = await runIngestion(ctx, jobId, tracker);
|
|
341
|
-
await tracker.markCompleted(jobId, result);
|
|
342
|
-
return { success: true, jobId, ...result };
|
|
343
|
-
} catch (e: any) {
|
|
344
|
-
await tracker.markFailed(jobId, e);
|
|
345
|
-
return { success: false, jobId, error: e?.message };
|
|
346
|
-
}
|
|
347
|
-
})
|
|
348
|
-
);
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
#### `src/workflows/webhook/job-status-check.ts`
|
|
352
|
-
|
|
353
|
-
**Purpose**: Query job status
|
|
354
|
-
**Trigger**: HTTP POST
|
|
355
|
-
**Endpoint**: `POST https://{workspace}.versori.run/data-batch-job-status`
|
|
356
|
-
**Request body**: `{ jobId: "data-batch-1234567890" }`
|
|
357
|
-
|
|
358
|
-
```typescript
|
|
359
|
-
import { webhook, fn } from '@versori/run';
|
|
360
|
-
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Webhook: Job Status Check
|
|
364
|
-
*
|
|
365
|
-
* Endpoint: POST https://{workspace}.versori.run/data-batch-job-status
|
|
366
|
-
* Request body: { jobId: "data-batch-1234567890" }
|
|
367
|
-
*
|
|
368
|
-
* Pattern: webhook().then(fn()) - no external API needed, only KV storage
|
|
369
|
-
* Lightweight: Only queries KV store, no Fluent API calls
|
|
370
|
-
*/
|
|
371
|
-
export const jobStatusCheck = webhook('data-batch-job-status', {
|
|
372
|
-
response: { mode: 'sync' },
|
|
373
|
-
connection: 'data-batch-job-status',
|
|
374
|
-
}).then(
|
|
375
|
-
fn('status', async ctx => {
|
|
376
|
-
const { data, log, openKv } = ctx;
|
|
377
|
-
const jobId = data?.jobId as string;
|
|
378
|
-
|
|
379
|
-
if (!jobId) {
|
|
380
|
-
return { success: false, error: 'jobId required' };
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const tracker = new JobTracker(openKv(':project:'), log);
|
|
384
|
-
const status = await tracker.getJob(jobId);
|
|
385
|
-
|
|
386
|
-
return status
|
|
387
|
-
? { success: true, jobId, ...status }
|
|
388
|
-
: { success: false, error: 'Job not found', jobId };
|
|
389
|
-
})
|
|
390
|
-
);
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
---
|
|
394
|
-
|
|
395
|
-
### 3. Entry Point (`index.ts`)
|
|
396
|
-
|
|
397
|
-
**Purpose**: Register all workflows with Versori platform
|
|
398
|
-
|
|
399
|
-
```typescript
|
|
400
|
-
/**
|
|
401
|
-
* Entry Point - Registers all workflows with Versori platform
|
|
402
|
-
*
|
|
403
|
-
* Versori automatically discovers and registers exported workflows
|
|
404
|
-
*
|
|
405
|
-
* File Structure:
|
|
406
|
-
* - src/workflows/scheduled/ → Time-based triggers (cron)
|
|
407
|
-
* - src/workflows/webhook/ → HTTP-based triggers (webhooks)
|
|
408
|
-
*/
|
|
409
|
-
|
|
410
|
-
// Import scheduled workflows
|
|
411
|
-
import { daily_data_sync } from './src/workflows/scheduled/daily-data-sync';
|
|
412
|
-
|
|
413
|
-
// Import webhook workflows
|
|
414
|
-
import { adhoc_data_sync } from './src/workflows/webhook/adhoc-data-sync';
|
|
415
|
-
import { jobStatusCheck } from './src/workflows/webhook/job-status-check';
|
|
416
|
-
|
|
417
|
-
// Register all workflows
|
|
418
|
-
export {
|
|
419
|
-
// Scheduled (time-based triggers)
|
|
420
|
-
daily_data_sync,
|
|
421
|
-
|
|
422
|
-
// Webhooks (HTTP-based triggers)
|
|
423
|
-
adhoc_data_sync,
|
|
424
|
-
jobStatusCheck,
|
|
425
|
-
};
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
**What Gets Exposed:**
|
|
429
|
-
|
|
430
|
-
- ✅ `adhoc_data_sync` → `https://{workspace}.versori.run/data-batch-adhoc`
|
|
431
|
-
- ✅ `jobStatusCheck` → `https://{workspace}.versori.run/data-batch-job-status`
|
|
432
|
-
- ❌ `daily_data_sync` → NOT exposed (runs automatically on cron)
|
|
433
|
-
|
|
434
|
-
---
|
|
435
|
-
|
|
436
|
-
### Adding New Workflows
|
|
437
|
-
|
|
438
|
-
**To add a scheduled workflow:**
|
|
439
|
-
1. Create file in `src/workflows/scheduled/` with descriptive name (e.g., `hourly-delta-sync.ts`)
|
|
440
|
-
2. Export the workflow from the file
|
|
441
|
-
3. Import and re-export in `index.ts`
|
|
442
|
-
|
|
443
|
-
**To add a webhook workflow:**
|
|
444
|
-
1. Create file in `src/workflows/webhook/` with descriptive name (e.g., `error-notification.ts`)
|
|
445
|
-
2. Export the workflow from the file
|
|
446
|
-
3. Import and re-export in `index.ts`
|
|
447
|
-
|
|
448
|
-
**Example - Adding hourly delta sync:**
|
|
449
|
-
|
|
450
|
-
```typescript
|
|
451
|
-
// src/workflows/scheduled/hourly-delta-sync.ts
|
|
452
|
-
export const hourlyDeltaSync = schedule(
|
|
453
|
-
'data-delta-hourly',
|
|
454
|
-
'0 * * * *' // Every hour
|
|
455
|
-
).then(
|
|
456
|
-
http('run-delta-sync', { connection: 'fluent_commerce' }, async ctx => {
|
|
457
|
-
// Delta sync logic (skip BPP)
|
|
458
|
-
const result = await runIngestion(ctx, jobId, tracker, { bppEnabled: false });
|
|
459
|
-
return result;
|
|
460
|
-
})
|
|
461
|
-
);
|
|
462
|
-
|
|
463
|
-
// index.ts (add to imports and exports)
|
|
464
|
-
import { hourlyDeltaSync } from './src/workflows/scheduled/hourly-delta-sync';
|
|
465
|
-
export { daily_data_sync, hourlyDeltaSync, ... };
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
---
|
|
469
|
-
## What You'll Build
|
|
470
|
-
|
|
471
|
-
- Versori scheduled reconciliation workflow (daily) with manual webhook trigger and job status query
|
|
472
|
-
- Flexible data source (S3 or SFTP) with file tracking and archival
|
|
473
|
-
- CSV parsing with validation
|
|
474
|
-
- Bulk GraphQL query for current inventory (single query, O(1) lookup)
|
|
475
|
-
- Variance calculation with edge case handling (zero-to-zero, divide-by-zero)
|
|
476
|
-
- Dual threshold logic (percentage AND absolute variance)
|
|
477
|
-
- Batch API adjustments with full audit trail attributes
|
|
478
|
-
- Reconciliation report generation (auto-adjusted vs manual review items)
|
|
479
|
-
- JobTracker integration for multi-step workflow visibility
|
|
480
|
-
- VersoriFileTracker for duplicate prevention
|
|
481
|
-
|
|
482
|
-
## Workflows (scheduled, ad hoc, job status)
|
|
483
|
-
|
|
484
|
-
**Security Note:** Webhook authentication is handled by Versori via the `connection` parameter in the webhook configuration. No manual API key validation needed.
|
|
485
|
-
|
|
486
|
-
```typescript
|
|
487
|
-
import { schedule, webhook, http, fn } from '@versori/run';
|
|
488
|
-
import { Buffer } from 'node:buffer'; // Required for Versori/Deno runtime
|
|
489
|
-
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
490
|
-
|
|
491
|
-
export const scheduledReconciliation = schedule('cycle-count-recon-scheduled', '0 2 * * *').then(
|
|
492
|
-
http('run-reconciliation', { connection: 'fluent_commerce' }, async ctx => {
|
|
493
|
-
const { log, openKv } = ctx;
|
|
494
|
-
const jobId = `cycle-count-recon-${Date.now()}`;
|
|
495
|
-
const tracker = new JobTracker(openKv(':project:'), log);
|
|
496
|
-
await tracker.createJob(jobId, { triggeredBy: 'schedule', stage: 'initialization' });
|
|
497
|
-
await tracker.updateJob(jobId, { status: 'processing' });
|
|
498
|
-
try {
|
|
499
|
-
const result = await runReconciliation(ctx, jobId, tracker);
|
|
500
|
-
await tracker.markCompleted(jobId, result);
|
|
501
|
-
return { success: true, jobId, ...result };
|
|
502
|
-
} catch (e: any) {
|
|
503
|
-
await tracker.markFailed(jobId, e);
|
|
504
|
-
return { success: false, jobId, error: e?.message };
|
|
505
|
-
}
|
|
506
|
-
})
|
|
507
|
-
);
|
|
508
|
-
|
|
509
|
-
export const adhocReconciliation = webhook('cycle-count-recon-adhoc', {
|
|
510
|
-
response: { mode: 'sync' },
|
|
511
|
-
connection: 'cycle-count-recon-adhoc', // Webhook auth (validates incoming X-API-Key)
|
|
512
|
-
}).then(
|
|
513
|
-
http('run-reconciliation-adhoc', { connection: 'fluent_commerce' }, async ctx => {
|
|
514
|
-
const { data, log, openKv } = ctx;
|
|
515
|
-
const jobId = `cycle-count-recon-adhoc-${Date.now()}`;
|
|
516
|
-
const tracker = new JobTracker(openKv(':project:'), log);
|
|
517
|
-
await tracker.createJob(jobId, { triggeredBy: 'manual', stage: 'initialization', details: { forceReprocess: data?.forceReprocess } });
|
|
518
|
-
await tracker.updateJob(jobId, { status: 'processing' });
|
|
519
|
-
try {
|
|
520
|
-
const result = await runReconciliation(ctx, jobId, tracker, data?.forceReprocess);
|
|
521
|
-
await tracker.markCompleted(jobId, result);
|
|
522
|
-
return { success: true, jobId, ...result };
|
|
523
|
-
} catch (e: any) {
|
|
524
|
-
await tracker.markFailed(jobId, e);
|
|
525
|
-
return { success: false, jobId, error: e?.message };
|
|
526
|
-
}
|
|
527
|
-
})
|
|
528
|
-
);
|
|
529
|
-
|
|
530
|
-
export const reconciliationJobStatus = webhook('cycle-count-recon-job-status', {
|
|
531
|
-
response: { mode: 'sync' },
|
|
532
|
-
connection: 'cycle-count-recon-job-status', // Webhook auth (validates incoming X-API-Key)
|
|
533
|
-
}).then(
|
|
534
|
-
fn('status', async (ctx) => {
|
|
535
|
-
const { data, log, openKv } = ctx;
|
|
536
|
-
const jobId = data?.jobId as string;
|
|
537
|
-
if (!jobId) return { success: false, error: 'jobId required' };
|
|
538
|
-
const tracker = new JobTracker(openKv(':project:'), log);
|
|
539
|
-
const status = await tracker.getJob(jobId);
|
|
540
|
-
return status
|
|
541
|
-
? { success: true, jobId, ...status }
|
|
542
|
-
: { success: false, error: 'Job not found', jobId };
|
|
543
|
-
})
|
|
544
|
-
);
|
|
545
|
-
|
|
546
|
-
// Ensure runReconciliation(ctx, jobId, tracker, forceReprocess) uses tracker.start/update/complete/fail for major steps
|
|
547
|
-
```
|
|
548
|
-
|
|
549
|
-
## SDK Methods Used
|
|
550
|
-
|
|
551
|
-
```typescript
|
|
552
|
-
import {
|
|
553
|
-
createClient,
|
|
554
|
-
CSVParserService,
|
|
555
|
-
UniversalMapper,
|
|
556
|
-
S3DataSource,
|
|
557
|
-
SftpDataSource,
|
|
558
|
-
VersoriFileTracker,
|
|
559
|
-
JobTracker,
|
|
560
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
561
|
-
import cycleCountMapping from './config/cycle-count.mapping.json' with { type: 'json' };
|
|
562
|
-
|
|
563
|
-
await createClient(ctx); // Versori context-aware client
|
|
564
|
-
const s3 = new S3DataSource({ type: 'S3_CSV', connectionId, name, s3Config }, log); // S3 ops
|
|
565
|
-
const sftp = new SftpDataSource({ type: 'SFTP_CSV', connectionId, name, sftpConfig }, log); // SFTP ops
|
|
566
|
-
const parser = new CSVParserService(); // CSV parsing
|
|
567
|
-
const mapper = new UniversalMapper(cycleCountMapping); // Transform cycle count records
|
|
568
|
-
const fileTracker = new VersoriFileTracker(openKv(':project:'), 'cycle-count-recon'); // File tracking
|
|
569
|
-
const jobTracker = new JobTracker(openKv(':project:'), log); // Job lifecycle tracking
|
|
570
|
-
await client.graphql({ query, variables, pagination: { maxPages: 100 } }); // Bulk inventory query
|
|
571
|
-
await client.createJob({ name, retailerId }); // Batch job create
|
|
572
|
-
await client.sendBatch(jobId, { action, entityType, entities }); // Send adjustments (fire-and-forget)
|
|
573
|
-
```
|
|
574
|
-
|
|
575
|
-
## Config blocks (for PDF/AI ingestion)
|
|
576
|
-
|
|
577
|
-
### Activation Variables (S3)
|
|
578
|
-
|
|
579
|
-
```json
|
|
580
|
-
{
|
|
581
|
-
"dataSource": "s3",
|
|
582
|
-
"s3BucketName": "wms-cycle-counts",
|
|
583
|
-
"awsRegion": "us-east-1",
|
|
584
|
-
"awsAccessKeyId": "AKIA...",
|
|
585
|
-
"awsSecretAccessKey": "...",
|
|
586
|
-
"s3Prefix": "cycle-counts/incoming/",
|
|
587
|
-
"archivePrefix": "cycle-counts/processed/",
|
|
588
|
-
"errorPrefix": "cycle-counts/errors/",
|
|
589
|
-
"filePattern": "cycle-count-*.csv",
|
|
590
|
-
"maxFilesPerRun": 5,
|
|
591
|
-
"variancePercentThreshold": 5,
|
|
592
|
-
"varianceAbsoluteThreshold": 10,
|
|
593
|
-
"enableAutoAdjustment": "true",
|
|
594
|
-
"enableArchival": "true",
|
|
595
|
-
"retailerId": "your-retailer-id"
|
|
596
|
-
}
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
### Activation Variables (SFTP)
|
|
600
|
-
|
|
601
|
-
```json
|
|
602
|
-
{
|
|
603
|
-
"dataSource": "sftp",
|
|
604
|
-
"sftpHost": "sftp.warehouse.com",
|
|
605
|
-
"sftpPort": 22,
|
|
606
|
-
"sftpUsername": "cycle_count_user",
|
|
607
|
-
"sftpPassword": "...",
|
|
608
|
-
"sftpIncomingPath": "/cycle-counts/incoming",
|
|
609
|
-
"sftpProcessedPath": "/cycle-counts/processed",
|
|
610
|
-
"sftpErrorPath": "/cycle-counts/errors",
|
|
611
|
-
"filePattern": "cycle-count-*.csv",
|
|
612
|
-
"maxFilesPerRun": 5,
|
|
613
|
-
"variancePercentThreshold": 5,
|
|
614
|
-
"varianceAbsoluteThreshold": 10,
|
|
615
|
-
"enableAutoAdjustment": "true",
|
|
616
|
-
"retailerId": "your-retailer-id"
|
|
617
|
-
}
|
|
618
|
-
```
|
|
619
|
-
|
|
620
|
-
### Batch Payload Defaults
|
|
621
|
-
|
|
622
|
-
```json
|
|
623
|
-
{
|
|
624
|
-
"action": "UPSERT",
|
|
625
|
-
"entityType": "INVENTORY",
|
|
626
|
-
"source": "CYCLE_COUNT",
|
|
627
|
-
"event": "CYCLE_COUNT_RECONCILIATION",
|
|
628
|
-
"bpp": "enabled"
|
|
629
|
-
}
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
## Sample CSV Input Data
|
|
633
|
-
|
|
634
|
-
```csv
|
|
635
|
-
location_code,sku,counted_quantity,count_date,counter_username,bin_location,lot_number
|
|
636
|
-
DC01,ACME-WIDGET-100,245,2025-01-17T08:30:00Z,john.smith,A-01-05,LOT20250115
|
|
637
|
-
DC01,ACME-GADGET-200,0,2025-01-17T08:32:00Z,john.smith,A-01-06,
|
|
638
|
-
DC01,ACME-TOOL-300,1520,2025-01-17T08:35:00Z,john.smith,A-02-01,LOT20250110
|
|
639
|
-
DC02,ACME-WIDGET-100,87,2025-01-17T09:00:00Z,jane.doe,B-03-12,LOT20250115
|
|
640
|
-
DC02,ACME-PART-400,2340,2025-01-17T09:05:00Z,jane.doe,B-03-13,LOT20250112
|
|
641
|
-
DC02,ACME-COMPONENT-500,45,2025-01-17T09:10:00Z,jane.doe,B-04-01,
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
### Column Descriptions
|
|
645
|
-
|
|
646
|
-
- `location_code`: Warehouse location identifier (maps to `locationRef`)
|
|
647
|
-
- `sku`: Product SKU (maps to `skuRef`)
|
|
648
|
-
- `counted_quantity`: Physical count from warehouse team
|
|
649
|
-
- `count_date`: ISO 8601 timestamp when count was performed
|
|
650
|
-
- `counter_username`: Username of person who performed count
|
|
651
|
-
- `bin_location`: Specific bin/shelf location within warehouse
|
|
652
|
-
- `lot_number`: Lot/batch number if applicable (for lot-tracked items)
|
|
653
|
-
|
|
654
|
-
### Mapping file (create at ./config/cycle-count.mapping.json)
|
|
655
|
-
|
|
656
|
-
```text
|
|
657
|
-
Create file: ./config/cycle-count.mapping.json
|
|
658
|
-
|
|
659
|
-
{
|
|
660
|
-
"name": "cycle-count.mapping",
|
|
661
|
-
"version": "1.0.0",
|
|
662
|
-
"description": "CSV cycle count to reconciliation record mapping",
|
|
663
|
-
"fields": {
|
|
664
|
-
"locationRef": { "source": "location_code", "required": true, "resolver": "sdk.trim" },
|
|
665
|
-
"skuRef": { "source": "sku", "required": true, "resolver": "sdk.trim" },
|
|
666
|
-
"countedQty": { "source": "counted_quantity", "required": true, "resolver": "sdk.parseInt" },
|
|
667
|
-
"countDate": { "source": "count_date", "required": false },
|
|
668
|
-
"counterName": { "source": "counter_username", "required": false },
|
|
669
|
-
"binLocation": { "source": "bin_location", "required": false },
|
|
670
|
-
"lotNumber": { "source": "lot_number", "required": false }
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
```
|
|
674
|
-
|
|
675
|
-
**Field Mapping**:
|
|
676
|
-
|
|
677
|
-
- `locationRef` → Fluent location reference
|
|
678
|
-
- `skuRef` → Fluent SKU reference
|
|
679
|
-
- `countedQty` → Physical counted quantity (integer)
|
|
680
|
-
- `countDate` → When count was performed (ISO 8601 timestamp)
|
|
681
|
-
- `counterName` → Person who performed count (for audit trail)
|
|
682
|
-
- `binLocation` → Specific bin/shelf location
|
|
683
|
-
- `lotNumber` → Lot/batch number for lot-tracked items
|
|
684
|
-
|
|
685
|
-
## Project Setup
|
|
686
|
-
|
|
687
|
-
```bash
|
|
688
|
-
mkdir versori-cycle-count-reconciliation && cd $_
|
|
689
|
-
npm init -y
|
|
690
|
-
npm install @fluentcommerce/fc-connect-sdk @versori/run
|
|
691
|
-
mkdir -p config
|
|
692
|
-
```
|
|
693
|
-
|
|
694
|
-
### Package Configuration (package.json)
|
|
695
|
-
|
|
696
|
-
```json
|
|
697
|
-
{
|
|
698
|
-
"name": "versori-cycle-count-reconciliation",
|
|
699
|
-
"version": "1.0.0",
|
|
700
|
-
"description": "Versori workflow: Cycle count reconciliation with variance analysis and Batch API",
|
|
701
|
-
"versori": {
|
|
702
|
-
"workflows": "./index.ts"
|
|
703
|
-
},
|
|
704
|
-
"type": "module",
|
|
705
|
-
"scripts": {
|
|
706
|
-
"deploy": "versori deploy",
|
|
707
|
-
"logs": "versori logs"
|
|
708
|
-
},
|
|
709
|
-
"dependencies": {
|
|
710
|
-
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
711
|
-
"@versori/run": "latest"
|
|
712
|
-
},
|
|
713
|
-
"devDependencies": {
|
|
714
|
-
"typescript": "^5.0.0",
|
|
715
|
-
"@types/node": "^20.0.0"
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
```
|
|
719
|
-
|
|
720
|
-
### TypeScript Configuration (tsconfig.json)
|
|
721
|
-
|
|
722
|
-
```json
|
|
723
|
-
{
|
|
724
|
-
"compilerOptions": {
|
|
725
|
-
"module": "ES2022",
|
|
726
|
-
"target": "ES2024",
|
|
727
|
-
"moduleResolution": "node"
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
```
|
|
731
|
-
|
|
732
|
-
### Activation Variables (Versori)
|
|
733
|
-
|
|
734
|
-
**For S3:**
|
|
735
|
-
```bash
|
|
736
|
-
# Required Variables
|
|
737
|
-
dataSource=s3
|
|
738
|
-
s3BucketName=wms-cycle-counts
|
|
739
|
-
awsAccessKeyId=AKIAXXXXXXXXXXXX
|
|
740
|
-
awsSecretAccessKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
741
|
-
retailerId=your-retailer-id
|
|
742
|
-
|
|
743
|
-
# Optional Variables (with defaults shown)
|
|
744
|
-
awsRegion=us-east-1
|
|
745
|
-
s3Prefix=cycle-counts/incoming/
|
|
746
|
-
archivePrefix=cycle-counts/processed/
|
|
747
|
-
errorPrefix=cycle-counts/errors/
|
|
748
|
-
filePattern=cycle-count-*.csv
|
|
749
|
-
maxFilesPerRun=5
|
|
750
|
-
variancePercentThreshold=5
|
|
751
|
-
varianceAbsoluteThreshold=10
|
|
752
|
-
enableAutoAdjustment=true
|
|
753
|
-
enableArchival=true
|
|
754
|
-
```
|
|
755
|
-
|
|
756
|
-
**For SFTP:**
|
|
757
|
-
```bash
|
|
758
|
-
# Required Variables
|
|
759
|
-
dataSource=sftp
|
|
760
|
-
sftpHost=sftp.warehouse.com
|
|
761
|
-
sftpPort=22
|
|
762
|
-
sftpUsername=cycle_count_user
|
|
763
|
-
sftpPassword=xxxxxxxx
|
|
764
|
-
sftpIncomingPath=/cycle-counts/incoming
|
|
765
|
-
sftpProcessedPath=/cycle-counts/processed
|
|
766
|
-
sftpErrorPath=/cycle-counts/errors
|
|
767
|
-
retailerId=your-retailer-id
|
|
768
|
-
|
|
769
|
-
# Optional Variables (with defaults shown)
|
|
770
|
-
filePattern=cycle-count-*.csv
|
|
771
|
-
maxFilesPerRun=5
|
|
772
|
-
variancePercentThreshold=5
|
|
773
|
-
varianceAbsoluteThreshold=10
|
|
774
|
-
enableAutoAdjustment=true
|
|
775
|
-
```
|
|
776
|
-
|
|
777
|
-
**Security Note:** Webhook authentication is now handled by Versori via the `connection` parameter. No manual API key validation needed.
|
|
778
|
-
|
|
779
|
-
---
|
|
780
|
-
|
|
781
|
-
## 🏗️ Production Modular Structure Implementation
|
|
782
|
-
|
|
783
|
-
> **✅ This section shows the COMPLETE production-ready modular structure.**
|
|
784
|
-
> All files are shown with proper imports/exports and folder organization.
|
|
785
|
-
> This follows the gold standard pattern from template-ingestion-sftp-xml-inventory-batch.md
|
|
786
|
-
|
|
787
|
-
### Complete Project Structure
|
|
788
|
-
|
|
789
|
-
```
|
|
790
|
-
cycle-count-reconciliation/
|
|
791
|
-
├── package.json # Dependencies and Versori config
|
|
792
|
-
├── tsconfig.json # TypeScript configuration
|
|
793
|
-
├── src/
|
|
794
|
-
│ ├── index.ts # Workflow entry point (exports)
|
|
795
|
-
│ ├── workflows/
|
|
796
|
-
│ │ └── cycle-count-reconciliation.workflow.ts # Main orchestrator
|
|
797
|
-
│ ├── services/
|
|
798
|
-
│ │ ├── inventory-query.service.ts # Bulk GraphQL inventory queries
|
|
799
|
-
│ │ ├── variance-calculator.service.ts # Variance calculation with dual thresholds
|
|
800
|
-
│ │ ├── batch-processor.service.ts # Batch API submission logic
|
|
801
|
-
│ │ └── report-generator.service.ts # Reconciliation report generation
|
|
802
|
-
│ ├── types/
|
|
803
|
-
│ │ └── cycle-count-types.ts # TypeScript interfaces
|
|
804
|
-
│ ├── utils/
|
|
805
|
-
│ │ ├── retry.ts # Retry with exponential backoff
|
|
806
|
-
│ │ └── data-source-factory.ts # S3/SFTP initialization
|
|
807
|
-
│ └── config/
|
|
808
|
-
│ └── cycle-count.mapping.json # Mapping configuration (external JSON)
|
|
809
|
-
```
|
|
810
|
-
|
|
811
|
-
---
|
|
812
|
-
|
|
813
|
-
### File: `src/index.ts`
|
|
814
|
-
|
|
815
|
-
```typescript
|
|
816
|
-
import { schedule, webhook, http, fn } from '@versori/run';
|
|
817
|
-
import { processReconciliation, getReconciliationStatus } from './workflows/cycle-count-reconciliation.workflow';
|
|
818
|
-
|
|
819
|
-
/**
|
|
820
|
-
* Scheduled workflow: Daily cycle count reconciliation
|
|
821
|
-
* Runs daily at 2 AM UTC
|
|
822
|
-
*/
|
|
823
|
-
export const scheduledReconciliation = schedule(
|
|
824
|
-
'cycle-count-recon-scheduled',
|
|
825
|
-
'0 2 * * *' // 2 AM UTC daily
|
|
826
|
-
).then(
|
|
827
|
-
http('run-reconciliation', { connection: 'fluent_commerce' }, async ctx => {
|
|
828
|
-
return await processReconciliation(ctx, { triggeredBy: 'schedule' });
|
|
829
|
-
})
|
|
830
|
-
);
|
|
831
|
-
|
|
832
|
-
/**
|
|
833
|
-
* Manual trigger endpoint for ad-hoc reconciliation
|
|
834
|
-
*/
|
|
835
|
-
export const adhocReconciliation = webhook('cycle-count-recon-adhoc', {
|
|
836
|
-
response: { mode: 'sync' },
|
|
837
|
-
connection: 'cycle-count-recon-adhoc',
|
|
838
|
-
}).then(
|
|
839
|
-
http('run-reconciliation-adhoc', { connection: 'fluent_commerce' }, async ctx => {
|
|
840
|
-
const { data } = ctx;
|
|
841
|
-
return await processReconciliation(ctx, {
|
|
842
|
-
triggeredBy: 'manual',
|
|
843
|
-
forceReprocess: data?.forceReprocess === true,
|
|
844
|
-
});
|
|
845
|
-
})
|
|
846
|
-
);
|
|
847
|
-
|
|
848
|
-
/**
|
|
849
|
-
* Job status query endpoint
|
|
850
|
-
*/
|
|
851
|
-
export const reconciliationJobStatus = webhook('cycle-count-recon-job-status', {
|
|
852
|
-
response: { mode: 'sync' },
|
|
853
|
-
connection: 'cycle-count-recon-job-status',
|
|
854
|
-
}).then(
|
|
855
|
-
fn('status', async ctx => {
|
|
856
|
-
const { data } = ctx;
|
|
857
|
-
return await getReconciliationStatus(ctx, data?.jobId);
|
|
858
|
-
})
|
|
859
|
-
);
|
|
860
|
-
```
|
|
861
|
-
|
|
862
|
-
---
|
|
863
|
-
|
|
864
|
-
### File: `src/types/cycle-count-types.ts`
|
|
865
|
-
|
|
866
|
-
```typescript
|
|
867
|
-
/**
|
|
868
|
-
* Type definitions for cycle count reconciliation workflow
|
|
869
|
-
*/
|
|
870
|
-
|
|
871
|
-
export interface CycleCountRecord {
|
|
872
|
-
locationRef: string;
|
|
873
|
-
skuRef: string;
|
|
874
|
-
countedQty: number;
|
|
875
|
-
countDate?: string;
|
|
876
|
-
counterName?: string;
|
|
877
|
-
binLocation?: string;
|
|
878
|
-
lotNumber?: string;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
export interface FluentInventory {
|
|
882
|
-
id: string;
|
|
883
|
-
locationRef: string;
|
|
884
|
-
skuRef: string;
|
|
885
|
-
qty: number;
|
|
886
|
-
type: string;
|
|
887
|
-
status: string;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
export interface VarianceItem extends CycleCountRecord {
|
|
891
|
-
fluentQty: number;
|
|
892
|
-
varianceQty: number;
|
|
893
|
-
variancePercent: number;
|
|
894
|
-
action: 'AUTO_ADJUST' | 'MANUAL_REVIEW';
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
export interface ReconciliationReport {
|
|
898
|
-
fileName: string;
|
|
899
|
-
processedAt: string;
|
|
900
|
-
summary: {
|
|
901
|
-
totalItemsCounted: number;
|
|
902
|
-
mappingErrors: number;
|
|
903
|
-
itemsWithNoVariance: number;
|
|
904
|
-
itemsWithVariance: number;
|
|
905
|
-
autoAdjustmentsCreated: number;
|
|
906
|
-
autoAdjustmentsFailed: number;
|
|
907
|
-
manualReviewRequired: number;
|
|
908
|
-
};
|
|
909
|
-
thresholds: {
|
|
910
|
-
percentThreshold: number;
|
|
911
|
-
absoluteThreshold: number;
|
|
912
|
-
};
|
|
913
|
-
variances: {
|
|
914
|
-
autoAdjusted: VarianceItem[];
|
|
915
|
-
manualReview: VarianceItem[];
|
|
916
|
-
};
|
|
917
|
-
errors?: any[];
|
|
918
|
-
}
|
|
919
|
-
```
|
|
920
|
-
|
|
921
|
-
---
|
|
922
|
-
|
|
923
|
-
### File: `src/utils/retry.ts`
|
|
924
|
-
|
|
925
|
-
```typescript
|
|
926
|
-
/**
|
|
927
|
-
* Retry utility with exponential backoff
|
|
928
|
-
*/
|
|
929
|
-
export async function retryWithBackoff<T>(
|
|
930
|
-
operation: () => Promise<T>,
|
|
931
|
-
maxRetries = 3,
|
|
932
|
-
baseDelayMs = 1000
|
|
933
|
-
): Promise<T> {
|
|
934
|
-
let lastError: any;
|
|
935
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
936
|
-
try {
|
|
937
|
-
return await operation();
|
|
938
|
-
} catch (error) {
|
|
939
|
-
lastError = error;
|
|
940
|
-
if (attempt < maxRetries - 1) {
|
|
941
|
-
const delay = baseDelayMs * Math.pow(2, attempt) + Math.floor(Math.random() * 200);
|
|
942
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
throw lastError;
|
|
947
|
-
}
|
|
948
|
-
```
|
|
949
|
-
|
|
950
|
-
---
|
|
951
|
-
|
|
952
|
-
### File: `src/utils/data-source-factory.ts`
|
|
953
|
-
|
|
954
|
-
```typescript
|
|
955
|
-
import { S3DataSource, SftpDataSource } from '@fluentcommerce/fc-connect-sdk';
|
|
956
|
-
|
|
957
|
-
/**
|
|
958
|
-
* Initialize S3 data source from activation variables
|
|
959
|
-
*/
|
|
960
|
-
export function initializeS3DataSource(activation: any, log): S3DataSource { // ✅ Versori native log - TypeScript infers type
|
|
961
|
-
return new S3DataSource(
|
|
962
|
-
{
|
|
963
|
-
type: 'S3_CSV',
|
|
964
|
-
connectionId: 's3-cycle-counts',
|
|
965
|
-
name: 'Cycle Count S3',
|
|
966
|
-
s3Config: {
|
|
967
|
-
bucket: activation.getVariable('s3BucketName'),
|
|
968
|
-
region: activation.getVariable('awsRegion') || 'us-east-1',
|
|
969
|
-
accessKeyId: activation.getVariable('awsAccessKeyId'),
|
|
970
|
-
secretAccessKey: activation.getVariable('awsSecretAccessKey'),
|
|
971
|
-
},
|
|
972
|
-
},
|
|
973
|
-
log
|
|
974
|
-
);
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
/**
|
|
978
|
-
* Initialize SFTP data source from activation variables
|
|
979
|
-
*/
|
|
980
|
-
export function initializeSftpDataSource(activation: any, log): SftpDataSource { // ✅ Versori native log - TypeScript infers type
|
|
981
|
-
return new SftpDataSource(
|
|
982
|
-
{
|
|
983
|
-
type: 'SFTP_CSV',
|
|
984
|
-
connectionId: 'sftp-cycle-counts',
|
|
985
|
-
name: 'Cycle Count SFTP',
|
|
986
|
-
sftpConfig: {
|
|
987
|
-
host: activation.getVariable('sftpHost'),
|
|
988
|
-
port: parseInt(activation.getVariable('sftpPort') || '22', 10),
|
|
989
|
-
username: activation.getVariable('sftpUsername'),
|
|
990
|
-
password: activation.getVariable('sftpPassword'),
|
|
991
|
-
privateKey: activation.getVariable('sftpPrivateKey'),
|
|
992
|
-
},
|
|
993
|
-
},
|
|
994
|
-
log
|
|
995
|
-
);
|
|
996
|
-
}
|
|
997
|
-
```
|
|
998
|
-
|
|
999
|
-
---
|
|
1000
|
-
|
|
1001
|
-
### File: `src/services/inventory-query.service.ts`
|
|
1002
|
-
|
|
1003
|
-
```typescript
|
|
1004
|
-
import type { FluentClient } from '@fluentcommerce/fc-connect-sdk';
|
|
1005
|
-
import type { CycleCountRecord, FluentInventory } from '../types/cycle-count-types';
|
|
1006
|
-
|
|
1007
|
-
/**
|
|
1008
|
-
* Service for querying current Fluent inventory in bulk
|
|
1009
|
-
*/
|
|
1010
|
-
export class InventoryQueryService {
|
|
1011
|
-
constructor(
|
|
1012
|
-
private client: FluentClient
|
|
1013
|
-
// ✅ No logger - workflow handles logging with Versori native log
|
|
1014
|
-
) {}
|
|
1015
|
-
|
|
1016
|
-
/**
|
|
1017
|
-
* Query current inventory for all cycle count items
|
|
1018
|
-
* Uses single GraphQL query with auto-pagination for O(1) lookup
|
|
1019
|
-
*/
|
|
1020
|
-
async queryCurrentInventory(
|
|
1021
|
-
cycleCounts: CycleCountRecord[]
|
|
1022
|
-
): Promise<Map<string, FluentInventory>> {
|
|
1023
|
-
// Extract unique location + SKU combinations
|
|
1024
|
-
const uniqueLocations = [...new Set(cycleCounts.map(cc => cc.locationRef))];
|
|
1025
|
-
const uniqueSkus = [...new Set(cycleCounts.map(cc => cc.skuRef))];
|
|
1026
|
-
|
|
1027
|
-
locations: uniqueLocations.length,
|
|
1028
|
-
skus: uniqueSkus.length,
|
|
1029
|
-
});
|
|
1030
|
-
|
|
1031
|
-
// Build GraphQL query
|
|
1032
|
-
const query = `
|
|
1033
|
-
query GetInventoryForReconciliation($first: Int, $after: String) {
|
|
1034
|
-
inventoryQuantities(first: $first, after: $after) {
|
|
1035
|
-
edges {
|
|
1036
|
-
node {
|
|
1037
|
-
id
|
|
1038
|
-
locationRef
|
|
1039
|
-
skuRef
|
|
1040
|
-
qty
|
|
1041
|
-
type
|
|
1042
|
-
status
|
|
1043
|
-
}
|
|
1044
|
-
cursor
|
|
1045
|
-
}
|
|
1046
|
-
pageInfo {
|
|
1047
|
-
hasNextPage
|
|
1048
|
-
endCursor
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
`;
|
|
1053
|
-
|
|
1054
|
-
// Execute query with auto-pagination
|
|
1055
|
-
const result = await this.client.graphql({
|
|
1056
|
-
query,
|
|
1057
|
-
variables: { first: 100 },
|
|
1058
|
-
pagination: { maxPages: 100 },
|
|
1059
|
-
});
|
|
1060
|
-
|
|
1061
|
-
// Build lookup map (O(1) access)
|
|
1062
|
-
const inventoryMap = new Map<string, FluentInventory>();
|
|
1063
|
-
for (const edge of result.data?.inventoryQuantities?.edges || []) {
|
|
1064
|
-
const node = edge.node;
|
|
1065
|
-
const key = `${node.locationRef}:${node.skuRef}`;
|
|
1066
|
-
inventoryMap.set(key, node);
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
return inventoryMap;
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
```
|
|
1073
|
-
|
|
1074
|
-
---
|
|
1075
|
-
|
|
1076
|
-
### File: `src/services/variance-calculator.service.ts`
|
|
1077
|
-
|
|
1078
|
-
```typescript
|
|
1079
|
-
import type { CycleCountRecord, FluentInventory, VarianceItem } from '../types/cycle-count-types';
|
|
1080
|
-
|
|
1081
|
-
/**
|
|
1082
|
-
* Service for calculating inventory variances with dual threshold logic
|
|
1083
|
-
*/
|
|
1084
|
-
export class VarianceCalculatorService {
|
|
1085
|
-
constructor(
|
|
1086
|
-
private percentThreshold: number = 5,
|
|
1087
|
-
private absoluteThreshold: number = 10
|
|
1088
|
-
// ✅ No logger - workflow handles logging with Versori native log
|
|
1089
|
-
) {}
|
|
1090
|
-
|
|
1091
|
-
/**
|
|
1092
|
-
* Calculate variances between counted and system inventory
|
|
1093
|
-
* Uses dual threshold logic (both must be met for auto-adjustment)
|
|
1094
|
-
*/
|
|
1095
|
-
calculateVariances(
|
|
1096
|
-
cycleCounts: CycleCountRecord[],
|
|
1097
|
-
inventoryMap: Map<string, FluentInventory>
|
|
1098
|
-
): VarianceItem[] {
|
|
1099
|
-
const variances: VarianceItem[] = [];
|
|
1100
|
-
|
|
1101
|
-
for (const cycleCount of cycleCounts) {
|
|
1102
|
-
const key = `${cycleCount.locationRef}:${cycleCount.skuRef}`;
|
|
1103
|
-
const fluentInventory = inventoryMap.get(key);
|
|
1104
|
-
const fluentQty = fluentInventory?.qty || 0;
|
|
1105
|
-
const countedQty = cycleCount.countedQty;
|
|
1106
|
-
|
|
1107
|
-
// Calculate variance
|
|
1108
|
-
const varianceQty = countedQty - fluentQty;
|
|
1109
|
-
|
|
1110
|
-
// Calculate percentage variance (handle divide-by-zero)
|
|
1111
|
-
const variancePercent =
|
|
1112
|
-
fluentQty === 0
|
|
1113
|
-
? countedQty === 0
|
|
1114
|
-
? 0 // 0 → 0 = no variance
|
|
1115
|
-
: 100 // 0 → N = 100% variance
|
|
1116
|
-
: Math.abs((varianceQty / fluentQty) * 100);
|
|
1117
|
-
|
|
1118
|
-
// Skip if no variance
|
|
1119
|
-
if (varianceQty === 0) {
|
|
1120
|
-
continue;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
// Determine action based on dual threshold
|
|
1124
|
-
const isWithinPercentThreshold = variancePercent < this.percentThreshold;
|
|
1125
|
-
const isWithinAbsoluteThreshold = Math.abs(varianceQty) < this.absoluteThreshold;
|
|
1126
|
-
const isWithinThreshold = isWithinPercentThreshold && isWithinAbsoluteThreshold;
|
|
1127
|
-
|
|
1128
|
-
const action = isWithinThreshold ? 'AUTO_ADJUST' : 'MANUAL_REVIEW';
|
|
1129
|
-
|
|
1130
|
-
variances.push({
|
|
1131
|
-
...cycleCount,
|
|
1132
|
-
fluentQty,
|
|
1133
|
-
varianceQty,
|
|
1134
|
-
variancePercent: parseFloat(variancePercent.toFixed(2)),
|
|
1135
|
-
action,
|
|
1136
|
-
});
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
totalVariances: variances.length,
|
|
1140
|
-
autoAdjust: variances.filter(v => v.action === 'AUTO_ADJUST').length,
|
|
1141
|
-
manualReview: variances.filter(v => v.action === 'MANUAL_REVIEW').length,
|
|
1142
|
-
});
|
|
1143
|
-
|
|
1144
|
-
return variances;
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
```
|
|
1148
|
-
|
|
1149
|
-
---
|
|
1150
|
-
|
|
1151
|
-
### File: `src/services/batch-processor.service.ts`
|
|
1152
|
-
|
|
1153
|
-
```typescript
|
|
1154
|
-
import type { FluentClient } from '@fluentcommerce/fc-connect-sdk';
|
|
1155
|
-
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
1156
|
-
import type { VarianceItem } from '../types/cycle-count-types';
|
|
1157
|
-
|
|
1158
|
-
/**
|
|
1159
|
-
* Service for sending inventory adjustments via Batch API
|
|
1160
|
-
*
|
|
1161
|
-
* ✅ PRODUCTION ENHANCEMENT: Optional logger for detailed progress tracking
|
|
1162
|
-
*/
|
|
1163
|
-
export class BatchProcessorService {
|
|
1164
|
-
constructor(
|
|
1165
|
-
private client: FluentClient,
|
|
1166
|
-
private jobTracker: JobTracker,
|
|
1167
|
-
private log?: any // ✅ Optional logger for progress tracking
|
|
1168
|
-
) {}
|
|
1169
|
-
|
|
1170
|
-
/**
|
|
1171
|
-
* Send inventory adjustment batches to Fluent Batch API
|
|
1172
|
-
*/
|
|
1173
|
-
async sendAdjustmentBatches(
|
|
1174
|
-
adjustments: VarianceItem[],
|
|
1175
|
-
fileName: string,
|
|
1176
|
-
retailerId: string
|
|
1177
|
-
): Promise<any> {
|
|
1178
|
-
|
|
1179
|
-
// ✅ PRODUCTION ENHANCEMENT: Log batch sending start
|
|
1180
|
-
if (this.log) {
|
|
1181
|
-
this.log.info('📤 Starting adjustment batch sending', {
|
|
1182
|
-
fileName,
|
|
1183
|
-
totalAdjustments: adjustments.length,
|
|
1184
|
-
retailerId,
|
|
1185
|
-
});
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
try {
|
|
1189
|
-
// Create batch job (BPP enabled by default)
|
|
1190
|
-
const job = await this.client.createJob({
|
|
1191
|
-
name: `Cycle Count Reconciliation - ${fileName}`,
|
|
1192
|
-
retailerId,
|
|
1193
|
-
});
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
// Build inventory adjustment entities
|
|
1197
|
-
const entities = adjustments.map(item => ({
|
|
1198
|
-
ref: `${item.locationRef}:${item.skuRef}`, // Composite ref
|
|
1199
|
-
locationRef: item.locationRef,
|
|
1200
|
-
skuRef: item.skuRef,
|
|
1201
|
-
qty: item.countedQty, // NEW absolute quantity (not delta)
|
|
1202
|
-
type: 'CORRECTION',
|
|
1203
|
-
status: 'AVAILABLE',
|
|
1204
|
-
attributes: [
|
|
1205
|
-
{
|
|
1206
|
-
name: 'cycle_count_adjustment',
|
|
1207
|
-
type: 'STRING',
|
|
1208
|
-
value: 'true',
|
|
1209
|
-
},
|
|
1210
|
-
{
|
|
1211
|
-
name: 'original_qty',
|
|
1212
|
-
type: 'INTEGER',
|
|
1213
|
-
value: String(item.fluentQty),
|
|
1214
|
-
},
|
|
1215
|
-
{
|
|
1216
|
-
name: 'variance_qty',
|
|
1217
|
-
type: 'INTEGER',
|
|
1218
|
-
value: String(item.varianceQty),
|
|
1219
|
-
},
|
|
1220
|
-
{
|
|
1221
|
-
name: 'count_date',
|
|
1222
|
-
type: 'STRING',
|
|
1223
|
-
value: item.countDate || new Date().toISOString(),
|
|
1224
|
-
},
|
|
1225
|
-
{
|
|
1226
|
-
name: 'counter_name',
|
|
1227
|
-
type: 'STRING',
|
|
1228
|
-
value: item.counterName || 'WMS System',
|
|
1229
|
-
},
|
|
1230
|
-
{
|
|
1231
|
-
name: 'source_file',
|
|
1232
|
-
type: 'STRING',
|
|
1233
|
-
value: fileName,
|
|
1234
|
-
},
|
|
1235
|
-
],
|
|
1236
|
-
}));
|
|
1237
|
-
|
|
1238
|
-
// Send batch (fire-and-forget - returns void, not an object)
|
|
1239
|
-
await this.client.sendBatch(job.id, {
|
|
1240
|
-
action: 'UPSERT',
|
|
1241
|
-
entityType: 'INVENTORY',
|
|
1242
|
-
entities,
|
|
1243
|
-
});
|
|
1244
|
-
|
|
1245
|
-
// ✅ PRODUCTION ENHANCEMENT: Log completion
|
|
1246
|
-
if (this.log) {
|
|
1247
|
-
this.log.info('✅ Adjustment batch sending completed', {
|
|
1248
|
-
fileName,
|
|
1249
|
-
jobId: job.id,
|
|
1250
|
-
totalAdjustments: entities.length,
|
|
1251
|
-
retailerId,
|
|
1252
|
-
});
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
// Batch submission successful
|
|
1256
|
-
return {
|
|
1257
|
-
jobId: job.id,
|
|
1258
|
-
successCount: entities.length,
|
|
1259
|
-
failureCount: 0,
|
|
1260
|
-
};
|
|
1261
|
-
} catch (error: any) {
|
|
1262
|
-
throw error;
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
```
|
|
1267
|
-
|
|
1268
|
-
---
|
|
1269
|
-
|
|
1270
|
-
### File: `src/services/report-generator.service.ts`
|
|
1271
|
-
|
|
1272
|
-
```typescript
|
|
1273
|
-
import type {
|
|
1274
|
-
ReconciliationReport,
|
|
1275
|
-
VarianceItem,
|
|
1276
|
-
CycleCountRecord,
|
|
1277
|
-
} from '../types/cycle-count-types';
|
|
1278
|
-
|
|
1279
|
-
/**
|
|
1280
|
-
* Service for generating reconciliation reports
|
|
1281
|
-
*/
|
|
1282
|
-
export class ReportGeneratorService {
|
|
1283
|
-
constructor() {
|
|
1284
|
-
// ✅ No logger - workflow handles logging with Versori native log
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
/**
|
|
1288
|
-
* Generate reconciliation report with summary and variance details
|
|
1289
|
-
*/
|
|
1290
|
-
generateReconciliationReport(
|
|
1291
|
-
fileName: string,
|
|
1292
|
-
cycleCounts: CycleCountRecord[],
|
|
1293
|
-
mappingErrors: any[],
|
|
1294
|
-
variances: VarianceItem[],
|
|
1295
|
-
batchResult: any,
|
|
1296
|
-
percentThreshold: number,
|
|
1297
|
-
absoluteThreshold: number
|
|
1298
|
-
): ReconciliationReport {
|
|
1299
|
-
const autoAdjusted = variances.filter(v => v.action === 'AUTO_ADJUST');
|
|
1300
|
-
const manualReview = variances.filter(v => v.action === 'MANUAL_REVIEW');
|
|
1301
|
-
|
|
1302
|
-
return {
|
|
1303
|
-
fileName,
|
|
1304
|
-
processedAt: new Date().toISOString(),
|
|
1305
|
-
summary: {
|
|
1306
|
-
totalItemsCounted: cycleCounts.length,
|
|
1307
|
-
mappingErrors: mappingErrors.length,
|
|
1308
|
-
itemsWithNoVariance: cycleCounts.length - variances.length,
|
|
1309
|
-
itemsWithVariance: variances.length,
|
|
1310
|
-
autoAdjustmentsCreated: batchResult?.successCount || 0,
|
|
1311
|
-
autoAdjustmentsFailed: batchResult?.failureCount || 0,
|
|
1312
|
-
manualReviewRequired: manualReview.length,
|
|
1313
|
-
},
|
|
1314
|
-
thresholds: {
|
|
1315
|
-
percentThreshold,
|
|
1316
|
-
absoluteThreshold,
|
|
1317
|
-
},
|
|
1318
|
-
variances: {
|
|
1319
|
-
autoAdjusted: autoAdjusted.map(item => ({
|
|
1320
|
-
...item,
|
|
1321
|
-
adjustmentStatus: batchResult ? 'SUCCESS' : 'NOT_SENT',
|
|
1322
|
-
})),
|
|
1323
|
-
manualReview: manualReview.map(item => ({
|
|
1324
|
-
...item,
|
|
1325
|
-
reason:
|
|
1326
|
-
item.variancePercent >= percentThreshold
|
|
1327
|
-
? `Exceeds ${percentThreshold}% threshold`
|
|
1328
|
-
: `Exceeds ${absoluteThreshold} unit threshold`,
|
|
1329
|
-
})),
|
|
1330
|
-
},
|
|
1331
|
-
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
1332
|
-
};
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
```
|
|
1336
|
-
|
|
1337
|
-
---
|
|
1338
|
-
|
|
1339
|
-
### File: `src/workflows/cycle-count-reconciliation.workflow.ts`
|
|
1340
|
-
|
|
1341
|
-
```typescript
|
|
1342
|
-
import { Buffer } from 'node:buffer';
|
|
1343
|
-
import {
|
|
1344
|
-
createClient,
|
|
1345
|
-
CSVParserService,
|
|
1346
|
-
UniversalMapper,
|
|
1347
|
-
VersoriFileTracker,
|
|
1348
|
-
JobTracker,
|
|
1349
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
1350
|
-
import type { FileMetadata } from '@fluentcommerce/fc-connect-sdk';
|
|
1351
|
-
import cycleCountMapping from '../config/cycle-count.mapping.json' with { type: 'json' };
|
|
1352
|
-
import { InventoryQueryService } from '../services/inventory-query.service';
|
|
1353
|
-
import { VarianceCalculatorService } from '../services/variance-calculator.service';
|
|
1354
|
-
import { BatchProcessorService } from '../services/batch-processor.service';
|
|
1355
|
-
import { ReportGeneratorService } from '../services/report-generator.service';
|
|
1356
|
-
import { retryWithBackoff } from '../utils/retry';
|
|
1357
|
-
import { initializeS3DataSource, initializeSftpDataSource } from '../utils/data-source-factory';
|
|
1358
|
-
import type { CycleCountRecord } from '../types/cycle-count-types';
|
|
1359
|
-
|
|
1360
|
-
/**
|
|
1361
|
-
* Main cycle count reconciliation workflow orchestrator
|
|
1362
|
-
*/
|
|
1363
|
-
export async function processReconciliation(
|
|
1364
|
-
ctx: any,
|
|
1365
|
-
options: { triggeredBy: string; forceReprocess?: boolean }
|
|
1366
|
-
) {
|
|
1367
|
-
const { log, openKv, activation } = ctx;
|
|
1368
|
-
const kv = openKv(':project:');
|
|
1369
|
-
const jobId = `cycle-count-recon-${options.triggeredBy}-${Date.now()}`;
|
|
1370
|
-
|
|
1371
|
-
// Initialize SDK clients
|
|
1372
|
-
const fluentClient = await createClient(ctx);
|
|
1373
|
-
const jobTracker = new JobTracker(kv, log);
|
|
1374
|
-
|
|
1375
|
-
await jobTracker.createJob(jobId, {
|
|
1376
|
-
triggeredBy: options.triggeredBy,
|
|
1377
|
-
stage: 'initialization',
|
|
1378
|
-
});
|
|
1379
|
-
await jobTracker.updateJob(jobId, { status: 'processing' });
|
|
1380
|
-
|
|
1381
|
-
// Initialize data source based on configuration
|
|
1382
|
-
const dataSourceType = activation.getVariable('dataSource');
|
|
1383
|
-
const dataSource =
|
|
1384
|
-
dataSourceType === 'sftp'
|
|
1385
|
-
? initializeSftpDataSource(activation, log)
|
|
1386
|
-
: initializeS3DataSource(activation, log);
|
|
1387
|
-
|
|
1388
|
-
const csvParser = new CSVParserService();
|
|
1389
|
-
const mapper = new UniversalMapper(cycleCountMapping);
|
|
1390
|
-
const fileTracker = new VersoriFileTracker(kv, 'cycle-count-recon');
|
|
1391
|
-
|
|
1392
|
-
// Initialize services
|
|
1393
|
-
const inventoryQuery = new InventoryQueryService(fluentClient);
|
|
1394
|
-
const percentThreshold = parseFloat(activation.getVariable('variancePercentThreshold') || '5');
|
|
1395
|
-
const absoluteThreshold = parseFloat(activation.getVariable('varianceAbsoluteThreshold') || '10');
|
|
1396
|
-
const varianceCalculator = new VarianceCalculatorService(percentThreshold, absoluteThreshold);
|
|
1397
|
-
// ✅ PRODUCTION ENHANCEMENT: Pass log to BatchProcessorService for detailed progress tracking
|
|
1398
|
-
const batchProcessor = new BatchProcessorService(fluentClient, jobTracker, log);
|
|
1399
|
-
const reportGenerator = new ReportGeneratorService();
|
|
1400
|
-
|
|
1401
|
-
const results = {
|
|
1402
|
-
filesProcessed: 0,
|
|
1403
|
-
filesSkipped: 0,
|
|
1404
|
-
totalItemsCounted: 0,
|
|
1405
|
-
totalVariances: 0,
|
|
1406
|
-
autoAdjusted: 0,
|
|
1407
|
-
manualReview: 0,
|
|
1408
|
-
errors: [] as any[],
|
|
1409
|
-
reports: [] as any[],
|
|
1410
|
-
};
|
|
1411
|
-
|
|
1412
|
-
try {
|
|
1413
|
-
// Discover files
|
|
1414
|
-
await jobTracker.updateJob(jobId, { stage: 'discovering-files' });
|
|
1415
|
-
log.info('Discovering cycle count files');
|
|
1416
|
-
|
|
1417
|
-
const files =
|
|
1418
|
-
dataSourceType === 'sftp'
|
|
1419
|
-
? await dataSource.listFiles({
|
|
1420
|
-
remotePath: activation.getVariable('sftpIncomingPath'),
|
|
1421
|
-
filePattern: activation.getVariable('filePattern'),
|
|
1422
|
-
})
|
|
1423
|
-
: await dataSource.listFiles({
|
|
1424
|
-
prefix: activation.getVariable('s3Prefix'),
|
|
1425
|
-
pattern: activation.getVariable('filePattern'),
|
|
1426
|
-
});
|
|
1427
|
-
|
|
1428
|
-
log.info('Found files', { count: files.length });
|
|
1429
|
-
|
|
1430
|
-
// Filter processed files (unless force reprocess)
|
|
1431
|
-
let filesToProcess = files;
|
|
1432
|
-
if (!options.forceReprocess) {
|
|
1433
|
-
const processedChecks = await Promise.all(
|
|
1434
|
-
files.map(async (file: FileMetadata) => ({
|
|
1435
|
-
file,
|
|
1436
|
-
processed: await fileTracker.wasFileProcessed(file.name),
|
|
1437
|
-
}))
|
|
1438
|
-
);
|
|
1439
|
-
|
|
1440
|
-
filesToProcess = processedChecks
|
|
1441
|
-
.filter(check => !check.processed)
|
|
1442
|
-
.map(check => check.file);
|
|
1443
|
-
|
|
1444
|
-
results.filesSkipped = files.length - filesToProcess.length;
|
|
1445
|
-
log.info('Filtered processed files', {
|
|
1446
|
-
total: files.length,
|
|
1447
|
-
toProcess: filesToProcess.length,
|
|
1448
|
-
skipped: results.filesSkipped,
|
|
1449
|
-
});
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
// Limit files per run
|
|
1453
|
-
const maxFiles = parseInt(activation.getVariable('maxFilesPerRun') || '5', 10);
|
|
1454
|
-
filesToProcess = filesToProcess.slice(0, maxFiles);
|
|
1455
|
-
|
|
1456
|
-
// Process each file
|
|
1457
|
-
for (const file of filesToProcess) {
|
|
1458
|
-
await jobTracker.updateJob(jobId, { stage: 'processing-file', details: { fileName: file.name } });
|
|
1459
|
-
|
|
1460
|
-
try {
|
|
1461
|
-
log.info('Processing file', { fileName: file.name });
|
|
1462
|
-
|
|
1463
|
-
// Download file
|
|
1464
|
-
await jobTracker.updateJob(jobId, { stage: 'downloading-file', details: { fileName: file.name } });
|
|
1465
|
-
const fileContent =
|
|
1466
|
-
dataSourceType === 'sftp'
|
|
1467
|
-
? await dataSource.downloadFile(file.path)
|
|
1468
|
-
: await dataSource.readFile(file.key);
|
|
1469
|
-
|
|
1470
|
-
// Parse CSV
|
|
1471
|
-
await jobTracker.updateJob(jobId, { stage: 'parsing-csv', details: { fileName: file.name } });
|
|
1472
|
-
const records = await csvParser.parse(fileContent);
|
|
1473
|
-
log.info('Parsed CSV', {
|
|
1474
|
-
fileName: file.name,
|
|
1475
|
-
recordCount: records.length,
|
|
1476
|
-
});
|
|
1477
|
-
|
|
1478
|
-
// Transform records
|
|
1479
|
-
await jobTracker.updateJob(jobId, { stage: 'transforming-records', details: { fileName: file.name } });
|
|
1480
|
-
const mapped = await mapper.mapBatch(records);
|
|
1481
|
-
const cycleCounts = mapped.data as CycleCountRecord[];
|
|
1482
|
-
const mappingErrors = mapped.errors;
|
|
1483
|
-
|
|
1484
|
-
log.info('Transformed records', {
|
|
1485
|
-
fileName: file.name,
|
|
1486
|
-
successCount: cycleCounts.length,
|
|
1487
|
-
errorCount: mappingErrors.length,
|
|
1488
|
-
});
|
|
1489
|
-
|
|
1490
|
-
// Query current Fluent inventory (bulk query)
|
|
1491
|
-
await jobTracker.updateJob(jobId, { stage: 'querying-inventory', details: { fileName: file.name } });
|
|
1492
|
-
const inventoryMap = await inventoryQuery.queryCurrentInventory(cycleCounts);
|
|
1493
|
-
|
|
1494
|
-
// Calculate variances
|
|
1495
|
-
await jobTracker.updateJob(jobId, { stage: 'calculating-variances', details: { fileName: file.name } });
|
|
1496
|
-
const variances = varianceCalculator.calculateVariances(cycleCounts, inventoryMap);
|
|
1497
|
-
|
|
1498
|
-
log.info('Calculated variances', {
|
|
1499
|
-
fileName: file.name,
|
|
1500
|
-
totalVariances: variances.length,
|
|
1501
|
-
autoAdjust: variances.filter(v => v.action === 'AUTO_ADJUST').length,
|
|
1502
|
-
manualReview: variances.filter(v => v.action === 'MANUAL_REVIEW').length,
|
|
1503
|
-
});
|
|
1504
|
-
|
|
1505
|
-
// Send adjustments via Batch API
|
|
1506
|
-
let batchResult;
|
|
1507
|
-
if (
|
|
1508
|
-
activation.getVariable('enableAutoAdjustment') === 'true' &&
|
|
1509
|
-
variances.some(v => v.action === 'AUTO_ADJUST')
|
|
1510
|
-
) {
|
|
1511
|
-
await jobTracker.updateJob(jobId, { stage: 'sending-batch', details: { fileName: file.name } });
|
|
1512
|
-
const retailerId = activation.getVariable('retailerId');
|
|
1513
|
-
const autoAdjustments = variances.filter(v => v.action === 'AUTO_ADJUST');
|
|
1514
|
-
|
|
1515
|
-
// ? Enhanced: Extract context for progress logging
|
|
1516
|
-
const uniqueLocations = [...new Set(autoAdjustments.map((v: any) => v.locationRef))];
|
|
1517
|
-
const sampleSKUs = autoAdjustments.slice(0, 5).map((v: any) => v.skuRef);
|
|
1518
|
-
|
|
1519
|
-
// ? Enhanced: Start logging with context
|
|
1520
|
-
log.info(`[BatchProcessor] Sending cycle count adjustments for file "${file.name}"`, {
|
|
1521
|
-
totalAdjustments: autoAdjustments.length,
|
|
1522
|
-
locations: uniqueLocations.join(', '),
|
|
1523
|
-
sampleSKUs: sampleSKUs.join(', '),
|
|
1524
|
-
fileName: file.name
|
|
1525
|
-
});
|
|
1526
|
-
|
|
1527
|
-
batchResult = await batchProcessor.sendAdjustmentBatches(
|
|
1528
|
-
autoAdjustments,
|
|
1529
|
-
file.name,
|
|
1530
|
-
retailerId
|
|
1531
|
-
);
|
|
1532
|
-
|
|
1533
|
-
// ✅ Logging handled in workflow with Versori native log
|
|
1534
|
-
log.info('[BatchProcessor] Sent adjustment batches', {
|
|
1535
|
-
file: file.name,
|
|
1536
|
-
jobId: batchResult.jobId,
|
|
1537
|
-
successCount: batchResult.successCount,
|
|
1538
|
-
failureCount: batchResult.failureCount,
|
|
1539
|
-
});
|
|
1540
|
-
|
|
1541
|
-
// ? Enhanced: Completion logging
|
|
1542
|
-
log.info(`[BatchProcessor] Cycle count adjustments sent successfully for file "${file.name}"`, {
|
|
1543
|
-
totalAdjustments: autoAdjustments.length,
|
|
1544
|
-
jobId: batchResult.jobId,
|
|
1545
|
-
successCount: batchResult.successCount
|
|
1546
|
-
});
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
// Generate reconciliation report
|
|
1550
|
-
await jobTracker.updateJob(jobId, { stage: 'generating-report', details: { fileName: file.name } });
|
|
1551
|
-
const report = reportGenerator.generateReconciliationReport(
|
|
1552
|
-
file.name,
|
|
1553
|
-
cycleCounts,
|
|
1554
|
-
mappingErrors,
|
|
1555
|
-
variances,
|
|
1556
|
-
batchResult,
|
|
1557
|
-
percentThreshold,
|
|
1558
|
-
absoluteThreshold
|
|
1559
|
-
);
|
|
1560
|
-
|
|
1561
|
-
results.reports.push(report);
|
|
1562
|
-
results.filesProcessed++;
|
|
1563
|
-
results.totalItemsCounted += cycleCounts.length;
|
|
1564
|
-
results.totalVariances += variances.length;
|
|
1565
|
-
results.autoAdjusted += report.summary.autoAdjustmentsCreated;
|
|
1566
|
-
results.manualReview += report.summary.manualReviewRequired;
|
|
1567
|
-
|
|
1568
|
-
// Mark file as processed
|
|
1569
|
-
await fileTracker.markFileProcessed(file.name, {
|
|
1570
|
-
recordCount: records.length,
|
|
1571
|
-
varianceCount: variances.length,
|
|
1572
|
-
adjustmentCount: report.summary.autoAdjustmentsCreated,
|
|
1573
|
-
manualReviewCount: report.summary.manualReviewRequired,
|
|
1574
|
-
});
|
|
1575
|
-
|
|
1576
|
-
// Archive file
|
|
1577
|
-
if (activation.getVariable('enableArchival') === 'true') {
|
|
1578
|
-
await jobTracker.updateJob(jobId, { stage: 'archiving-file', details: { fileName: file.name } });
|
|
1579
|
-
await archiveFile(dataSource, file, dataSourceType, activation, log);
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
log.info('File processing completed', {
|
|
1583
|
-
fileName: file.name,
|
|
1584
|
-
report: report.summary,
|
|
1585
|
-
});
|
|
1586
|
-
} catch (error: any) {
|
|
1587
|
-
// ✅ Enhanced error logging: Extract all error details for visibility
|
|
1588
|
-
const errorDetails = {
|
|
1589
|
-
message: error?.message || 'Unknown error',
|
|
1590
|
-
stack: error?.stack,
|
|
1591
|
-
fileName: error?.fileName,
|
|
1592
|
-
lineNumber: error?.lineNumber,
|
|
1593
|
-
originalError: error?.context?.originalError?.message,
|
|
1594
|
-
errorType: error?.name || 'Error',
|
|
1595
|
-
};
|
|
1596
|
-
log.error('File processing failed', errorDetails, { fileName: file.name });
|
|
1597
|
-
results.errors.push({
|
|
1598
|
-
fileName: file.name,
|
|
1599
|
-
error: error.message,
|
|
1600
|
-
});
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
await jobTracker.markCompleted(jobId, results);
|
|
1605
|
-
return { success: true, jobId, ...results };
|
|
1606
|
-
} catch (error: any) {
|
|
1607
|
-
// ✅ Enhanced error logging: Extract all error details for visibility
|
|
1608
|
-
const errorDetails = {
|
|
1609
|
-
message: error?.message || 'Unknown error',
|
|
1610
|
-
stack: error?.stack,
|
|
1611
|
-
fileName: error?.fileName,
|
|
1612
|
-
lineNumber: error?.lineNumber,
|
|
1613
|
-
originalError: error?.context?.originalError?.message,
|
|
1614
|
-
errorType: error?.name || 'Error',
|
|
1615
|
-
};
|
|
1616
|
-
log.error('[CycleCountRecon] Fatal error:', errorDetails);
|
|
1617
|
-
await jobTracker.markFailed(jobId, error);
|
|
1618
|
-
return { success: false, jobId, error: error.message };
|
|
1619
|
-
} finally {
|
|
1620
|
-
// Cleanup data source
|
|
1621
|
-
if (typeof dataSource.dispose === 'function') {
|
|
1622
|
-
await dataSource.dispose();
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
/**
|
|
1628
|
-
* Query job status
|
|
1629
|
-
*/
|
|
1630
|
-
export async function getReconciliationStatus(ctx: any, jobId?: string) {
|
|
1631
|
-
const { log, openKv } = ctx;
|
|
1632
|
-
if (!jobId) {
|
|
1633
|
-
return { success: false, error: 'jobId required' };
|
|
1634
|
-
}
|
|
1635
|
-
const tracker = new JobTracker(openKv(':project:'), log);
|
|
1636
|
-
const job = await tracker.getJob(jobId);
|
|
1637
|
-
return job ? { success: true, jobId, ...job } : { success: false, jobId, error: 'Job not found' };
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
/**
|
|
1641
|
-
* Archive file helper
|
|
1642
|
-
*/
|
|
1643
|
-
async function archiveFile(dataSource: any, file: any, dataSourceType: string, activation: any, log: any): Promise<void> {
|
|
1644
|
-
try {
|
|
1645
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1646
|
-
const archiveName = `${file.name.replace(/\.[^/.]+$/, '')}-${timestamp}${file.name.match(/\.[^/.]+$/)?.[0] || ''}`;
|
|
1647
|
-
|
|
1648
|
-
if (dataSourceType === 'sftp') {
|
|
1649
|
-
// SFTP: Move file
|
|
1650
|
-
const archivePath = `${activation.getVariable('sftpProcessedPath')}/${archiveName}`;
|
|
1651
|
-
await dataSource.moveFile(file.path, archivePath);
|
|
1652
|
-
log.info('Moved file to processed folder', {
|
|
1653
|
-
from: file.path,
|
|
1654
|
-
to: archivePath,
|
|
1655
|
-
});
|
|
1656
|
-
} else {
|
|
1657
|
-
// S3: Move file
|
|
1658
|
-
const archiveKey = `${activation.getVariable('archivePrefix')}${archiveName}`;
|
|
1659
|
-
await dataSource.moveFile(file.key, archiveKey);
|
|
1660
|
-
log.info('Archived file to S3', {
|
|
1661
|
-
from: file.key,
|
|
1662
|
-
to: archiveKey,
|
|
1663
|
-
});
|
|
1664
|
-
}
|
|
1665
|
-
} catch (error: any) {
|
|
1666
|
-
// ✅ Enhanced error logging: Extract all error details for visibility
|
|
1667
|
-
const errorDetails = {
|
|
1668
|
-
message: error?.message || 'Unknown error',
|
|
1669
|
-
stack: error?.stack,
|
|
1670
|
-
fileName: file.name,
|
|
1671
|
-
errorType: error?.name || 'Error',
|
|
1672
|
-
};
|
|
1673
|
-
log.error('Failed to archive file', errorDetails);
|
|
1674
|
-
// Don't throw - archival is optional
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
```
|
|
1678
|
-
|
|
1679
|
-
---
|
|
1680
|
-
|
|
1681
|
-
### File: `src/config/cycle-count.mapping.json`
|
|
1682
|
-
|
|
1683
|
-
```json
|
|
1684
|
-
{
|
|
1685
|
-
"name": "cycle-count.mapping",
|
|
1686
|
-
"version": "1.0.0",
|
|
1687
|
-
"description": "CSV cycle count to reconciliation record mapping",
|
|
1688
|
-
"fields": {
|
|
1689
|
-
"locationRef": { "source": "location_code", "required": true, "resolver": "sdk.trim" },
|
|
1690
|
-
"skuRef": { "source": "sku", "required": true, "resolver": "sdk.trim" },
|
|
1691
|
-
"countedQty": { "source": "counted_quantity", "required": true, "resolver": "sdk.parseInt" },
|
|
1692
|
-
"countDate": { "source": "count_date", "required": false },
|
|
1693
|
-
"counterName": { "source": "counter_username", "required": false },
|
|
1694
|
-
"binLocation": { "source": "bin_location", "required": false },
|
|
1695
|
-
"lotNumber": { "source": "lot_number", "required": false }
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
```
|
|
1699
|
-
|
|
1700
|
-
---
|
|
1701
|
-
|
|
1702
|
-
### Benefits of This Modular Approach
|
|
1703
|
-
|
|
1704
|
-
1. **Separation of Concerns**: Each service has one clear responsibility
|
|
1705
|
-
2. **Reusability**: Services can be imported and used in other workflows
|
|
1706
|
-
3. **Testability**: Easy to write unit tests for individual services
|
|
1707
|
-
4. **Maintainability**: Changes isolated to specific service files
|
|
1708
|
-
5. **Type Safety**: Centralized type definitions prevent duplication
|
|
1709
|
-
6. **Scalability**: Easy to add new services without modifying existing code
|
|
1710
|
-
7. **Follows Gold Standard**: Matches pattern from template-ingestion-sftp-xml-inventory-batch.md
|
|
1711
|
-
|
|
1712
|
-
---
|
|
1713
|
-
|
|
1714
|
-
## Testing
|
|
1715
|
-
|
|
1716
|
-
- Upload a small CSV to S3/SFTP (2-3 cycle count rows) with known variances
|
|
1717
|
-
- Trigger `cycle-count-recon-adhoc` webhook with valid API key
|
|
1718
|
-
- Verify variances calculated correctly (auto-adjust vs manual review)
|
|
1719
|
-
- Confirm Batch job created and adjustments sent for auto-adjust items
|
|
1720
|
-
- Check file moved from `incoming/` to `processed/` folder
|
|
1721
|
-
- Verify reconciliation report generated with correct summary
|
|
1722
|
-
- Query job status via `cycle-count-recon-job-status` webhook
|
|
1723
|
-
- Check KV state: file tracking and job metadata
|
|
1724
|
-
|
|
1725
|
-
---
|
|
1726
|
-
|
|
1727
|
-
## Monitoring
|
|
1728
|
-
|
|
1729
|
-
### Success Response
|
|
1730
|
-
|
|
1731
|
-
```json
|
|
1732
|
-
{
|
|
1733
|
-
"success": true,
|
|
1734
|
-
"filesProcessed": 1,
|
|
1735
|
-
"filesSkipped": 0,
|
|
1736
|
-
"filesFailed": 0,
|
|
1737
|
-
"results": [
|
|
1738
|
-
{
|
|
1739
|
-
"file": "cycle-counts_2025-01-22.csv",
|
|
1740
|
-
"success": true,
|
|
1741
|
-
"recordCount": 50,
|
|
1742
|
-
"batchCount": 2,
|
|
1743
|
-
"jobId": "job-123456",
|
|
1744
|
-
"adjustmentsCreated": 25,
|
|
1745
|
-
"adjustmentsRequiringReview": 5,
|
|
1746
|
-
"duration": 12345
|
|
1747
|
-
}
|
|
1748
|
-
],
|
|
1749
|
-
"duration": 13456
|
|
1750
|
-
}
|
|
1751
|
-
```
|
|
1752
|
-
|
|
1753
|
-
### Partial Success Response
|
|
1754
|
-
|
|
1755
|
-
```json
|
|
1756
|
-
{
|
|
1757
|
-
"success": true,
|
|
1758
|
-
"filesProcessed": 1,
|
|
1759
|
-
"filesSkipped": 0,
|
|
1760
|
-
"filesFailed": 0,
|
|
1761
|
-
"results": [
|
|
1762
|
-
{
|
|
1763
|
-
"file": "cycle-counts_2025-01-22.csv",
|
|
1764
|
-
"success": true,
|
|
1765
|
-
"recordCount": 50,
|
|
1766
|
-
"batchCount": 2,
|
|
1767
|
-
"jobId": "job-123456",
|
|
1768
|
-
"adjustmentsCreated": 45,
|
|
1769
|
-
"adjustmentsRequiringReview": 5,
|
|
1770
|
-
"errors": ["SKU-001: Inventory not found", "SKU-002: Invalid variance calculation"]
|
|
1771
|
-
}
|
|
1772
|
-
],
|
|
1773
|
-
"duration": 13456
|
|
1774
|
-
}
|
|
1775
|
-
```
|
|
1776
|
-
|
|
1777
|
-
### Error Response
|
|
1778
|
-
|
|
1779
|
-
```json
|
|
1780
|
-
{
|
|
1781
|
-
"success": false,
|
|
1782
|
-
"filesProcessed": 0,
|
|
1783
|
-
"filesFailed": 1,
|
|
1784
|
-
"results": [
|
|
1785
|
-
{
|
|
1786
|
-
"file": "cycle-counts_2025-01-22.csv",
|
|
1787
|
-
"success": false,
|
|
1788
|
-
"error": "CSV parse error: Invalid structure"
|
|
1789
|
-
}
|
|
1790
|
-
],
|
|
1791
|
-
"duration": 876
|
|
1792
|
-
}
|
|
1793
|
-
```
|
|
1794
|
-
|
|
1795
|
-
### Monitoring Metrics
|
|
1796
|
-
|
|
1797
|
-
Monitor these metrics via Versori logs filtered by `jobId` or `stage`:
|
|
1798
|
-
|
|
1799
|
-
- **Files Processed** - Total files successfully processed
|
|
1800
|
-
- **Batch Jobs Created** - Total Batch jobs created in Fluent Commerce
|
|
1801
|
-
- **Adjustments Created** - Total inventory adjustments sent
|
|
1802
|
-
- **Adjustments Requiring Review** - Adjustments flagged for manual review
|
|
1803
|
-
- **Processing Duration** - Time taken for complete workflow
|
|
1804
|
-
- **Variance Thresholds** - Monitor variance calculation accuracy
|
|
1805
|
-
|
|
1806
|
-
Use the status webhook for dashboards and automated monitoring.
|
|
1807
|
-
|
|
1808
|
-
---
|
|
1809
|
-
|
|
1810
|
-
- **All variances flagged for manual review** - Thresholds too strict; increase `variancePercentThreshold` and `varianceAbsoluteThreshold`.
|
|
1811
|
-
- **Percentage variance always 100%** - Fluent inventory not found (divide by zero); verify inventory exists before cycle count.
|
|
1812
|
-
- **Batch API timeout** - Too many adjustments in single batch; chunk adjustments into smaller batches of 500.
|
|
1813
|
-
- **Duplicate adjustments** - File tracking not working; verify VersoriFileTracker uses correct KV scope (`:project:`).
|
|
1814
|
-
- **S3/SFTP access denied** - Validate IAM permissions or SFTP credentials; ensure bucket/host/paths are correct.
|
|
1815
|
-
- **CSV header mismatch** - Trim headers and verify mapping sources match exact column names.
|
|
1816
|
-
|
|
1817
|
-
### Required IAM Permissions (S3)
|
|
1818
|
-
|
|
1819
|
-
```json
|
|
1820
|
-
{
|
|
1821
|
-
"Version": "2012-10-17",
|
|
1822
|
-
"Statement": [
|
|
1823
|
-
{
|
|
1824
|
-
"Effect": "Allow",
|
|
1825
|
-
"Action": ["s3:ListBucket", "s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
|
|
1826
|
-
"Resource": [
|
|
1827
|
-
"arn:aws:s3:::wms-cycle-counts",
|
|
1828
|
-
"arn:aws:s3:::wms-cycle-counts/*"
|
|
1829
|
-
]
|
|
1830
|
-
}
|
|
1831
|
-
]
|
|
1832
|
-
}
|
|
1833
|
-
```
|
|
1834
|
-
|
|
1835
|
-
## Production Checklist
|
|
1836
|
-
|
|
1837
|
-
- Activation secrets stored securely; no secrets in code
|
|
1838
|
-
- AWS credentials validated (S3) or SFTP credentials tested
|
|
1839
|
-
- Archive/error paths writable and monitored
|
|
1840
|
-
- Variance thresholds tuned for your business requirements
|
|
1841
|
-
- Logging/metrics and alerting configured for failed reconciliations
|
|
1842
|
-
- Manual review notification system configured
|
|
1843
|
-
- Webhook API key security verified
|
|
1844
|
-
- Clear runbook for failures and retries
|
|
1845
|
-
- Reconciliation report storage/retention policy defined
|
|
1846
|
-
- File naming/ordering strategy documented
|
|
1847
|
-
- BPP (Batch Pre-Processing) behavior tested
|
|
1848
|
-
|
|
1849
|
-
## Related Guides
|
|
1850
|
-
|
|
1851
|
-
- State & KV patterns: `docs/use-cases/versori/03-kv-state-management.md`
|
|
1852
|
-
- Error handling & retry: `docs/use-cases/patterns/error-handling-retry.md`
|
|
1853
|
-
- Universal Mapping: `fc-connect-sdk/docs/guides/mapping/readme.md`
|
|
1854
|
-
- Batch ingestion: `fc-connect-sdk/docs/guides/ingestion.md`
|
|
1855
|
-
- JobTracker: `fc-connect-sdk/docs/guides/job-tracker.md`
|
|
1856
|
-
|
|
1857
|
-
## Advanced Patterns
|
|
1858
|
-
|
|
1859
|
-
### Pattern 1: Dual Threshold Logic (Percentage AND Absolute)
|
|
1860
|
-
|
|
1861
|
-
**Both thresholds must be met for auto-adjustment:**
|
|
1862
|
-
|
|
1863
|
-
```typescript
|
|
1864
|
-
const percentThreshold = 5; // 5%
|
|
1865
|
-
const absoluteThreshold = 10; // 10 units
|
|
1866
|
-
|
|
1867
|
-
const isWithinPercentThreshold = variancePercent < percentThreshold;
|
|
1868
|
-
const isWithinAbsoluteThreshold = Math.abs(varianceQty) < absoluteThreshold;
|
|
1869
|
-
const isWithinThreshold = isWithinPercentThreshold && isWithinAbsoluteThreshold;
|
|
1870
|
-
|
|
1871
|
-
const action = isWithinThreshold ? 'AUTO_ADJUST' : 'MANUAL_REVIEW';
|
|
1872
|
-
```
|
|
1873
|
-
|
|
1874
|
-
**Why dual thresholds?** Prevents incorrect auto-adjustments:
|
|
1875
|
-
|
|
1876
|
-
| Fluent Qty | Counted | Variance Qty | Variance % | Auto-Adjust? | Reason |
|
|
1877
|
-
| ---------- | ------- | ------------ | ---------- | ------------ | ------------------------- |
|
|
1878
|
-
| 100 | 103 | +3 | 3% | Yes | Both thresholds met |
|
|
1879
|
-
| 100 | 115 | +15 | 15% | No | Exceeds 10 unit threshold |
|
|
1880
|
-
| 10 | 11 | +1 | 10% | No | Exceeds 5% threshold |
|
|
1881
|
-
| 1000 | 1008 | +8 | 0.8% | Yes | Both thresholds met |
|
|
1882
|
-
| 5 | 0 | -5 | 100% | No | Exceeds both thresholds |
|
|
1883
|
-
|
|
1884
|
-
### Pattern 2: Variance Calculation with Edge Cases
|
|
1885
|
-
|
|
1886
|
-
**Handle divide-by-zero and edge cases:**
|
|
1887
|
-
|
|
1888
|
-
```typescript
|
|
1889
|
-
const fluentQty = fluentInventory?.qty || 0;
|
|
1890
|
-
const countedQty = cycleCount.countedQty;
|
|
1891
|
-
const varianceQty = countedQty - fluentQty;
|
|
1892
|
-
|
|
1893
|
-
// Calculate percentage variance (handle divide-by-zero)
|
|
1894
|
-
const variancePercent =
|
|
1895
|
-
fluentQty === 0
|
|
1896
|
-
? countedQty === 0
|
|
1897
|
-
? 0 // 0 → 0 = no variance
|
|
1898
|
-
: 100 // 0 → N = 100% variance
|
|
1899
|
-
: Math.abs((varianceQty / fluentQty) * 100);
|
|
1900
|
-
```
|
|
1901
|
-
|
|
1902
|
-
**Edge cases handled:**
|
|
1903
|
-
|
|
1904
|
-
- **Zero to zero**: No variance (0%)
|
|
1905
|
-
- **Zero to positive**: 100% variance (requires review)
|
|
1906
|
-
- **Negative variance**: Counted less than system (shrinkage)
|
|
1907
|
-
- **Positive variance**: Counted more than system (found inventory)
|
|
1908
|
-
|
|
1909
|
-
### Pattern 3: Bulk Inventory Query (Single Query, O(1) Lookup)
|
|
1910
|
-
|
|
1911
|
-
**Single GraphQL query for all items:**
|
|
1912
|
-
|
|
1913
|
-
```typescript
|
|
1914
|
-
// Extract unique combinations
|
|
1915
|
-
const uniqueLocations = [...new Set(cycleCounts.map(cc => cc.locationRef))];
|
|
1916
|
-
const uniqueSkus = [...new Set(cycleCounts.map(cc => cc.skuRef))];
|
|
1917
|
-
|
|
1918
|
-
// Query with auto-pagination
|
|
1919
|
-
const result = await client.graphql({
|
|
1920
|
-
query: inventoryQuery,
|
|
1921
|
-
variables: { first: 100 },
|
|
1922
|
-
pagination: { maxPages: 100 },
|
|
1923
|
-
});
|
|
1924
|
-
|
|
1925
|
-
// Build lookup map (O(1) access)
|
|
1926
|
-
const inventoryMap = new Map<string, FluentInventory>();
|
|
1927
|
-
for (const edge of result.data?.inventoryQuantities?.edges || []) {
|
|
1928
|
-
const node = edge.node;
|
|
1929
|
-
const key = `${node.locationRef}:${node.skuRef}`;
|
|
1930
|
-
inventoryMap.set(key, node);
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
// Fast lookup during reconciliation
|
|
1934
|
-
for (const cycleCount of cycleCounts) {
|
|
1935
|
-
const key = `${cycleCount.locationRef}:${cycleCount.skuRef}`;
|
|
1936
|
-
const fluentInventory = inventoryMap.get(key); // O(1)
|
|
1937
|
-
}
|
|
1938
|
-
```
|
|
1939
|
-
|
|
1940
|
-
**Performance comparison:**
|
|
1941
|
-
|
|
1942
|
-
- Bad (100 items): 100 GraphQL queries = ~10-20 seconds
|
|
1943
|
-
- Good (100 items): 1 GraphQL query + Map lookup = ~1-2 seconds
|
|
1944
|
-
|
|
1945
|
-
### Pattern 4: Batch API with Full Audit Trail
|
|
1946
|
-
|
|
1947
|
-
**Include complete audit trail in attributes:**
|
|
1948
|
-
|
|
1949
|
-
```typescript
|
|
1950
|
-
const entities = adjustments.map(item => ({
|
|
1951
|
-
ref: `${item.locationRef}:${item.skuRef}`,
|
|
1952
|
-
locationRef: item.locationRef,
|
|
1953
|
-
skuRef: item.skuRef,
|
|
1954
|
-
qty: item.countedQty, // NEW absolute quantity (not delta)
|
|
1955
|
-
type: 'CORRECTION',
|
|
1956
|
-
status: 'AVAILABLE',
|
|
1957
|
-
attributes: [
|
|
1958
|
-
{ name: 'cycle_count_adjustment', type: 'STRING', value: 'true' },
|
|
1959
|
-
{ name: 'original_qty', type: 'INTEGER', value: String(item.fluentQty) },
|
|
1960
|
-
{ name: 'variance_qty', type: 'INTEGER', value: String(item.varianceQty) },
|
|
1961
|
-
{ name: 'count_date', type: 'STRING', value: item.countDate || new Date().toISOString() },
|
|
1962
|
-
{ name: 'counter_name', type: 'STRING', value: item.counterName || 'WMS System' },
|
|
1963
|
-
{ name: 'source_file', type: 'STRING', value: fileName },
|
|
1964
|
-
],
|
|
1965
|
-
}));
|
|
1966
|
-
```
|
|
1967
|
-
|
|
1968
|
-
**CRITICAL:** `qty` is the **new absolute quantity**, NOT the delta!
|
|
1969
|
-
|
|
1970
|
-
```typescript
|
|
1971
|
-
// WRONG - This would set qty to variance
|
|
1972
|
-
qty: item.varianceQty;
|
|
1973
|
-
|
|
1974
|
-
// CORRECT - This sets qty to actual counted quantity
|
|
1975
|
-
qty: item.countedQty;
|
|
1976
|
-
```
|
|
1977
|
-
|
|
1978
|
-
### Pattern 5: Reconciliation Report Structure
|
|
1979
|
-
|
|
1980
|
-
**Structured report for variance analysis:**
|
|
1981
|
-
|
|
1982
|
-
```typescript
|
|
1983
|
-
return {
|
|
1984
|
-
fileName,
|
|
1985
|
-
processedAt: new Date().toISOString(),
|
|
1986
|
-
summary: {
|
|
1987
|
-
totalItemsCounted: cycleCounts.length,
|
|
1988
|
-
mappingErrors: mappingErrors.length,
|
|
1989
|
-
itemsWithNoVariance: cycleCounts.length - variances.length,
|
|
1990
|
-
itemsWithVariance: variances.length,
|
|
1991
|
-
autoAdjustmentsCreated: batchResult?.successCount || 0,
|
|
1992
|
-
autoAdjustmentsFailed: batchResult?.failureCount || 0,
|
|
1993
|
-
manualReviewRequired: manualReview.length,
|
|
1994
|
-
},
|
|
1995
|
-
thresholds: {
|
|
1996
|
-
percentThreshold: parseFloat(activation.getVariable('variancePercentThreshold')),
|
|
1997
|
-
absoluteThreshold: parseFloat(activation.getVariable('varianceAbsoluteThreshold')),
|
|
1998
|
-
},
|
|
1999
|
-
variances: {
|
|
2000
|
-
autoAdjusted: [...],
|
|
2001
|
-
manualReview: [...],
|
|
2002
|
-
},
|
|
2003
|
-
};
|
|
2004
|
-
```
|
|
2005
|
-
|
|
2006
|
-
**Report usage:**
|
|
2007
|
-
|
|
2008
|
-
1. Operations dashboard - Display summary metrics
|
|
2009
|
-
2. Email notifications - Alert on large variances
|
|
2010
|
-
3. Audit trail - Store in S3/KV for compliance
|
|
2011
|
-
4. Manual review queue - Feed to approval UI
|
|
2012
|
-
|
|
2013
|
-
---
|
|
2014
|
-
|
|
2015
|
-
**End of Template**
|
|
2016
|
-
|
|
2017
|
-
## Related Templates
|
|
2018
|
-
|
|
2019
|
-
**For standard inventory batch ingestion** (without reconciliation logic), see:
|
|
2020
|
-
- [SFTP XML Inventory Batch Template](./template-ingestion-sftp-xml-inventory-batch.md) - Simpler workflow without variance calculation
|
|
2021
|
-
|
|
2022
|
-
**Key Differences:**
|
|
2023
|
-
- This template adds cycle count reconciliation with dual threshold variance detection
|
|
2024
|
-
- Standard template is for direct inventory ingestion without comparison logic
|
|
2025
|
-
- Use this template when comparing physical counts against system inventory
|
|
2026
|
-
- Use standard template for regular inventory file uploads
|
|
2027
|
-
|
|
2028
|
-
---
|
|
2029
|
-
|
|
2030
|
-
[← Back to Batch API Templates](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) | [Ingestion Templates Index →](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)
|
|
1
|
+
---
|
|
2
|
+
template_id: tpl-ingest-cycle-count-reconciliation
|
|
3
|
+
canonical_filename: template-ingestion-cycle-count-reconciliation.md
|
|
4
|
+
sdk_version: ^0.1.39
|
|
5
|
+
runtime: versori
|
|
6
|
+
direction: ingestion
|
|
7
|
+
source: s3-csv|sftp-csv
|
|
8
|
+
destination: fluent-batch-api
|
|
9
|
+
entity: inventory
|
|
10
|
+
format: csv
|
|
11
|
+
logging: versori
|
|
12
|
+
status: stable
|
|
13
|
+
complexity: medium-high
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Template: Ingestion - Cycle Count Reconciliation with Batch API
|
|
17
|
+
|
|
18
|
+
## STEP 1: Read Template Header
|
|
19
|
+
|
|
20
|
+
This template is now loaded into your context. Review the metadata above:
|
|
21
|
+
|
|
22
|
+
- **Template ID**: `tpl-ingest-cycle-count-reconciliation`
|
|
23
|
+
- **SDK Version**: `^0.1.30`
|
|
24
|
+
- **Runtime**: Versori Platform
|
|
25
|
+
- **Direction**: Ingestion (data → Fluent Commerce)
|
|
26
|
+
- **Source**: S3 or SFTP CSV files
|
|
27
|
+
- **Destination**: Fluent Batch API
|
|
28
|
+
- **Entity**: InventoryQuantity (inventory adjustments)
|
|
29
|
+
- **Complexity**: Medium-High
|
|
30
|
+
- **Status**: Stable (validated and production-ready)
|
|
31
|
+
|
|
32
|
+
**Before implementing:**
|
|
33
|
+
1. Verify SDK version compatibility: `npm list @fluentcommerce/fc-connect-sdk`
|
|
34
|
+
2. Ensure you have Fluent API credentials (OAuth2: clientId, clientSecret, username, password)
|
|
35
|
+
3. Have S3 or SFTP access credentials ready
|
|
36
|
+
4. Review the workflow overview and entity fields below
|
|
37
|
+
|
|
38
|
+
## STEP 2: Workflow Overview
|
|
39
|
+
|
|
40
|
+
### What This Template Does
|
|
41
|
+
|
|
42
|
+
This template implements a **specialized cycle count reconciliation workflow** for Versori Platform that:
|
|
43
|
+
|
|
44
|
+
1. **Reads physical cycle count files** from S3 or SFTP (CSV format with count data)
|
|
45
|
+
2. **Compares against current Fluent inventory** using bulk GraphQL query for performance
|
|
46
|
+
3. **Calculates variance** with dual threshold logic (percentage AND absolute variance)
|
|
47
|
+
4. **Sends inventory adjustments** via Batch API for items requiring correction
|
|
48
|
+
5. **Generates reconciliation reports** with auto-adjusted vs manual review items
|
|
49
|
+
6. **Tracks file processing** to prevent duplicate reconciliations
|
|
50
|
+
7. **Maintains audit trail** with full variance tracking and metadata
|
|
51
|
+
|
|
52
|
+
### Key SDK Methods Used
|
|
53
|
+
|
|
54
|
+
- **createClient()** - Auto-detect Versori context and create FluentClient
|
|
55
|
+
- **CSVParserService** - Parse cycle count CSV files
|
|
56
|
+
- **UniversalMapper** - Transform CSV records to reconciliation format
|
|
57
|
+
- **S3DataSource / SftpDataSource** - Read files with retry logic and archival
|
|
58
|
+
- **VersoriFileTracker** - Prevent duplicate file processing
|
|
59
|
+
- **JobTracker** - Track multi-step workflow lifecycle
|
|
60
|
+
- **client.graphql()** - Bulk inventory query with auto-pagination
|
|
61
|
+
- **client.createJob()** - Create Batch API job with BPP enabled
|
|
62
|
+
- **client.sendBatch()** - Send inventory adjustments (entityType: 'INVENTORY') - fire-and-forget
|
|
63
|
+
|
|
64
|
+
### Critical Entity Details
|
|
65
|
+
|
|
66
|
+
**Entity**: `InventoryQuantity` (NOT Product)
|
|
67
|
+
|
|
68
|
+
**Batch API EntityType**: `'INVENTORY'`
|
|
69
|
+
|
|
70
|
+
**Required Fields**:
|
|
71
|
+
- `ref` - Composite reference: `${locationRef}:${skuRef}`
|
|
72
|
+
- `locationRef` - Warehouse location identifier
|
|
73
|
+
- `skuRef` - SKU/article reference (NOTE: Field is `skuRef`, not `articleRef` in this context)
|
|
74
|
+
- `qty` - **NEW absolute quantity** (not delta/variance) - CRITICAL!
|
|
75
|
+
- `type` - Inventory type: `'CORRECTION'` for cycle count adjustments
|
|
76
|
+
- `status` - Inventory status: `'AVAILABLE'`
|
|
77
|
+
|
|
78
|
+
**Audit Trail Attributes**:
|
|
79
|
+
- `cycle_count_adjustment` - Flag indicating cycle count origin
|
|
80
|
+
- `original_qty` - System quantity before adjustment
|
|
81
|
+
- `variance_qty` - Calculated variance (counted - system)
|
|
82
|
+
- `count_date` - When physical count was performed
|
|
83
|
+
- `counter_name` - Person who performed count
|
|
84
|
+
- `source_file` - CSV filename for traceability
|
|
85
|
+
|
|
86
|
+
### Variance Calculation Logic
|
|
87
|
+
|
|
88
|
+
**Dual Threshold Approach** (both must be met for auto-adjustment):
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
const percentThreshold = 5; // 5% variance threshold
|
|
92
|
+
const absoluteThreshold = 10; // 10 units variance threshold
|
|
93
|
+
|
|
94
|
+
const varianceQty = countedQty - fluentQty;
|
|
95
|
+
const variancePercent = Math.abs((varianceQty / fluentQty) * 100);
|
|
96
|
+
|
|
97
|
+
// Auto-adjust only if BOTH thresholds are met
|
|
98
|
+
const isWithinThreshold =
|
|
99
|
+
(variancePercent < percentThreshold) &&
|
|
100
|
+
(Math.abs(varianceQty) < absoluteThreshold);
|
|
101
|
+
|
|
102
|
+
const action = isWithinThreshold ? 'AUTO_ADJUST' : 'MANUAL_REVIEW';
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Edge Cases Handled**:
|
|
106
|
+
- Zero to zero (0 → 0): No variance
|
|
107
|
+
- Zero to positive (0 → N): 100% variance → Manual review
|
|
108
|
+
- Divide by zero: Safe percentage calculation
|
|
109
|
+
|
|
110
|
+
### Workflow Steps
|
|
111
|
+
|
|
112
|
+
1. **File Discovery** - List CSV files from S3/SFTP with pattern matching
|
|
113
|
+
2. **Duplicate Prevention** - Check VersoriFileTracker for already-processed files
|
|
114
|
+
3. **CSV Parsing** - Parse cycle count data with validation
|
|
115
|
+
4. **Field Mapping** - Transform CSV to reconciliation records via UniversalMapper
|
|
116
|
+
5. **Bulk Inventory Query** - Single GraphQL query for all items (O(1) lookup)
|
|
117
|
+
6. **Variance Calculation** - Compare counted vs system quantities with dual thresholds
|
|
118
|
+
7. **Batch API Send** - Send auto-adjustments via createJob + sendBatch (BPP enabled)
|
|
119
|
+
8. **Status Polling** - Poll batch completion with timeout
|
|
120
|
+
9. **Report Generation** - Create reconciliation report with summary and details
|
|
121
|
+
10. **File Archival** - Move processed file to archive folder
|
|
122
|
+
11. **State Tracking** - Mark file as processed in KV store
|
|
123
|
+
|
|
124
|
+
### GraphQL Query (Bulk Inventory)
|
|
125
|
+
|
|
126
|
+
```graphql
|
|
127
|
+
query GetInventoryForReconciliation($first: Int, $after: String) {
|
|
128
|
+
inventoryQuantities(first: $first, after: $after) {
|
|
129
|
+
edges {
|
|
130
|
+
node {
|
|
131
|
+
id
|
|
132
|
+
locationRef
|
|
133
|
+
skuRef
|
|
134
|
+
qty
|
|
135
|
+
type
|
|
136
|
+
status
|
|
137
|
+
}
|
|
138
|
+
cursor
|
|
139
|
+
}
|
|
140
|
+
pageInfo {
|
|
141
|
+
hasNextPage
|
|
142
|
+
endCursor
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Performance**: Single query + Map lookup = O(1) per item (vs O(N) individual queries)
|
|
149
|
+
|
|
150
|
+
### Batch API Integration
|
|
151
|
+
|
|
152
|
+
**BPP (Batch Pre-Processing)**: **ENABLED** (default)
|
|
153
|
+
- Fluent's change detection filters unchanged inventory records
|
|
154
|
+
- Only actual changes trigger workflows
|
|
155
|
+
- Reduces downstream processing load
|
|
156
|
+
|
|
157
|
+
**When to Skip BPP**: Never for cycle count reconciliation (full accuracy required)
|
|
158
|
+
|
|
159
|
+
### No Hallucinated Methods
|
|
160
|
+
|
|
161
|
+
All methods used in this template are verified SDK methods:
|
|
162
|
+
- ✅ `createClient()` - Client factory
|
|
163
|
+
- ✅ `CSVParserService.parse()` - CSV parsing
|
|
164
|
+
- ✅ `UniversalMapper.mapBatch()` - Batch transformation
|
|
165
|
+
- ✅ `S3DataSource.listFiles()` / `readFile()` / `moveFile()` - S3 operations
|
|
166
|
+
- ✅ `SftpDataSource.listFiles()` / `downloadFile()` / `moveFile()` - SFTP operations
|
|
167
|
+
- ✅ `VersoriFileTracker.wasFileProcessed()` / `markFileProcessed()` - File tracking
|
|
168
|
+
- ✅ `JobTracker.createJob()` / `updateJob()` / `markCompleted()` - Job tracking
|
|
169
|
+
- ✅ `client.graphql()` - GraphQL query with auto-pagination
|
|
170
|
+
- ✅ `client.createJob()` - Batch job creation
|
|
171
|
+
- ✅ `client.sendBatch()` - Batch send (fire-and-forget)
|
|
172
|
+
|
|
173
|
+
### Workflows Provided
|
|
174
|
+
|
|
175
|
+
1. **Scheduled Reconciliation** (`scheduledReconciliation`)
|
|
176
|
+
- Cron: Daily at 2 AM UTC
|
|
177
|
+
- Auto-triggered with JobTracker integration
|
|
178
|
+
|
|
179
|
+
2. **Ad Hoc Reconciliation** (`adhocReconciliation`)
|
|
180
|
+
- Manual webhook trigger with API key authentication
|
|
181
|
+
- Supports `forceReprocess` flag to override file tracking
|
|
182
|
+
|
|
183
|
+
3. **Job Status Query** (`reconciliationJobStatus`)
|
|
184
|
+
- Webhook endpoint to query job status by `jobId`
|
|
185
|
+
- Returns job lifecycle state and results
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
**FC Connect SDK Use Case Guide**
|
|
190
|
+
|
|
191
|
+
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
192
|
+
> **Version**: `^0.1.30`
|
|
193
|
+
|
|
194
|
+
**Context**: Scheduled Versori workflow that reads physical cycle count CSV files from S3/SFTP, compares against current Fluent inventory, calculates variances with dual threshold logic, and sends automatic adjustments via Batch API with full audit trail.
|
|
195
|
+
|
|
196
|
+
**Complexity**: Medium-High
|
|
197
|
+
|
|
198
|
+
**Runtime**: Versori Platform (Scheduled + Manual Trigger + Status Query)
|
|
199
|
+
|
|
200
|
+
**Estimated Lines**: ~950
|
|
201
|
+
|
|
202
|
+
## Versori Workflows Structure
|
|
203
|
+
|
|
204
|
+
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
205
|
+
|
|
206
|
+
**Trigger Types:**
|
|
207
|
+
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
208
|
+
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
209
|
+
- **`workflow()`** → Durable workflows (advanced, rarely used)
|
|
210
|
+
|
|
211
|
+
**Execution Steps (chained to triggers):**
|
|
212
|
+
- **`http()`** → External API calls (chained from schedule/webhook)
|
|
213
|
+
- **`fn()`** → Internal processing (chained from schedule/webhook)
|
|
214
|
+
|
|
215
|
+
### Recommended Project Structure
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
data-batch-sync/
|
|
219
|
+
├── index.ts # Entry point - exports all workflows
|
|
220
|
+
└── src/
|
|
221
|
+
├── workflows/
|
|
222
|
+
│ ├── scheduled/
|
|
223
|
+
│ │ └── daily-data-sync.ts # Scheduled: Daily data sync
|
|
224
|
+
│ │
|
|
225
|
+
│ └── webhook/
|
|
226
|
+
│ ├── adhoc-data-sync.ts # Webhook: Manual trigger
|
|
227
|
+
│ └── job-status-check.ts # Webhook: Status query
|
|
228
|
+
│
|
|
229
|
+
├── services/
|
|
230
|
+
│ └── data-sync.service.ts # Shared orchestration logic (reusable)
|
|
231
|
+
│
|
|
232
|
+
└── types/
|
|
233
|
+
└── data.types.ts # Shared type definitions
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Benefits:**
|
|
237
|
+
- ✅ Clear trigger separation (`scheduled/` vs `webhook/`)
|
|
238
|
+
- ✅ Descriptive file names (easy to browse and understand)
|
|
239
|
+
- ✅ Scalable (add new workflows without cluttering)
|
|
240
|
+
- ✅ Reusable code in `services/` (DRY principle)
|
|
241
|
+
- ✅ Easy to modify individual workflows without affecting others
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Workflow Files
|
|
246
|
+
|
|
247
|
+
### 1. Scheduled Workflows (`src/workflows/scheduled/`)
|
|
248
|
+
|
|
249
|
+
All time-based triggers that run automatically on cron schedules.
|
|
250
|
+
|
|
251
|
+
#### `src/workflows/scheduled/daily-data-sync.ts`
|
|
252
|
+
|
|
253
|
+
**Purpose**: Automatic Daily data sync
|
|
254
|
+
**Trigger**: Cron schedule (`0 2 * * *`)
|
|
255
|
+
**Exposed as Endpoint**: ❌ NO - Runs automatically
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
import { schedule, http } from '@versori/run';
|
|
259
|
+
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
260
|
+
import { runIngestion } from '../../services/data-sync.service.ts';
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Scheduled Workflow: Daily Data Sync
|
|
264
|
+
*
|
|
265
|
+
* Runs automatically daily at 2 AM UTC
|
|
266
|
+
* NOT exposed as HTTP endpoint - Versori executes on schedule
|
|
267
|
+
*
|
|
268
|
+
* Uses shared service: data-sync.service.ts
|
|
269
|
+
*/
|
|
270
|
+
export const daily_data_sync = schedule(
|
|
271
|
+
'data-batch-scheduled',
|
|
272
|
+
'0 2 * * *' // Daily at 2 AM UTC
|
|
273
|
+
).then(
|
|
274
|
+
http('run-data-batch', { connection: 'fluent_commerce' }, async ctx => {
|
|
275
|
+
const { log, openKv } = ctx;
|
|
276
|
+
const jobId = `data-batch-${Date.now()}`;
|
|
277
|
+
const tracker = new JobTracker(openKv(':project:'), log);
|
|
278
|
+
|
|
279
|
+
await tracker.createJob(jobId, { triggeredBy: 'schedule', stage: 'initialization' });
|
|
280
|
+
await tracker.updateJob(jobId, { status: 'processing' });
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
// Reuse shared orchestration logic
|
|
284
|
+
const result = await runIngestion(ctx, jobId, tracker);
|
|
285
|
+
await tracker.markCompleted(jobId, result);
|
|
286
|
+
return { success: true, jobId, ...result };
|
|
287
|
+
} catch (e: any) {
|
|
288
|
+
await tracker.markFailed(jobId, e);
|
|
289
|
+
return { success: false, jobId, error: e?.message };
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
);
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
### 2. Webhook Workflows (`src/workflows/webhook/`)
|
|
298
|
+
|
|
299
|
+
All HTTP-based triggers that create webhook endpoints.
|
|
300
|
+
|
|
301
|
+
#### `src/workflows/webhook/adhoc-data-sync.ts`
|
|
302
|
+
|
|
303
|
+
**Purpose**: Manual data sync trigger (on-demand)
|
|
304
|
+
**Trigger**: HTTP POST
|
|
305
|
+
**Endpoint**: `POST https://{workspace}.versori.run/data-batch-adhoc`
|
|
306
|
+
**Use Cases**: Testing, priority processing, ad-hoc runs
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
import { webhook, http } from '@versori/run';
|
|
310
|
+
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
311
|
+
import { runIngestion } from '../../services/data-sync.service.ts';
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Webhook: Manual Data Sync Trigger
|
|
315
|
+
*
|
|
316
|
+
* Endpoint: POST https://{workspace}.versori.run/data-batch-adhoc
|
|
317
|
+
* Request body (optional): { filePattern: "urgent_*.xml", maxFiles: 5 }
|
|
318
|
+
*
|
|
319
|
+
* Pattern: webhook().then(http()) - needs Fluent API access
|
|
320
|
+
* Uses shared service: data-sync.service.ts
|
|
321
|
+
*/
|
|
322
|
+
export const adhoc_data_sync = webhook('data-batch-adhoc', {
|
|
323
|
+
response: { mode: 'sync' },
|
|
324
|
+
connection: 'data-batch-adhoc', // Versori validates API key
|
|
325
|
+
}).then(
|
|
326
|
+
http('run-data-batch-adhoc', { connection: 'fluent_commerce' }, async ctx => {
|
|
327
|
+
const { log, openKv, data } = ctx;
|
|
328
|
+
const jobId = `data-batch-adhoc-${Date.now()}`;
|
|
329
|
+
const tracker = new JobTracker(openKv(':project:'), log);
|
|
330
|
+
|
|
331
|
+
await tracker.createJob(jobId, {
|
|
332
|
+
triggeredBy: 'manual',
|
|
333
|
+
stage: 'initialization',
|
|
334
|
+
options: data // Optional: filePattern, maxFiles, etc.
|
|
335
|
+
});
|
|
336
|
+
await tracker.updateJob(jobId, { status: 'processing' });
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
// Same orchestration logic as scheduled workflow
|
|
340
|
+
const result = await runIngestion(ctx, jobId, tracker);
|
|
341
|
+
await tracker.markCompleted(jobId, result);
|
|
342
|
+
return { success: true, jobId, ...result };
|
|
343
|
+
} catch (e: any) {
|
|
344
|
+
await tracker.markFailed(jobId, e);
|
|
345
|
+
return { success: false, jobId, error: e?.message };
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
);
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
#### `src/workflows/webhook/job-status-check.ts`
|
|
352
|
+
|
|
353
|
+
**Purpose**: Query job status
|
|
354
|
+
**Trigger**: HTTP POST
|
|
355
|
+
**Endpoint**: `POST https://{workspace}.versori.run/data-batch-job-status`
|
|
356
|
+
**Request body**: `{ jobId: "data-batch-1234567890" }`
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
import { webhook, fn } from '@versori/run';
|
|
360
|
+
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Webhook: Job Status Check
|
|
364
|
+
*
|
|
365
|
+
* Endpoint: POST https://{workspace}.versori.run/data-batch-job-status
|
|
366
|
+
* Request body: { jobId: "data-batch-1234567890" }
|
|
367
|
+
*
|
|
368
|
+
* Pattern: webhook().then(fn()) - no external API needed, only KV storage
|
|
369
|
+
* Lightweight: Only queries KV store, no Fluent API calls
|
|
370
|
+
*/
|
|
371
|
+
export const jobStatusCheck = webhook('data-batch-job-status', {
|
|
372
|
+
response: { mode: 'sync' },
|
|
373
|
+
connection: 'data-batch-job-status',
|
|
374
|
+
}).then(
|
|
375
|
+
fn('status', async ctx => {
|
|
376
|
+
const { data, log, openKv } = ctx;
|
|
377
|
+
const jobId = data?.jobId as string;
|
|
378
|
+
|
|
379
|
+
if (!jobId) {
|
|
380
|
+
return { success: false, error: 'jobId required' };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const tracker = new JobTracker(openKv(':project:'), log);
|
|
384
|
+
const status = await tracker.getJob(jobId);
|
|
385
|
+
|
|
386
|
+
return status
|
|
387
|
+
? { success: true, jobId, ...status }
|
|
388
|
+
: { success: false, error: 'Job not found', jobId };
|
|
389
|
+
})
|
|
390
|
+
);
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
### 3. Entry Point (`index.ts`)
|
|
396
|
+
|
|
397
|
+
**Purpose**: Register all workflows with Versori platform
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
/**
|
|
401
|
+
* Entry Point - Registers all workflows with Versori platform
|
|
402
|
+
*
|
|
403
|
+
* Versori automatically discovers and registers exported workflows
|
|
404
|
+
*
|
|
405
|
+
* File Structure:
|
|
406
|
+
* - src/workflows/scheduled/ → Time-based triggers (cron)
|
|
407
|
+
* - src/workflows/webhook/ → HTTP-based triggers (webhooks)
|
|
408
|
+
*/
|
|
409
|
+
|
|
410
|
+
// Import scheduled workflows
|
|
411
|
+
import { daily_data_sync } from './src/workflows/scheduled/daily-data-sync';
|
|
412
|
+
|
|
413
|
+
// Import webhook workflows
|
|
414
|
+
import { adhoc_data_sync } from './src/workflows/webhook/adhoc-data-sync';
|
|
415
|
+
import { jobStatusCheck } from './src/workflows/webhook/job-status-check';
|
|
416
|
+
|
|
417
|
+
// Register all workflows
|
|
418
|
+
export {
|
|
419
|
+
// Scheduled (time-based triggers)
|
|
420
|
+
daily_data_sync,
|
|
421
|
+
|
|
422
|
+
// Webhooks (HTTP-based triggers)
|
|
423
|
+
adhoc_data_sync,
|
|
424
|
+
jobStatusCheck,
|
|
425
|
+
};
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**What Gets Exposed:**
|
|
429
|
+
|
|
430
|
+
- ✅ `adhoc_data_sync` → `https://{workspace}.versori.run/data-batch-adhoc`
|
|
431
|
+
- ✅ `jobStatusCheck` → `https://{workspace}.versori.run/data-batch-job-status`
|
|
432
|
+
- ❌ `daily_data_sync` → NOT exposed (runs automatically on cron)
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
### Adding New Workflows
|
|
437
|
+
|
|
438
|
+
**To add a scheduled workflow:**
|
|
439
|
+
1. Create file in `src/workflows/scheduled/` with descriptive name (e.g., `hourly-delta-sync.ts`)
|
|
440
|
+
2. Export the workflow from the file
|
|
441
|
+
3. Import and re-export in `index.ts`
|
|
442
|
+
|
|
443
|
+
**To add a webhook workflow:**
|
|
444
|
+
1. Create file in `src/workflows/webhook/` with descriptive name (e.g., `error-notification.ts`)
|
|
445
|
+
2. Export the workflow from the file
|
|
446
|
+
3. Import and re-export in `index.ts`
|
|
447
|
+
|
|
448
|
+
**Example - Adding hourly delta sync:**
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
// src/workflows/scheduled/hourly-delta-sync.ts
|
|
452
|
+
export const hourlyDeltaSync = schedule(
|
|
453
|
+
'data-delta-hourly',
|
|
454
|
+
'0 * * * *' // Every hour
|
|
455
|
+
).then(
|
|
456
|
+
http('run-delta-sync', { connection: 'fluent_commerce' }, async ctx => {
|
|
457
|
+
// Delta sync logic (skip BPP)
|
|
458
|
+
const result = await runIngestion(ctx, jobId, tracker, { bppEnabled: false });
|
|
459
|
+
return result;
|
|
460
|
+
})
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
// index.ts (add to imports and exports)
|
|
464
|
+
import { hourlyDeltaSync } from './src/workflows/scheduled/hourly-delta-sync';
|
|
465
|
+
export { daily_data_sync, hourlyDeltaSync, ... };
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
## What You'll Build
|
|
470
|
+
|
|
471
|
+
- Versori scheduled reconciliation workflow (daily) with manual webhook trigger and job status query
|
|
472
|
+
- Flexible data source (S3 or SFTP) with file tracking and archival
|
|
473
|
+
- CSV parsing with validation
|
|
474
|
+
- Bulk GraphQL query for current inventory (single query, O(1) lookup)
|
|
475
|
+
- Variance calculation with edge case handling (zero-to-zero, divide-by-zero)
|
|
476
|
+
- Dual threshold logic (percentage AND absolute variance)
|
|
477
|
+
- Batch API adjustments with full audit trail attributes
|
|
478
|
+
- Reconciliation report generation (auto-adjusted vs manual review items)
|
|
479
|
+
- JobTracker integration for multi-step workflow visibility
|
|
480
|
+
- VersoriFileTracker for duplicate prevention
|
|
481
|
+
|
|
482
|
+
## Workflows (scheduled, ad hoc, job status)
|
|
483
|
+
|
|
484
|
+
**Security Note:** Webhook authentication is handled by Versori via the `connection` parameter in the webhook configuration. No manual API key validation needed.
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
import { schedule, webhook, http, fn } from '@versori/run';
|
|
488
|
+
import { Buffer } from 'node:buffer'; // Required for Versori/Deno runtime
|
|
489
|
+
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
490
|
+
|
|
491
|
+
export const scheduledReconciliation = schedule('cycle-count-recon-scheduled', '0 2 * * *').then(
|
|
492
|
+
http('run-reconciliation', { connection: 'fluent_commerce' }, async ctx => {
|
|
493
|
+
const { log, openKv } = ctx;
|
|
494
|
+
const jobId = `cycle-count-recon-${Date.now()}`;
|
|
495
|
+
const tracker = new JobTracker(openKv(':project:'), log);
|
|
496
|
+
await tracker.createJob(jobId, { triggeredBy: 'schedule', stage: 'initialization' });
|
|
497
|
+
await tracker.updateJob(jobId, { status: 'processing' });
|
|
498
|
+
try {
|
|
499
|
+
const result = await runReconciliation(ctx, jobId, tracker);
|
|
500
|
+
await tracker.markCompleted(jobId, result);
|
|
501
|
+
return { success: true, jobId, ...result };
|
|
502
|
+
} catch (e: any) {
|
|
503
|
+
await tracker.markFailed(jobId, e);
|
|
504
|
+
return { success: false, jobId, error: e?.message };
|
|
505
|
+
}
|
|
506
|
+
})
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
export const adhocReconciliation = webhook('cycle-count-recon-adhoc', {
|
|
510
|
+
response: { mode: 'sync' },
|
|
511
|
+
connection: 'cycle-count-recon-adhoc', // Webhook auth (validates incoming X-API-Key)
|
|
512
|
+
}).then(
|
|
513
|
+
http('run-reconciliation-adhoc', { connection: 'fluent_commerce' }, async ctx => {
|
|
514
|
+
const { data, log, openKv } = ctx;
|
|
515
|
+
const jobId = `cycle-count-recon-adhoc-${Date.now()}`;
|
|
516
|
+
const tracker = new JobTracker(openKv(':project:'), log);
|
|
517
|
+
await tracker.createJob(jobId, { triggeredBy: 'manual', stage: 'initialization', details: { forceReprocess: data?.forceReprocess } });
|
|
518
|
+
await tracker.updateJob(jobId, { status: 'processing' });
|
|
519
|
+
try {
|
|
520
|
+
const result = await runReconciliation(ctx, jobId, tracker, data?.forceReprocess);
|
|
521
|
+
await tracker.markCompleted(jobId, result);
|
|
522
|
+
return { success: true, jobId, ...result };
|
|
523
|
+
} catch (e: any) {
|
|
524
|
+
await tracker.markFailed(jobId, e);
|
|
525
|
+
return { success: false, jobId, error: e?.message };
|
|
526
|
+
}
|
|
527
|
+
})
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
export const reconciliationJobStatus = webhook('cycle-count-recon-job-status', {
|
|
531
|
+
response: { mode: 'sync' },
|
|
532
|
+
connection: 'cycle-count-recon-job-status', // Webhook auth (validates incoming X-API-Key)
|
|
533
|
+
}).then(
|
|
534
|
+
fn('status', async (ctx) => {
|
|
535
|
+
const { data, log, openKv } = ctx;
|
|
536
|
+
const jobId = data?.jobId as string;
|
|
537
|
+
if (!jobId) return { success: false, error: 'jobId required' };
|
|
538
|
+
const tracker = new JobTracker(openKv(':project:'), log);
|
|
539
|
+
const status = await tracker.getJob(jobId);
|
|
540
|
+
return status
|
|
541
|
+
? { success: true, jobId, ...status }
|
|
542
|
+
: { success: false, error: 'Job not found', jobId };
|
|
543
|
+
})
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
// Ensure runReconciliation(ctx, jobId, tracker, forceReprocess) uses tracker.start/update/complete/fail for major steps
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## SDK Methods Used
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
import {
|
|
553
|
+
createClient,
|
|
554
|
+
CSVParserService,
|
|
555
|
+
UniversalMapper,
|
|
556
|
+
S3DataSource,
|
|
557
|
+
SftpDataSource,
|
|
558
|
+
VersoriFileTracker,
|
|
559
|
+
JobTracker,
|
|
560
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
561
|
+
import cycleCountMapping from './config/cycle-count.mapping.json' with { type: 'json' };
|
|
562
|
+
|
|
563
|
+
await createClient(ctx); // Versori context-aware client
|
|
564
|
+
const s3 = new S3DataSource({ type: 'S3_CSV', connectionId, name, s3Config }, log); // S3 ops
|
|
565
|
+
const sftp = new SftpDataSource({ type: 'SFTP_CSV', connectionId, name, sftpConfig }, log); // SFTP ops
|
|
566
|
+
const parser = new CSVParserService(); // CSV parsing
|
|
567
|
+
const mapper = new UniversalMapper(cycleCountMapping); // Transform cycle count records
|
|
568
|
+
const fileTracker = new VersoriFileTracker(openKv(':project:'), 'cycle-count-recon'); // File tracking
|
|
569
|
+
const jobTracker = new JobTracker(openKv(':project:'), log); // Job lifecycle tracking
|
|
570
|
+
await client.graphql({ query, variables, pagination: { maxPages: 100 } }); // Bulk inventory query
|
|
571
|
+
await client.createJob({ name, retailerId }); // Batch job create
|
|
572
|
+
await client.sendBatch(jobId, { action, entityType, entities }); // Send adjustments (fire-and-forget)
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
## Config blocks (for PDF/AI ingestion)
|
|
576
|
+
|
|
577
|
+
### Activation Variables (S3)
|
|
578
|
+
|
|
579
|
+
```json
|
|
580
|
+
{
|
|
581
|
+
"dataSource": "s3",
|
|
582
|
+
"s3BucketName": "wms-cycle-counts",
|
|
583
|
+
"awsRegion": "us-east-1",
|
|
584
|
+
"awsAccessKeyId": "AKIA...",
|
|
585
|
+
"awsSecretAccessKey": "...",
|
|
586
|
+
"s3Prefix": "cycle-counts/incoming/",
|
|
587
|
+
"archivePrefix": "cycle-counts/processed/",
|
|
588
|
+
"errorPrefix": "cycle-counts/errors/",
|
|
589
|
+
"filePattern": "cycle-count-*.csv",
|
|
590
|
+
"maxFilesPerRun": 5,
|
|
591
|
+
"variancePercentThreshold": 5,
|
|
592
|
+
"varianceAbsoluteThreshold": 10,
|
|
593
|
+
"enableAutoAdjustment": "true",
|
|
594
|
+
"enableArchival": "true",
|
|
595
|
+
"retailerId": "your-retailer-id"
|
|
596
|
+
}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Activation Variables (SFTP)
|
|
600
|
+
|
|
601
|
+
```json
|
|
602
|
+
{
|
|
603
|
+
"dataSource": "sftp",
|
|
604
|
+
"sftpHost": "sftp.warehouse.com",
|
|
605
|
+
"sftpPort": 22,
|
|
606
|
+
"sftpUsername": "cycle_count_user",
|
|
607
|
+
"sftpPassword": "...",
|
|
608
|
+
"sftpIncomingPath": "/cycle-counts/incoming",
|
|
609
|
+
"sftpProcessedPath": "/cycle-counts/processed",
|
|
610
|
+
"sftpErrorPath": "/cycle-counts/errors",
|
|
611
|
+
"filePattern": "cycle-count-*.csv",
|
|
612
|
+
"maxFilesPerRun": 5,
|
|
613
|
+
"variancePercentThreshold": 5,
|
|
614
|
+
"varianceAbsoluteThreshold": 10,
|
|
615
|
+
"enableAutoAdjustment": "true",
|
|
616
|
+
"retailerId": "your-retailer-id"
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### Batch Payload Defaults
|
|
621
|
+
|
|
622
|
+
```json
|
|
623
|
+
{
|
|
624
|
+
"action": "UPSERT",
|
|
625
|
+
"entityType": "INVENTORY",
|
|
626
|
+
"source": "CYCLE_COUNT",
|
|
627
|
+
"event": "CYCLE_COUNT_RECONCILIATION",
|
|
628
|
+
"bpp": "enabled"
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
## Sample CSV Input Data
|
|
633
|
+
|
|
634
|
+
```csv
|
|
635
|
+
location_code,sku,counted_quantity,count_date,counter_username,bin_location,lot_number
|
|
636
|
+
DC01,ACME-WIDGET-100,245,2025-01-17T08:30:00Z,john.smith,A-01-05,LOT20250115
|
|
637
|
+
DC01,ACME-GADGET-200,0,2025-01-17T08:32:00Z,john.smith,A-01-06,
|
|
638
|
+
DC01,ACME-TOOL-300,1520,2025-01-17T08:35:00Z,john.smith,A-02-01,LOT20250110
|
|
639
|
+
DC02,ACME-WIDGET-100,87,2025-01-17T09:00:00Z,jane.doe,B-03-12,LOT20250115
|
|
640
|
+
DC02,ACME-PART-400,2340,2025-01-17T09:05:00Z,jane.doe,B-03-13,LOT20250112
|
|
641
|
+
DC02,ACME-COMPONENT-500,45,2025-01-17T09:10:00Z,jane.doe,B-04-01,
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### Column Descriptions
|
|
645
|
+
|
|
646
|
+
- `location_code`: Warehouse location identifier (maps to `locationRef`)
|
|
647
|
+
- `sku`: Product SKU (maps to `skuRef`)
|
|
648
|
+
- `counted_quantity`: Physical count from warehouse team
|
|
649
|
+
- `count_date`: ISO 8601 timestamp when count was performed
|
|
650
|
+
- `counter_username`: Username of person who performed count
|
|
651
|
+
- `bin_location`: Specific bin/shelf location within warehouse
|
|
652
|
+
- `lot_number`: Lot/batch number if applicable (for lot-tracked items)
|
|
653
|
+
|
|
654
|
+
### Mapping file (create at ./config/cycle-count.mapping.json)
|
|
655
|
+
|
|
656
|
+
```text
|
|
657
|
+
Create file: ./config/cycle-count.mapping.json
|
|
658
|
+
|
|
659
|
+
{
|
|
660
|
+
"name": "cycle-count.mapping",
|
|
661
|
+
"version": "1.0.0",
|
|
662
|
+
"description": "CSV cycle count to reconciliation record mapping",
|
|
663
|
+
"fields": {
|
|
664
|
+
"locationRef": { "source": "location_code", "required": true, "resolver": "sdk.trim" },
|
|
665
|
+
"skuRef": { "source": "sku", "required": true, "resolver": "sdk.trim" },
|
|
666
|
+
"countedQty": { "source": "counted_quantity", "required": true, "resolver": "sdk.parseInt" },
|
|
667
|
+
"countDate": { "source": "count_date", "required": false },
|
|
668
|
+
"counterName": { "source": "counter_username", "required": false },
|
|
669
|
+
"binLocation": { "source": "bin_location", "required": false },
|
|
670
|
+
"lotNumber": { "source": "lot_number", "required": false }
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
**Field Mapping**:
|
|
676
|
+
|
|
677
|
+
- `locationRef` → Fluent location reference
|
|
678
|
+
- `skuRef` → Fluent SKU reference
|
|
679
|
+
- `countedQty` → Physical counted quantity (integer)
|
|
680
|
+
- `countDate` → When count was performed (ISO 8601 timestamp)
|
|
681
|
+
- `counterName` → Person who performed count (for audit trail)
|
|
682
|
+
- `binLocation` → Specific bin/shelf location
|
|
683
|
+
- `lotNumber` → Lot/batch number for lot-tracked items
|
|
684
|
+
|
|
685
|
+
## Project Setup
|
|
686
|
+
|
|
687
|
+
```bash
|
|
688
|
+
mkdir versori-cycle-count-reconciliation && cd $_
|
|
689
|
+
npm init -y
|
|
690
|
+
npm install @fluentcommerce/fc-connect-sdk @versori/run
|
|
691
|
+
mkdir -p config
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
### Package Configuration (package.json)
|
|
695
|
+
|
|
696
|
+
```json
|
|
697
|
+
{
|
|
698
|
+
"name": "versori-cycle-count-reconciliation",
|
|
699
|
+
"version": "1.0.0",
|
|
700
|
+
"description": "Versori workflow: Cycle count reconciliation with variance analysis and Batch API",
|
|
701
|
+
"versori": {
|
|
702
|
+
"workflows": "./index.ts"
|
|
703
|
+
},
|
|
704
|
+
"type": "module",
|
|
705
|
+
"scripts": {
|
|
706
|
+
"deploy": "versori deploy",
|
|
707
|
+
"logs": "versori logs"
|
|
708
|
+
},
|
|
709
|
+
"dependencies": {
|
|
710
|
+
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
711
|
+
"@versori/run": "latest"
|
|
712
|
+
},
|
|
713
|
+
"devDependencies": {
|
|
714
|
+
"typescript": "^5.0.0",
|
|
715
|
+
"@types/node": "^20.0.0"
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
### TypeScript Configuration (tsconfig.json)
|
|
721
|
+
|
|
722
|
+
```json
|
|
723
|
+
{
|
|
724
|
+
"compilerOptions": {
|
|
725
|
+
"module": "ES2022",
|
|
726
|
+
"target": "ES2024",
|
|
727
|
+
"moduleResolution": "node"
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
### Activation Variables (Versori)
|
|
733
|
+
|
|
734
|
+
**For S3:**
|
|
735
|
+
```bash
|
|
736
|
+
# Required Variables
|
|
737
|
+
dataSource=s3
|
|
738
|
+
s3BucketName=wms-cycle-counts
|
|
739
|
+
awsAccessKeyId=AKIAXXXXXXXXXXXX
|
|
740
|
+
awsSecretAccessKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
741
|
+
retailerId=your-retailer-id
|
|
742
|
+
|
|
743
|
+
# Optional Variables (with defaults shown)
|
|
744
|
+
awsRegion=us-east-1
|
|
745
|
+
s3Prefix=cycle-counts/incoming/
|
|
746
|
+
archivePrefix=cycle-counts/processed/
|
|
747
|
+
errorPrefix=cycle-counts/errors/
|
|
748
|
+
filePattern=cycle-count-*.csv
|
|
749
|
+
maxFilesPerRun=5
|
|
750
|
+
variancePercentThreshold=5
|
|
751
|
+
varianceAbsoluteThreshold=10
|
|
752
|
+
enableAutoAdjustment=true
|
|
753
|
+
enableArchival=true
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
**For SFTP:**
|
|
757
|
+
```bash
|
|
758
|
+
# Required Variables
|
|
759
|
+
dataSource=sftp
|
|
760
|
+
sftpHost=sftp.warehouse.com
|
|
761
|
+
sftpPort=22
|
|
762
|
+
sftpUsername=cycle_count_user
|
|
763
|
+
sftpPassword=xxxxxxxx
|
|
764
|
+
sftpIncomingPath=/cycle-counts/incoming
|
|
765
|
+
sftpProcessedPath=/cycle-counts/processed
|
|
766
|
+
sftpErrorPath=/cycle-counts/errors
|
|
767
|
+
retailerId=your-retailer-id
|
|
768
|
+
|
|
769
|
+
# Optional Variables (with defaults shown)
|
|
770
|
+
filePattern=cycle-count-*.csv
|
|
771
|
+
maxFilesPerRun=5
|
|
772
|
+
variancePercentThreshold=5
|
|
773
|
+
varianceAbsoluteThreshold=10
|
|
774
|
+
enableAutoAdjustment=true
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
**Security Note:** Webhook authentication is now handled by Versori via the `connection` parameter. No manual API key validation needed.
|
|
778
|
+
|
|
779
|
+
---
|
|
780
|
+
|
|
781
|
+
## 🏗️ Production Modular Structure Implementation
|
|
782
|
+
|
|
783
|
+
> **✅ This section shows the COMPLETE production-ready modular structure.**
|
|
784
|
+
> All files are shown with proper imports/exports and folder organization.
|
|
785
|
+
> This follows the gold standard pattern from template-ingestion-sftp-xml-inventory-batch.md
|
|
786
|
+
|
|
787
|
+
### Complete Project Structure
|
|
788
|
+
|
|
789
|
+
```
|
|
790
|
+
cycle-count-reconciliation/
|
|
791
|
+
├── package.json # Dependencies and Versori config
|
|
792
|
+
├── tsconfig.json # TypeScript configuration
|
|
793
|
+
├── src/
|
|
794
|
+
│ ├── index.ts # Workflow entry point (exports)
|
|
795
|
+
│ ├── workflows/
|
|
796
|
+
│ │ └── cycle-count-reconciliation.workflow.ts # Main orchestrator
|
|
797
|
+
│ ├── services/
|
|
798
|
+
│ │ ├── inventory-query.service.ts # Bulk GraphQL inventory queries
|
|
799
|
+
│ │ ├── variance-calculator.service.ts # Variance calculation with dual thresholds
|
|
800
|
+
│ │ ├── batch-processor.service.ts # Batch API submission logic
|
|
801
|
+
│ │ └── report-generator.service.ts # Reconciliation report generation
|
|
802
|
+
│ ├── types/
|
|
803
|
+
│ │ └── cycle-count-types.ts # TypeScript interfaces
|
|
804
|
+
│ ├── utils/
|
|
805
|
+
│ │ ├── retry.ts # Retry with exponential backoff
|
|
806
|
+
│ │ └── data-source-factory.ts # S3/SFTP initialization
|
|
807
|
+
│ └── config/
|
|
808
|
+
│ └── cycle-count.mapping.json # Mapping configuration (external JSON)
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
---
|
|
812
|
+
|
|
813
|
+
### File: `src/index.ts`
|
|
814
|
+
|
|
815
|
+
```typescript
|
|
816
|
+
import { schedule, webhook, http, fn } from '@versori/run';
|
|
817
|
+
import { processReconciliation, getReconciliationStatus } from './workflows/cycle-count-reconciliation.workflow';
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Scheduled workflow: Daily cycle count reconciliation
|
|
821
|
+
* Runs daily at 2 AM UTC
|
|
822
|
+
*/
|
|
823
|
+
export const scheduledReconciliation = schedule(
|
|
824
|
+
'cycle-count-recon-scheduled',
|
|
825
|
+
'0 2 * * *' // 2 AM UTC daily
|
|
826
|
+
).then(
|
|
827
|
+
http('run-reconciliation', { connection: 'fluent_commerce' }, async ctx => {
|
|
828
|
+
return await processReconciliation(ctx, { triggeredBy: 'schedule' });
|
|
829
|
+
})
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Manual trigger endpoint for ad-hoc reconciliation
|
|
834
|
+
*/
|
|
835
|
+
export const adhocReconciliation = webhook('cycle-count-recon-adhoc', {
|
|
836
|
+
response: { mode: 'sync' },
|
|
837
|
+
connection: 'cycle-count-recon-adhoc',
|
|
838
|
+
}).then(
|
|
839
|
+
http('run-reconciliation-adhoc', { connection: 'fluent_commerce' }, async ctx => {
|
|
840
|
+
const { data } = ctx;
|
|
841
|
+
return await processReconciliation(ctx, {
|
|
842
|
+
triggeredBy: 'manual',
|
|
843
|
+
forceReprocess: data?.forceReprocess === true,
|
|
844
|
+
});
|
|
845
|
+
})
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Job status query endpoint
|
|
850
|
+
*/
|
|
851
|
+
export const reconciliationJobStatus = webhook('cycle-count-recon-job-status', {
|
|
852
|
+
response: { mode: 'sync' },
|
|
853
|
+
connection: 'cycle-count-recon-job-status',
|
|
854
|
+
}).then(
|
|
855
|
+
fn('status', async ctx => {
|
|
856
|
+
const { data } = ctx;
|
|
857
|
+
return await getReconciliationStatus(ctx, data?.jobId);
|
|
858
|
+
})
|
|
859
|
+
);
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
---
|
|
863
|
+
|
|
864
|
+
### File: `src/types/cycle-count-types.ts`
|
|
865
|
+
|
|
866
|
+
```typescript
|
|
867
|
+
/**
|
|
868
|
+
* Type definitions for cycle count reconciliation workflow
|
|
869
|
+
*/
|
|
870
|
+
|
|
871
|
+
export interface CycleCountRecord {
|
|
872
|
+
locationRef: string;
|
|
873
|
+
skuRef: string;
|
|
874
|
+
countedQty: number;
|
|
875
|
+
countDate?: string;
|
|
876
|
+
counterName?: string;
|
|
877
|
+
binLocation?: string;
|
|
878
|
+
lotNumber?: string;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
export interface FluentInventory {
|
|
882
|
+
id: string;
|
|
883
|
+
locationRef: string;
|
|
884
|
+
skuRef: string;
|
|
885
|
+
qty: number;
|
|
886
|
+
type: string;
|
|
887
|
+
status: string;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
export interface VarianceItem extends CycleCountRecord {
|
|
891
|
+
fluentQty: number;
|
|
892
|
+
varianceQty: number;
|
|
893
|
+
variancePercent: number;
|
|
894
|
+
action: 'AUTO_ADJUST' | 'MANUAL_REVIEW';
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
export interface ReconciliationReport {
|
|
898
|
+
fileName: string;
|
|
899
|
+
processedAt: string;
|
|
900
|
+
summary: {
|
|
901
|
+
totalItemsCounted: number;
|
|
902
|
+
mappingErrors: number;
|
|
903
|
+
itemsWithNoVariance: number;
|
|
904
|
+
itemsWithVariance: number;
|
|
905
|
+
autoAdjustmentsCreated: number;
|
|
906
|
+
autoAdjustmentsFailed: number;
|
|
907
|
+
manualReviewRequired: number;
|
|
908
|
+
};
|
|
909
|
+
thresholds: {
|
|
910
|
+
percentThreshold: number;
|
|
911
|
+
absoluteThreshold: number;
|
|
912
|
+
};
|
|
913
|
+
variances: {
|
|
914
|
+
autoAdjusted: VarianceItem[];
|
|
915
|
+
manualReview: VarianceItem[];
|
|
916
|
+
};
|
|
917
|
+
errors?: any[];
|
|
918
|
+
}
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
---
|
|
922
|
+
|
|
923
|
+
### File: `src/utils/retry.ts`
|
|
924
|
+
|
|
925
|
+
```typescript
|
|
926
|
+
/**
|
|
927
|
+
* Retry utility with exponential backoff
|
|
928
|
+
*/
|
|
929
|
+
export async function retryWithBackoff<T>(
|
|
930
|
+
operation: () => Promise<T>,
|
|
931
|
+
maxRetries = 3,
|
|
932
|
+
baseDelayMs = 1000
|
|
933
|
+
): Promise<T> {
|
|
934
|
+
let lastError: any;
|
|
935
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
936
|
+
try {
|
|
937
|
+
return await operation();
|
|
938
|
+
} catch (error) {
|
|
939
|
+
lastError = error;
|
|
940
|
+
if (attempt < maxRetries - 1) {
|
|
941
|
+
const delay = baseDelayMs * Math.pow(2, attempt) + Math.floor(Math.random() * 200);
|
|
942
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
throw lastError;
|
|
947
|
+
}
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
---
|
|
951
|
+
|
|
952
|
+
### File: `src/utils/data-source-factory.ts`
|
|
953
|
+
|
|
954
|
+
```typescript
|
|
955
|
+
import { S3DataSource, SftpDataSource } from '@fluentcommerce/fc-connect-sdk';
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Initialize S3 data source from activation variables
|
|
959
|
+
*/
|
|
960
|
+
export function initializeS3DataSource(activation: any, log): S3DataSource { // ✅ Versori native log - TypeScript infers type
|
|
961
|
+
return new S3DataSource(
|
|
962
|
+
{
|
|
963
|
+
type: 'S3_CSV',
|
|
964
|
+
connectionId: 's3-cycle-counts',
|
|
965
|
+
name: 'Cycle Count S3',
|
|
966
|
+
s3Config: {
|
|
967
|
+
bucket: activation.getVariable('s3BucketName'),
|
|
968
|
+
region: activation.getVariable('awsRegion') || 'us-east-1',
|
|
969
|
+
accessKeyId: activation.getVariable('awsAccessKeyId'),
|
|
970
|
+
secretAccessKey: activation.getVariable('awsSecretAccessKey'),
|
|
971
|
+
},
|
|
972
|
+
},
|
|
973
|
+
log
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Initialize SFTP data source from activation variables
|
|
979
|
+
*/
|
|
980
|
+
export function initializeSftpDataSource(activation: any, log): SftpDataSource { // ✅ Versori native log - TypeScript infers type
|
|
981
|
+
return new SftpDataSource(
|
|
982
|
+
{
|
|
983
|
+
type: 'SFTP_CSV',
|
|
984
|
+
connectionId: 'sftp-cycle-counts',
|
|
985
|
+
name: 'Cycle Count SFTP',
|
|
986
|
+
sftpConfig: {
|
|
987
|
+
host: activation.getVariable('sftpHost'),
|
|
988
|
+
port: parseInt(activation.getVariable('sftpPort') || '22', 10),
|
|
989
|
+
username: activation.getVariable('sftpUsername'),
|
|
990
|
+
password: activation.getVariable('sftpPassword'),
|
|
991
|
+
privateKey: activation.getVariable('sftpPrivateKey'),
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
log
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
---
|
|
1000
|
+
|
|
1001
|
+
### File: `src/services/inventory-query.service.ts`
|
|
1002
|
+
|
|
1003
|
+
```typescript
|
|
1004
|
+
import type { FluentClient } from '@fluentcommerce/fc-connect-sdk';
|
|
1005
|
+
import type { CycleCountRecord, FluentInventory } from '../types/cycle-count-types';
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Service for querying current Fluent inventory in bulk
|
|
1009
|
+
*/
|
|
1010
|
+
export class InventoryQueryService {
|
|
1011
|
+
constructor(
|
|
1012
|
+
private client: FluentClient
|
|
1013
|
+
// ✅ No logger - workflow handles logging with Versori native log
|
|
1014
|
+
) {}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Query current inventory for all cycle count items
|
|
1018
|
+
* Uses single GraphQL query with auto-pagination for O(1) lookup
|
|
1019
|
+
*/
|
|
1020
|
+
async queryCurrentInventory(
|
|
1021
|
+
cycleCounts: CycleCountRecord[]
|
|
1022
|
+
): Promise<Map<string, FluentInventory>> {
|
|
1023
|
+
// Extract unique location + SKU combinations
|
|
1024
|
+
const uniqueLocations = [...new Set(cycleCounts.map(cc => cc.locationRef))];
|
|
1025
|
+
const uniqueSkus = [...new Set(cycleCounts.map(cc => cc.skuRef))];
|
|
1026
|
+
|
|
1027
|
+
locations: uniqueLocations.length,
|
|
1028
|
+
skus: uniqueSkus.length,
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
// Build GraphQL query
|
|
1032
|
+
const query = `
|
|
1033
|
+
query GetInventoryForReconciliation($first: Int, $after: String) {
|
|
1034
|
+
inventoryQuantities(first: $first, after: $after) {
|
|
1035
|
+
edges {
|
|
1036
|
+
node {
|
|
1037
|
+
id
|
|
1038
|
+
locationRef
|
|
1039
|
+
skuRef
|
|
1040
|
+
qty
|
|
1041
|
+
type
|
|
1042
|
+
status
|
|
1043
|
+
}
|
|
1044
|
+
cursor
|
|
1045
|
+
}
|
|
1046
|
+
pageInfo {
|
|
1047
|
+
hasNextPage
|
|
1048
|
+
endCursor
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
`;
|
|
1053
|
+
|
|
1054
|
+
// Execute query with auto-pagination
|
|
1055
|
+
const result = await this.client.graphql({
|
|
1056
|
+
query,
|
|
1057
|
+
variables: { first: 100 },
|
|
1058
|
+
pagination: { maxPages: 100 },
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
// Build lookup map (O(1) access)
|
|
1062
|
+
const inventoryMap = new Map<string, FluentInventory>();
|
|
1063
|
+
for (const edge of result.data?.inventoryQuantities?.edges || []) {
|
|
1064
|
+
const node = edge.node;
|
|
1065
|
+
const key = `${node.locationRef}:${node.skuRef}`;
|
|
1066
|
+
inventoryMap.set(key, node);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
return inventoryMap;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
---
|
|
1075
|
+
|
|
1076
|
+
### File: `src/services/variance-calculator.service.ts`
|
|
1077
|
+
|
|
1078
|
+
```typescript
|
|
1079
|
+
import type { CycleCountRecord, FluentInventory, VarianceItem } from '../types/cycle-count-types';
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Service for calculating inventory variances with dual threshold logic
|
|
1083
|
+
*/
|
|
1084
|
+
export class VarianceCalculatorService {
|
|
1085
|
+
constructor(
|
|
1086
|
+
private percentThreshold: number = 5,
|
|
1087
|
+
private absoluteThreshold: number = 10
|
|
1088
|
+
// ✅ No logger - workflow handles logging with Versori native log
|
|
1089
|
+
) {}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Calculate variances between counted and system inventory
|
|
1093
|
+
* Uses dual threshold logic (both must be met for auto-adjustment)
|
|
1094
|
+
*/
|
|
1095
|
+
calculateVariances(
|
|
1096
|
+
cycleCounts: CycleCountRecord[],
|
|
1097
|
+
inventoryMap: Map<string, FluentInventory>
|
|
1098
|
+
): VarianceItem[] {
|
|
1099
|
+
const variances: VarianceItem[] = [];
|
|
1100
|
+
|
|
1101
|
+
for (const cycleCount of cycleCounts) {
|
|
1102
|
+
const key = `${cycleCount.locationRef}:${cycleCount.skuRef}`;
|
|
1103
|
+
const fluentInventory = inventoryMap.get(key);
|
|
1104
|
+
const fluentQty = fluentInventory?.qty || 0;
|
|
1105
|
+
const countedQty = cycleCount.countedQty;
|
|
1106
|
+
|
|
1107
|
+
// Calculate variance
|
|
1108
|
+
const varianceQty = countedQty - fluentQty;
|
|
1109
|
+
|
|
1110
|
+
// Calculate percentage variance (handle divide-by-zero)
|
|
1111
|
+
const variancePercent =
|
|
1112
|
+
fluentQty === 0
|
|
1113
|
+
? countedQty === 0
|
|
1114
|
+
? 0 // 0 → 0 = no variance
|
|
1115
|
+
: 100 // 0 → N = 100% variance
|
|
1116
|
+
: Math.abs((varianceQty / fluentQty) * 100);
|
|
1117
|
+
|
|
1118
|
+
// Skip if no variance
|
|
1119
|
+
if (varianceQty === 0) {
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Determine action based on dual threshold
|
|
1124
|
+
const isWithinPercentThreshold = variancePercent < this.percentThreshold;
|
|
1125
|
+
const isWithinAbsoluteThreshold = Math.abs(varianceQty) < this.absoluteThreshold;
|
|
1126
|
+
const isWithinThreshold = isWithinPercentThreshold && isWithinAbsoluteThreshold;
|
|
1127
|
+
|
|
1128
|
+
const action = isWithinThreshold ? 'AUTO_ADJUST' : 'MANUAL_REVIEW';
|
|
1129
|
+
|
|
1130
|
+
variances.push({
|
|
1131
|
+
...cycleCount,
|
|
1132
|
+
fluentQty,
|
|
1133
|
+
varianceQty,
|
|
1134
|
+
variancePercent: parseFloat(variancePercent.toFixed(2)),
|
|
1135
|
+
action,
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
totalVariances: variances.length,
|
|
1140
|
+
autoAdjust: variances.filter(v => v.action === 'AUTO_ADJUST').length,
|
|
1141
|
+
manualReview: variances.filter(v => v.action === 'MANUAL_REVIEW').length,
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
return variances;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
---
|
|
1150
|
+
|
|
1151
|
+
### File: `src/services/batch-processor.service.ts`
|
|
1152
|
+
|
|
1153
|
+
```typescript
|
|
1154
|
+
import type { FluentClient } from '@fluentcommerce/fc-connect-sdk';
|
|
1155
|
+
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
1156
|
+
import type { VarianceItem } from '../types/cycle-count-types';
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Service for sending inventory adjustments via Batch API
|
|
1160
|
+
*
|
|
1161
|
+
* ✅ PRODUCTION ENHANCEMENT: Optional logger for detailed progress tracking
|
|
1162
|
+
*/
|
|
1163
|
+
export class BatchProcessorService {
|
|
1164
|
+
constructor(
|
|
1165
|
+
private client: FluentClient,
|
|
1166
|
+
private jobTracker: JobTracker,
|
|
1167
|
+
private log?: any // ✅ Optional logger for progress tracking
|
|
1168
|
+
) {}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Send inventory adjustment batches to Fluent Batch API
|
|
1172
|
+
*/
|
|
1173
|
+
async sendAdjustmentBatches(
|
|
1174
|
+
adjustments: VarianceItem[],
|
|
1175
|
+
fileName: string,
|
|
1176
|
+
retailerId: string
|
|
1177
|
+
): Promise<any> {
|
|
1178
|
+
|
|
1179
|
+
// ✅ PRODUCTION ENHANCEMENT: Log batch sending start
|
|
1180
|
+
if (this.log) {
|
|
1181
|
+
this.log.info('📤 Starting adjustment batch sending', {
|
|
1182
|
+
fileName,
|
|
1183
|
+
totalAdjustments: adjustments.length,
|
|
1184
|
+
retailerId,
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
try {
|
|
1189
|
+
// Create batch job (BPP enabled by default)
|
|
1190
|
+
const job = await this.client.createJob({
|
|
1191
|
+
name: `Cycle Count Reconciliation - ${fileName}`,
|
|
1192
|
+
retailerId,
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
// Build inventory adjustment entities
|
|
1197
|
+
const entities = adjustments.map(item => ({
|
|
1198
|
+
ref: `${item.locationRef}:${item.skuRef}`, // Composite ref
|
|
1199
|
+
locationRef: item.locationRef,
|
|
1200
|
+
skuRef: item.skuRef,
|
|
1201
|
+
qty: item.countedQty, // NEW absolute quantity (not delta)
|
|
1202
|
+
type: 'CORRECTION',
|
|
1203
|
+
status: 'AVAILABLE',
|
|
1204
|
+
attributes: [
|
|
1205
|
+
{
|
|
1206
|
+
name: 'cycle_count_adjustment',
|
|
1207
|
+
type: 'STRING',
|
|
1208
|
+
value: 'true',
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
name: 'original_qty',
|
|
1212
|
+
type: 'INTEGER',
|
|
1213
|
+
value: String(item.fluentQty),
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
name: 'variance_qty',
|
|
1217
|
+
type: 'INTEGER',
|
|
1218
|
+
value: String(item.varianceQty),
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
name: 'count_date',
|
|
1222
|
+
type: 'STRING',
|
|
1223
|
+
value: item.countDate || new Date().toISOString(),
|
|
1224
|
+
},
|
|
1225
|
+
{
|
|
1226
|
+
name: 'counter_name',
|
|
1227
|
+
type: 'STRING',
|
|
1228
|
+
value: item.counterName || 'WMS System',
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
name: 'source_file',
|
|
1232
|
+
type: 'STRING',
|
|
1233
|
+
value: fileName,
|
|
1234
|
+
},
|
|
1235
|
+
],
|
|
1236
|
+
}));
|
|
1237
|
+
|
|
1238
|
+
// Send batch (fire-and-forget - returns void, not an object)
|
|
1239
|
+
await this.client.sendBatch(job.id, {
|
|
1240
|
+
action: 'UPSERT',
|
|
1241
|
+
entityType: 'INVENTORY',
|
|
1242
|
+
entities,
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
// ✅ PRODUCTION ENHANCEMENT: Log completion
|
|
1246
|
+
if (this.log) {
|
|
1247
|
+
this.log.info('✅ Adjustment batch sending completed', {
|
|
1248
|
+
fileName,
|
|
1249
|
+
jobId: job.id,
|
|
1250
|
+
totalAdjustments: entities.length,
|
|
1251
|
+
retailerId,
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Batch submission successful
|
|
1256
|
+
return {
|
|
1257
|
+
jobId: job.id,
|
|
1258
|
+
successCount: entities.length,
|
|
1259
|
+
failureCount: 0,
|
|
1260
|
+
};
|
|
1261
|
+
} catch (error: any) {
|
|
1262
|
+
throw error;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
```
|
|
1267
|
+
|
|
1268
|
+
---
|
|
1269
|
+
|
|
1270
|
+
### File: `src/services/report-generator.service.ts`
|
|
1271
|
+
|
|
1272
|
+
```typescript
|
|
1273
|
+
import type {
|
|
1274
|
+
ReconciliationReport,
|
|
1275
|
+
VarianceItem,
|
|
1276
|
+
CycleCountRecord,
|
|
1277
|
+
} from '../types/cycle-count-types';
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Service for generating reconciliation reports
|
|
1281
|
+
*/
|
|
1282
|
+
export class ReportGeneratorService {
|
|
1283
|
+
constructor() {
|
|
1284
|
+
// ✅ No logger - workflow handles logging with Versori native log
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Generate reconciliation report with summary and variance details
|
|
1289
|
+
*/
|
|
1290
|
+
generateReconciliationReport(
|
|
1291
|
+
fileName: string,
|
|
1292
|
+
cycleCounts: CycleCountRecord[],
|
|
1293
|
+
mappingErrors: any[],
|
|
1294
|
+
variances: VarianceItem[],
|
|
1295
|
+
batchResult: any,
|
|
1296
|
+
percentThreshold: number,
|
|
1297
|
+
absoluteThreshold: number
|
|
1298
|
+
): ReconciliationReport {
|
|
1299
|
+
const autoAdjusted = variances.filter(v => v.action === 'AUTO_ADJUST');
|
|
1300
|
+
const manualReview = variances.filter(v => v.action === 'MANUAL_REVIEW');
|
|
1301
|
+
|
|
1302
|
+
return {
|
|
1303
|
+
fileName,
|
|
1304
|
+
processedAt: new Date().toISOString(),
|
|
1305
|
+
summary: {
|
|
1306
|
+
totalItemsCounted: cycleCounts.length,
|
|
1307
|
+
mappingErrors: mappingErrors.length,
|
|
1308
|
+
itemsWithNoVariance: cycleCounts.length - variances.length,
|
|
1309
|
+
itemsWithVariance: variances.length,
|
|
1310
|
+
autoAdjustmentsCreated: batchResult?.successCount || 0,
|
|
1311
|
+
autoAdjustmentsFailed: batchResult?.failureCount || 0,
|
|
1312
|
+
manualReviewRequired: manualReview.length,
|
|
1313
|
+
},
|
|
1314
|
+
thresholds: {
|
|
1315
|
+
percentThreshold,
|
|
1316
|
+
absoluteThreshold,
|
|
1317
|
+
},
|
|
1318
|
+
variances: {
|
|
1319
|
+
autoAdjusted: autoAdjusted.map(item => ({
|
|
1320
|
+
...item,
|
|
1321
|
+
adjustmentStatus: batchResult ? 'SUCCESS' : 'NOT_SENT',
|
|
1322
|
+
})),
|
|
1323
|
+
manualReview: manualReview.map(item => ({
|
|
1324
|
+
...item,
|
|
1325
|
+
reason:
|
|
1326
|
+
item.variancePercent >= percentThreshold
|
|
1327
|
+
? `Exceeds ${percentThreshold}% threshold`
|
|
1328
|
+
: `Exceeds ${absoluteThreshold} unit threshold`,
|
|
1329
|
+
})),
|
|
1330
|
+
},
|
|
1331
|
+
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
```
|
|
1336
|
+
|
|
1337
|
+
---
|
|
1338
|
+
|
|
1339
|
+
### File: `src/workflows/cycle-count-reconciliation.workflow.ts`
|
|
1340
|
+
|
|
1341
|
+
```typescript
|
|
1342
|
+
import { Buffer } from 'node:buffer';
|
|
1343
|
+
import {
|
|
1344
|
+
createClient,
|
|
1345
|
+
CSVParserService,
|
|
1346
|
+
UniversalMapper,
|
|
1347
|
+
VersoriFileTracker,
|
|
1348
|
+
JobTracker,
|
|
1349
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
1350
|
+
import type { FileMetadata } from '@fluentcommerce/fc-connect-sdk';
|
|
1351
|
+
import cycleCountMapping from '../config/cycle-count.mapping.json' with { type: 'json' };
|
|
1352
|
+
import { InventoryQueryService } from '../services/inventory-query.service';
|
|
1353
|
+
import { VarianceCalculatorService } from '../services/variance-calculator.service';
|
|
1354
|
+
import { BatchProcessorService } from '../services/batch-processor.service';
|
|
1355
|
+
import { ReportGeneratorService } from '../services/report-generator.service';
|
|
1356
|
+
import { retryWithBackoff } from '../utils/retry';
|
|
1357
|
+
import { initializeS3DataSource, initializeSftpDataSource } from '../utils/data-source-factory';
|
|
1358
|
+
import type { CycleCountRecord } from '../types/cycle-count-types';
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Main cycle count reconciliation workflow orchestrator
|
|
1362
|
+
*/
|
|
1363
|
+
export async function processReconciliation(
|
|
1364
|
+
ctx: any,
|
|
1365
|
+
options: { triggeredBy: string; forceReprocess?: boolean }
|
|
1366
|
+
) {
|
|
1367
|
+
const { log, openKv, activation } = ctx;
|
|
1368
|
+
const kv = openKv(':project:');
|
|
1369
|
+
const jobId = `cycle-count-recon-${options.triggeredBy}-${Date.now()}`;
|
|
1370
|
+
|
|
1371
|
+
// Initialize SDK clients
|
|
1372
|
+
const fluentClient = await createClient(ctx);
|
|
1373
|
+
const jobTracker = new JobTracker(kv, log);
|
|
1374
|
+
|
|
1375
|
+
await jobTracker.createJob(jobId, {
|
|
1376
|
+
triggeredBy: options.triggeredBy,
|
|
1377
|
+
stage: 'initialization',
|
|
1378
|
+
});
|
|
1379
|
+
await jobTracker.updateJob(jobId, { status: 'processing' });
|
|
1380
|
+
|
|
1381
|
+
// Initialize data source based on configuration
|
|
1382
|
+
const dataSourceType = activation.getVariable('dataSource');
|
|
1383
|
+
const dataSource =
|
|
1384
|
+
dataSourceType === 'sftp'
|
|
1385
|
+
? initializeSftpDataSource(activation, log)
|
|
1386
|
+
: initializeS3DataSource(activation, log);
|
|
1387
|
+
|
|
1388
|
+
const csvParser = new CSVParserService();
|
|
1389
|
+
const mapper = new UniversalMapper(cycleCountMapping);
|
|
1390
|
+
const fileTracker = new VersoriFileTracker(kv, 'cycle-count-recon');
|
|
1391
|
+
|
|
1392
|
+
// Initialize services
|
|
1393
|
+
const inventoryQuery = new InventoryQueryService(fluentClient);
|
|
1394
|
+
const percentThreshold = parseFloat(activation.getVariable('variancePercentThreshold') || '5');
|
|
1395
|
+
const absoluteThreshold = parseFloat(activation.getVariable('varianceAbsoluteThreshold') || '10');
|
|
1396
|
+
const varianceCalculator = new VarianceCalculatorService(percentThreshold, absoluteThreshold);
|
|
1397
|
+
// ✅ PRODUCTION ENHANCEMENT: Pass log to BatchProcessorService for detailed progress tracking
|
|
1398
|
+
const batchProcessor = new BatchProcessorService(fluentClient, jobTracker, log);
|
|
1399
|
+
const reportGenerator = new ReportGeneratorService();
|
|
1400
|
+
|
|
1401
|
+
const results = {
|
|
1402
|
+
filesProcessed: 0,
|
|
1403
|
+
filesSkipped: 0,
|
|
1404
|
+
totalItemsCounted: 0,
|
|
1405
|
+
totalVariances: 0,
|
|
1406
|
+
autoAdjusted: 0,
|
|
1407
|
+
manualReview: 0,
|
|
1408
|
+
errors: [] as any[],
|
|
1409
|
+
reports: [] as any[],
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
try {
|
|
1413
|
+
// Discover files
|
|
1414
|
+
await jobTracker.updateJob(jobId, { stage: 'discovering-files' });
|
|
1415
|
+
log.info('Discovering cycle count files');
|
|
1416
|
+
|
|
1417
|
+
const files =
|
|
1418
|
+
dataSourceType === 'sftp'
|
|
1419
|
+
? await dataSource.listFiles({
|
|
1420
|
+
remotePath: activation.getVariable('sftpIncomingPath'),
|
|
1421
|
+
filePattern: activation.getVariable('filePattern'),
|
|
1422
|
+
})
|
|
1423
|
+
: await dataSource.listFiles({
|
|
1424
|
+
prefix: activation.getVariable('s3Prefix'),
|
|
1425
|
+
pattern: activation.getVariable('filePattern'),
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
log.info('Found files', { count: files.length });
|
|
1429
|
+
|
|
1430
|
+
// Filter processed files (unless force reprocess)
|
|
1431
|
+
let filesToProcess = files;
|
|
1432
|
+
if (!options.forceReprocess) {
|
|
1433
|
+
const processedChecks = await Promise.all(
|
|
1434
|
+
files.map(async (file: FileMetadata) => ({
|
|
1435
|
+
file,
|
|
1436
|
+
processed: await fileTracker.wasFileProcessed(file.name),
|
|
1437
|
+
}))
|
|
1438
|
+
);
|
|
1439
|
+
|
|
1440
|
+
filesToProcess = processedChecks
|
|
1441
|
+
.filter(check => !check.processed)
|
|
1442
|
+
.map(check => check.file);
|
|
1443
|
+
|
|
1444
|
+
results.filesSkipped = files.length - filesToProcess.length;
|
|
1445
|
+
log.info('Filtered processed files', {
|
|
1446
|
+
total: files.length,
|
|
1447
|
+
toProcess: filesToProcess.length,
|
|
1448
|
+
skipped: results.filesSkipped,
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// Limit files per run
|
|
1453
|
+
const maxFiles = parseInt(activation.getVariable('maxFilesPerRun') || '5', 10);
|
|
1454
|
+
filesToProcess = filesToProcess.slice(0, maxFiles);
|
|
1455
|
+
|
|
1456
|
+
// Process each file
|
|
1457
|
+
for (const file of filesToProcess) {
|
|
1458
|
+
await jobTracker.updateJob(jobId, { stage: 'processing-file', details: { fileName: file.name } });
|
|
1459
|
+
|
|
1460
|
+
try {
|
|
1461
|
+
log.info('Processing file', { fileName: file.name });
|
|
1462
|
+
|
|
1463
|
+
// Download file
|
|
1464
|
+
await jobTracker.updateJob(jobId, { stage: 'downloading-file', details: { fileName: file.name } });
|
|
1465
|
+
const fileContent =
|
|
1466
|
+
dataSourceType === 'sftp'
|
|
1467
|
+
? await dataSource.downloadFile(file.path)
|
|
1468
|
+
: await dataSource.readFile(file.key);
|
|
1469
|
+
|
|
1470
|
+
// Parse CSV
|
|
1471
|
+
await jobTracker.updateJob(jobId, { stage: 'parsing-csv', details: { fileName: file.name } });
|
|
1472
|
+
const records = await csvParser.parse(fileContent);
|
|
1473
|
+
log.info('Parsed CSV', {
|
|
1474
|
+
fileName: file.name,
|
|
1475
|
+
recordCount: records.length,
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
// Transform records
|
|
1479
|
+
await jobTracker.updateJob(jobId, { stage: 'transforming-records', details: { fileName: file.name } });
|
|
1480
|
+
const mapped = await mapper.mapBatch(records);
|
|
1481
|
+
const cycleCounts = mapped.data as CycleCountRecord[];
|
|
1482
|
+
const mappingErrors = mapped.errors;
|
|
1483
|
+
|
|
1484
|
+
log.info('Transformed records', {
|
|
1485
|
+
fileName: file.name,
|
|
1486
|
+
successCount: cycleCounts.length,
|
|
1487
|
+
errorCount: mappingErrors.length,
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
// Query current Fluent inventory (bulk query)
|
|
1491
|
+
await jobTracker.updateJob(jobId, { stage: 'querying-inventory', details: { fileName: file.name } });
|
|
1492
|
+
const inventoryMap = await inventoryQuery.queryCurrentInventory(cycleCounts);
|
|
1493
|
+
|
|
1494
|
+
// Calculate variances
|
|
1495
|
+
await jobTracker.updateJob(jobId, { stage: 'calculating-variances', details: { fileName: file.name } });
|
|
1496
|
+
const variances = varianceCalculator.calculateVariances(cycleCounts, inventoryMap);
|
|
1497
|
+
|
|
1498
|
+
log.info('Calculated variances', {
|
|
1499
|
+
fileName: file.name,
|
|
1500
|
+
totalVariances: variances.length,
|
|
1501
|
+
autoAdjust: variances.filter(v => v.action === 'AUTO_ADJUST').length,
|
|
1502
|
+
manualReview: variances.filter(v => v.action === 'MANUAL_REVIEW').length,
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
// Send adjustments via Batch API
|
|
1506
|
+
let batchResult;
|
|
1507
|
+
if (
|
|
1508
|
+
activation.getVariable('enableAutoAdjustment') === 'true' &&
|
|
1509
|
+
variances.some(v => v.action === 'AUTO_ADJUST')
|
|
1510
|
+
) {
|
|
1511
|
+
await jobTracker.updateJob(jobId, { stage: 'sending-batch', details: { fileName: file.name } });
|
|
1512
|
+
const retailerId = activation.getVariable('retailerId');
|
|
1513
|
+
const autoAdjustments = variances.filter(v => v.action === 'AUTO_ADJUST');
|
|
1514
|
+
|
|
1515
|
+
// ? Enhanced: Extract context for progress logging
|
|
1516
|
+
const uniqueLocations = [...new Set(autoAdjustments.map((v: any) => v.locationRef))];
|
|
1517
|
+
const sampleSKUs = autoAdjustments.slice(0, 5).map((v: any) => v.skuRef);
|
|
1518
|
+
|
|
1519
|
+
// ? Enhanced: Start logging with context
|
|
1520
|
+
log.info(`[BatchProcessor] Sending cycle count adjustments for file "${file.name}"`, {
|
|
1521
|
+
totalAdjustments: autoAdjustments.length,
|
|
1522
|
+
locations: uniqueLocations.join(', '),
|
|
1523
|
+
sampleSKUs: sampleSKUs.join(', '),
|
|
1524
|
+
fileName: file.name
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
batchResult = await batchProcessor.sendAdjustmentBatches(
|
|
1528
|
+
autoAdjustments,
|
|
1529
|
+
file.name,
|
|
1530
|
+
retailerId
|
|
1531
|
+
);
|
|
1532
|
+
|
|
1533
|
+
// ✅ Logging handled in workflow with Versori native log
|
|
1534
|
+
log.info('[BatchProcessor] Sent adjustment batches', {
|
|
1535
|
+
file: file.name,
|
|
1536
|
+
jobId: batchResult.jobId,
|
|
1537
|
+
successCount: batchResult.successCount,
|
|
1538
|
+
failureCount: batchResult.failureCount,
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
// ? Enhanced: Completion logging
|
|
1542
|
+
log.info(`[BatchProcessor] Cycle count adjustments sent successfully for file "${file.name}"`, {
|
|
1543
|
+
totalAdjustments: autoAdjustments.length,
|
|
1544
|
+
jobId: batchResult.jobId,
|
|
1545
|
+
successCount: batchResult.successCount
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Generate reconciliation report
|
|
1550
|
+
await jobTracker.updateJob(jobId, { stage: 'generating-report', details: { fileName: file.name } });
|
|
1551
|
+
const report = reportGenerator.generateReconciliationReport(
|
|
1552
|
+
file.name,
|
|
1553
|
+
cycleCounts,
|
|
1554
|
+
mappingErrors,
|
|
1555
|
+
variances,
|
|
1556
|
+
batchResult,
|
|
1557
|
+
percentThreshold,
|
|
1558
|
+
absoluteThreshold
|
|
1559
|
+
);
|
|
1560
|
+
|
|
1561
|
+
results.reports.push(report);
|
|
1562
|
+
results.filesProcessed++;
|
|
1563
|
+
results.totalItemsCounted += cycleCounts.length;
|
|
1564
|
+
results.totalVariances += variances.length;
|
|
1565
|
+
results.autoAdjusted += report.summary.autoAdjustmentsCreated;
|
|
1566
|
+
results.manualReview += report.summary.manualReviewRequired;
|
|
1567
|
+
|
|
1568
|
+
// Mark file as processed
|
|
1569
|
+
await fileTracker.markFileProcessed(file.name, {
|
|
1570
|
+
recordCount: records.length,
|
|
1571
|
+
varianceCount: variances.length,
|
|
1572
|
+
adjustmentCount: report.summary.autoAdjustmentsCreated,
|
|
1573
|
+
manualReviewCount: report.summary.manualReviewRequired,
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
// Archive file
|
|
1577
|
+
if (activation.getVariable('enableArchival') === 'true') {
|
|
1578
|
+
await jobTracker.updateJob(jobId, { stage: 'archiving-file', details: { fileName: file.name } });
|
|
1579
|
+
await archiveFile(dataSource, file, dataSourceType, activation, log);
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
log.info('File processing completed', {
|
|
1583
|
+
fileName: file.name,
|
|
1584
|
+
report: report.summary,
|
|
1585
|
+
});
|
|
1586
|
+
} catch (error: any) {
|
|
1587
|
+
// ✅ Enhanced error logging: Extract all error details for visibility
|
|
1588
|
+
const errorDetails = {
|
|
1589
|
+
message: error?.message || 'Unknown error',
|
|
1590
|
+
stack: error?.stack,
|
|
1591
|
+
fileName: error?.fileName,
|
|
1592
|
+
lineNumber: error?.lineNumber,
|
|
1593
|
+
originalError: error?.context?.originalError?.message,
|
|
1594
|
+
errorType: error?.name || 'Error',
|
|
1595
|
+
};
|
|
1596
|
+
log.error('File processing failed', errorDetails, { fileName: file.name });
|
|
1597
|
+
results.errors.push({
|
|
1598
|
+
fileName: file.name,
|
|
1599
|
+
error: error.message,
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
await jobTracker.markCompleted(jobId, results);
|
|
1605
|
+
return { success: true, jobId, ...results };
|
|
1606
|
+
} catch (error: any) {
|
|
1607
|
+
// ✅ Enhanced error logging: Extract all error details for visibility
|
|
1608
|
+
const errorDetails = {
|
|
1609
|
+
message: error?.message || 'Unknown error',
|
|
1610
|
+
stack: error?.stack,
|
|
1611
|
+
fileName: error?.fileName,
|
|
1612
|
+
lineNumber: error?.lineNumber,
|
|
1613
|
+
originalError: error?.context?.originalError?.message,
|
|
1614
|
+
errorType: error?.name || 'Error',
|
|
1615
|
+
};
|
|
1616
|
+
log.error('[CycleCountRecon] Fatal error:', errorDetails);
|
|
1617
|
+
await jobTracker.markFailed(jobId, error);
|
|
1618
|
+
return { success: false, jobId, error: error.message };
|
|
1619
|
+
} finally {
|
|
1620
|
+
// Cleanup data source
|
|
1621
|
+
if (typeof dataSource.dispose === 'function') {
|
|
1622
|
+
await dataSource.dispose();
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* Query job status
|
|
1629
|
+
*/
|
|
1630
|
+
export async function getReconciliationStatus(ctx: any, jobId?: string) {
|
|
1631
|
+
const { log, openKv } = ctx;
|
|
1632
|
+
if (!jobId) {
|
|
1633
|
+
return { success: false, error: 'jobId required' };
|
|
1634
|
+
}
|
|
1635
|
+
const tracker = new JobTracker(openKv(':project:'), log);
|
|
1636
|
+
const job = await tracker.getJob(jobId);
|
|
1637
|
+
return job ? { success: true, jobId, ...job } : { success: false, jobId, error: 'Job not found' };
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* Archive file helper
|
|
1642
|
+
*/
|
|
1643
|
+
async function archiveFile(dataSource: any, file: any, dataSourceType: string, activation: any, log: any): Promise<void> {
|
|
1644
|
+
try {
|
|
1645
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1646
|
+
const archiveName = `${file.name.replace(/\.[^/.]+$/, '')}-${timestamp}${file.name.match(/\.[^/.]+$/)?.[0] || ''}`;
|
|
1647
|
+
|
|
1648
|
+
if (dataSourceType === 'sftp') {
|
|
1649
|
+
// SFTP: Move file
|
|
1650
|
+
const archivePath = `${activation.getVariable('sftpProcessedPath')}/${archiveName}`;
|
|
1651
|
+
await dataSource.moveFile(file.path, archivePath);
|
|
1652
|
+
log.info('Moved file to processed folder', {
|
|
1653
|
+
from: file.path,
|
|
1654
|
+
to: archivePath,
|
|
1655
|
+
});
|
|
1656
|
+
} else {
|
|
1657
|
+
// S3: Move file
|
|
1658
|
+
const archiveKey = `${activation.getVariable('archivePrefix')}${archiveName}`;
|
|
1659
|
+
await dataSource.moveFile(file.key, archiveKey);
|
|
1660
|
+
log.info('Archived file to S3', {
|
|
1661
|
+
from: file.key,
|
|
1662
|
+
to: archiveKey,
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
} catch (error: any) {
|
|
1666
|
+
// ✅ Enhanced error logging: Extract all error details for visibility
|
|
1667
|
+
const errorDetails = {
|
|
1668
|
+
message: error?.message || 'Unknown error',
|
|
1669
|
+
stack: error?.stack,
|
|
1670
|
+
fileName: file.name,
|
|
1671
|
+
errorType: error?.name || 'Error',
|
|
1672
|
+
};
|
|
1673
|
+
log.error('Failed to archive file', errorDetails);
|
|
1674
|
+
// Don't throw - archival is optional
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
```
|
|
1678
|
+
|
|
1679
|
+
---
|
|
1680
|
+
|
|
1681
|
+
### File: `src/config/cycle-count.mapping.json`
|
|
1682
|
+
|
|
1683
|
+
```json
|
|
1684
|
+
{
|
|
1685
|
+
"name": "cycle-count.mapping",
|
|
1686
|
+
"version": "1.0.0",
|
|
1687
|
+
"description": "CSV cycle count to reconciliation record mapping",
|
|
1688
|
+
"fields": {
|
|
1689
|
+
"locationRef": { "source": "location_code", "required": true, "resolver": "sdk.trim" },
|
|
1690
|
+
"skuRef": { "source": "sku", "required": true, "resolver": "sdk.trim" },
|
|
1691
|
+
"countedQty": { "source": "counted_quantity", "required": true, "resolver": "sdk.parseInt" },
|
|
1692
|
+
"countDate": { "source": "count_date", "required": false },
|
|
1693
|
+
"counterName": { "source": "counter_username", "required": false },
|
|
1694
|
+
"binLocation": { "source": "bin_location", "required": false },
|
|
1695
|
+
"lotNumber": { "source": "lot_number", "required": false }
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
```
|
|
1699
|
+
|
|
1700
|
+
---
|
|
1701
|
+
|
|
1702
|
+
### Benefits of This Modular Approach
|
|
1703
|
+
|
|
1704
|
+
1. **Separation of Concerns**: Each service has one clear responsibility
|
|
1705
|
+
2. **Reusability**: Services can be imported and used in other workflows
|
|
1706
|
+
3. **Testability**: Easy to write unit tests for individual services
|
|
1707
|
+
4. **Maintainability**: Changes isolated to specific service files
|
|
1708
|
+
5. **Type Safety**: Centralized type definitions prevent duplication
|
|
1709
|
+
6. **Scalability**: Easy to add new services without modifying existing code
|
|
1710
|
+
7. **Follows Gold Standard**: Matches pattern from template-ingestion-sftp-xml-inventory-batch.md
|
|
1711
|
+
|
|
1712
|
+
---
|
|
1713
|
+
|
|
1714
|
+
## Testing
|
|
1715
|
+
|
|
1716
|
+
- Upload a small CSV to S3/SFTP (2-3 cycle count rows) with known variances
|
|
1717
|
+
- Trigger `cycle-count-recon-adhoc` webhook with valid API key
|
|
1718
|
+
- Verify variances calculated correctly (auto-adjust vs manual review)
|
|
1719
|
+
- Confirm Batch job created and adjustments sent for auto-adjust items
|
|
1720
|
+
- Check file moved from `incoming/` to `processed/` folder
|
|
1721
|
+
- Verify reconciliation report generated with correct summary
|
|
1722
|
+
- Query job status via `cycle-count-recon-job-status` webhook
|
|
1723
|
+
- Check KV state: file tracking and job metadata
|
|
1724
|
+
|
|
1725
|
+
---
|
|
1726
|
+
|
|
1727
|
+
## Monitoring
|
|
1728
|
+
|
|
1729
|
+
### Success Response
|
|
1730
|
+
|
|
1731
|
+
```json
|
|
1732
|
+
{
|
|
1733
|
+
"success": true,
|
|
1734
|
+
"filesProcessed": 1,
|
|
1735
|
+
"filesSkipped": 0,
|
|
1736
|
+
"filesFailed": 0,
|
|
1737
|
+
"results": [
|
|
1738
|
+
{
|
|
1739
|
+
"file": "cycle-counts_2025-01-22.csv",
|
|
1740
|
+
"success": true,
|
|
1741
|
+
"recordCount": 50,
|
|
1742
|
+
"batchCount": 2,
|
|
1743
|
+
"jobId": "job-123456",
|
|
1744
|
+
"adjustmentsCreated": 25,
|
|
1745
|
+
"adjustmentsRequiringReview": 5,
|
|
1746
|
+
"duration": 12345
|
|
1747
|
+
}
|
|
1748
|
+
],
|
|
1749
|
+
"duration": 13456
|
|
1750
|
+
}
|
|
1751
|
+
```
|
|
1752
|
+
|
|
1753
|
+
### Partial Success Response
|
|
1754
|
+
|
|
1755
|
+
```json
|
|
1756
|
+
{
|
|
1757
|
+
"success": true,
|
|
1758
|
+
"filesProcessed": 1,
|
|
1759
|
+
"filesSkipped": 0,
|
|
1760
|
+
"filesFailed": 0,
|
|
1761
|
+
"results": [
|
|
1762
|
+
{
|
|
1763
|
+
"file": "cycle-counts_2025-01-22.csv",
|
|
1764
|
+
"success": true,
|
|
1765
|
+
"recordCount": 50,
|
|
1766
|
+
"batchCount": 2,
|
|
1767
|
+
"jobId": "job-123456",
|
|
1768
|
+
"adjustmentsCreated": 45,
|
|
1769
|
+
"adjustmentsRequiringReview": 5,
|
|
1770
|
+
"errors": ["SKU-001: Inventory not found", "SKU-002: Invalid variance calculation"]
|
|
1771
|
+
}
|
|
1772
|
+
],
|
|
1773
|
+
"duration": 13456
|
|
1774
|
+
}
|
|
1775
|
+
```
|
|
1776
|
+
|
|
1777
|
+
### Error Response
|
|
1778
|
+
|
|
1779
|
+
```json
|
|
1780
|
+
{
|
|
1781
|
+
"success": false,
|
|
1782
|
+
"filesProcessed": 0,
|
|
1783
|
+
"filesFailed": 1,
|
|
1784
|
+
"results": [
|
|
1785
|
+
{
|
|
1786
|
+
"file": "cycle-counts_2025-01-22.csv",
|
|
1787
|
+
"success": false,
|
|
1788
|
+
"error": "CSV parse error: Invalid structure"
|
|
1789
|
+
}
|
|
1790
|
+
],
|
|
1791
|
+
"duration": 876
|
|
1792
|
+
}
|
|
1793
|
+
```
|
|
1794
|
+
|
|
1795
|
+
### Monitoring Metrics
|
|
1796
|
+
|
|
1797
|
+
Monitor these metrics via Versori logs filtered by `jobId` or `stage`:
|
|
1798
|
+
|
|
1799
|
+
- **Files Processed** - Total files successfully processed
|
|
1800
|
+
- **Batch Jobs Created** - Total Batch jobs created in Fluent Commerce
|
|
1801
|
+
- **Adjustments Created** - Total inventory adjustments sent
|
|
1802
|
+
- **Adjustments Requiring Review** - Adjustments flagged for manual review
|
|
1803
|
+
- **Processing Duration** - Time taken for complete workflow
|
|
1804
|
+
- **Variance Thresholds** - Monitor variance calculation accuracy
|
|
1805
|
+
|
|
1806
|
+
Use the status webhook for dashboards and automated monitoring.
|
|
1807
|
+
|
|
1808
|
+
---
|
|
1809
|
+
|
|
1810
|
+
- **All variances flagged for manual review** - Thresholds too strict; increase `variancePercentThreshold` and `varianceAbsoluteThreshold`.
|
|
1811
|
+
- **Percentage variance always 100%** - Fluent inventory not found (divide by zero); verify inventory exists before cycle count.
|
|
1812
|
+
- **Batch API timeout** - Too many adjustments in single batch; chunk adjustments into smaller batches of 500.
|
|
1813
|
+
- **Duplicate adjustments** - File tracking not working; verify VersoriFileTracker uses correct KV scope (`:project:`).
|
|
1814
|
+
- **S3/SFTP access denied** - Validate IAM permissions or SFTP credentials; ensure bucket/host/paths are correct.
|
|
1815
|
+
- **CSV header mismatch** - Trim headers and verify mapping sources match exact column names.
|
|
1816
|
+
|
|
1817
|
+
### Required IAM Permissions (S3)
|
|
1818
|
+
|
|
1819
|
+
```json
|
|
1820
|
+
{
|
|
1821
|
+
"Version": "2012-10-17",
|
|
1822
|
+
"Statement": [
|
|
1823
|
+
{
|
|
1824
|
+
"Effect": "Allow",
|
|
1825
|
+
"Action": ["s3:ListBucket", "s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
|
|
1826
|
+
"Resource": [
|
|
1827
|
+
"arn:aws:s3:::wms-cycle-counts",
|
|
1828
|
+
"arn:aws:s3:::wms-cycle-counts/*"
|
|
1829
|
+
]
|
|
1830
|
+
}
|
|
1831
|
+
]
|
|
1832
|
+
}
|
|
1833
|
+
```
|
|
1834
|
+
|
|
1835
|
+
## Production Checklist
|
|
1836
|
+
|
|
1837
|
+
- Activation secrets stored securely; no secrets in code
|
|
1838
|
+
- AWS credentials validated (S3) or SFTP credentials tested
|
|
1839
|
+
- Archive/error paths writable and monitored
|
|
1840
|
+
- Variance thresholds tuned for your business requirements
|
|
1841
|
+
- Logging/metrics and alerting configured for failed reconciliations
|
|
1842
|
+
- Manual review notification system configured
|
|
1843
|
+
- Webhook API key security verified
|
|
1844
|
+
- Clear runbook for failures and retries
|
|
1845
|
+
- Reconciliation report storage/retention policy defined
|
|
1846
|
+
- File naming/ordering strategy documented
|
|
1847
|
+
- BPP (Batch Pre-Processing) behavior tested
|
|
1848
|
+
|
|
1849
|
+
## Related Guides
|
|
1850
|
+
|
|
1851
|
+
- State & KV patterns: `docs/use-cases/versori/03-kv-state-management.md`
|
|
1852
|
+
- Error handling & retry: `docs/use-cases/patterns/error-handling-retry.md`
|
|
1853
|
+
- Universal Mapping: `fc-connect-sdk/docs/guides/mapping/readme.md`
|
|
1854
|
+
- Batch ingestion: `fc-connect-sdk/docs/guides/ingestion.md`
|
|
1855
|
+
- JobTracker: `fc-connect-sdk/docs/guides/job-tracker.md`
|
|
1856
|
+
|
|
1857
|
+
## Advanced Patterns
|
|
1858
|
+
|
|
1859
|
+
### Pattern 1: Dual Threshold Logic (Percentage AND Absolute)
|
|
1860
|
+
|
|
1861
|
+
**Both thresholds must be met for auto-adjustment:**
|
|
1862
|
+
|
|
1863
|
+
```typescript
|
|
1864
|
+
const percentThreshold = 5; // 5%
|
|
1865
|
+
const absoluteThreshold = 10; // 10 units
|
|
1866
|
+
|
|
1867
|
+
const isWithinPercentThreshold = variancePercent < percentThreshold;
|
|
1868
|
+
const isWithinAbsoluteThreshold = Math.abs(varianceQty) < absoluteThreshold;
|
|
1869
|
+
const isWithinThreshold = isWithinPercentThreshold && isWithinAbsoluteThreshold;
|
|
1870
|
+
|
|
1871
|
+
const action = isWithinThreshold ? 'AUTO_ADJUST' : 'MANUAL_REVIEW';
|
|
1872
|
+
```
|
|
1873
|
+
|
|
1874
|
+
**Why dual thresholds?** Prevents incorrect auto-adjustments:
|
|
1875
|
+
|
|
1876
|
+
| Fluent Qty | Counted | Variance Qty | Variance % | Auto-Adjust? | Reason |
|
|
1877
|
+
| ---------- | ------- | ------------ | ---------- | ------------ | ------------------------- |
|
|
1878
|
+
| 100 | 103 | +3 | 3% | Yes | Both thresholds met |
|
|
1879
|
+
| 100 | 115 | +15 | 15% | No | Exceeds 10 unit threshold |
|
|
1880
|
+
| 10 | 11 | +1 | 10% | No | Exceeds 5% threshold |
|
|
1881
|
+
| 1000 | 1008 | +8 | 0.8% | Yes | Both thresholds met |
|
|
1882
|
+
| 5 | 0 | -5 | 100% | No | Exceeds both thresholds |
|
|
1883
|
+
|
|
1884
|
+
### Pattern 2: Variance Calculation with Edge Cases
|
|
1885
|
+
|
|
1886
|
+
**Handle divide-by-zero and edge cases:**
|
|
1887
|
+
|
|
1888
|
+
```typescript
|
|
1889
|
+
const fluentQty = fluentInventory?.qty || 0;
|
|
1890
|
+
const countedQty = cycleCount.countedQty;
|
|
1891
|
+
const varianceQty = countedQty - fluentQty;
|
|
1892
|
+
|
|
1893
|
+
// Calculate percentage variance (handle divide-by-zero)
|
|
1894
|
+
const variancePercent =
|
|
1895
|
+
fluentQty === 0
|
|
1896
|
+
? countedQty === 0
|
|
1897
|
+
? 0 // 0 → 0 = no variance
|
|
1898
|
+
: 100 // 0 → N = 100% variance
|
|
1899
|
+
: Math.abs((varianceQty / fluentQty) * 100);
|
|
1900
|
+
```
|
|
1901
|
+
|
|
1902
|
+
**Edge cases handled:**
|
|
1903
|
+
|
|
1904
|
+
- **Zero to zero**: No variance (0%)
|
|
1905
|
+
- **Zero to positive**: 100% variance (requires review)
|
|
1906
|
+
- **Negative variance**: Counted less than system (shrinkage)
|
|
1907
|
+
- **Positive variance**: Counted more than system (found inventory)
|
|
1908
|
+
|
|
1909
|
+
### Pattern 3: Bulk Inventory Query (Single Query, O(1) Lookup)
|
|
1910
|
+
|
|
1911
|
+
**Single GraphQL query for all items:**
|
|
1912
|
+
|
|
1913
|
+
```typescript
|
|
1914
|
+
// Extract unique combinations
|
|
1915
|
+
const uniqueLocations = [...new Set(cycleCounts.map(cc => cc.locationRef))];
|
|
1916
|
+
const uniqueSkus = [...new Set(cycleCounts.map(cc => cc.skuRef))];
|
|
1917
|
+
|
|
1918
|
+
// Query with auto-pagination
|
|
1919
|
+
const result = await client.graphql({
|
|
1920
|
+
query: inventoryQuery,
|
|
1921
|
+
variables: { first: 100 },
|
|
1922
|
+
pagination: { maxPages: 100 },
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
// Build lookup map (O(1) access)
|
|
1926
|
+
const inventoryMap = new Map<string, FluentInventory>();
|
|
1927
|
+
for (const edge of result.data?.inventoryQuantities?.edges || []) {
|
|
1928
|
+
const node = edge.node;
|
|
1929
|
+
const key = `${node.locationRef}:${node.skuRef}`;
|
|
1930
|
+
inventoryMap.set(key, node);
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// Fast lookup during reconciliation
|
|
1934
|
+
for (const cycleCount of cycleCounts) {
|
|
1935
|
+
const key = `${cycleCount.locationRef}:${cycleCount.skuRef}`;
|
|
1936
|
+
const fluentInventory = inventoryMap.get(key); // O(1)
|
|
1937
|
+
}
|
|
1938
|
+
```
|
|
1939
|
+
|
|
1940
|
+
**Performance comparison:**
|
|
1941
|
+
|
|
1942
|
+
- Bad (100 items): 100 GraphQL queries = ~10-20 seconds
|
|
1943
|
+
- Good (100 items): 1 GraphQL query + Map lookup = ~1-2 seconds
|
|
1944
|
+
|
|
1945
|
+
### Pattern 4: Batch API with Full Audit Trail
|
|
1946
|
+
|
|
1947
|
+
**Include complete audit trail in attributes:**
|
|
1948
|
+
|
|
1949
|
+
```typescript
|
|
1950
|
+
const entities = adjustments.map(item => ({
|
|
1951
|
+
ref: `${item.locationRef}:${item.skuRef}`,
|
|
1952
|
+
locationRef: item.locationRef,
|
|
1953
|
+
skuRef: item.skuRef,
|
|
1954
|
+
qty: item.countedQty, // NEW absolute quantity (not delta)
|
|
1955
|
+
type: 'CORRECTION',
|
|
1956
|
+
status: 'AVAILABLE',
|
|
1957
|
+
attributes: [
|
|
1958
|
+
{ name: 'cycle_count_adjustment', type: 'STRING', value: 'true' },
|
|
1959
|
+
{ name: 'original_qty', type: 'INTEGER', value: String(item.fluentQty) },
|
|
1960
|
+
{ name: 'variance_qty', type: 'INTEGER', value: String(item.varianceQty) },
|
|
1961
|
+
{ name: 'count_date', type: 'STRING', value: item.countDate || new Date().toISOString() },
|
|
1962
|
+
{ name: 'counter_name', type: 'STRING', value: item.counterName || 'WMS System' },
|
|
1963
|
+
{ name: 'source_file', type: 'STRING', value: fileName },
|
|
1964
|
+
],
|
|
1965
|
+
}));
|
|
1966
|
+
```
|
|
1967
|
+
|
|
1968
|
+
**CRITICAL:** `qty` is the **new absolute quantity**, NOT the delta!
|
|
1969
|
+
|
|
1970
|
+
```typescript
|
|
1971
|
+
// WRONG - This would set qty to variance
|
|
1972
|
+
qty: item.varianceQty;
|
|
1973
|
+
|
|
1974
|
+
// CORRECT - This sets qty to actual counted quantity
|
|
1975
|
+
qty: item.countedQty;
|
|
1976
|
+
```
|
|
1977
|
+
|
|
1978
|
+
### Pattern 5: Reconciliation Report Structure
|
|
1979
|
+
|
|
1980
|
+
**Structured report for variance analysis:**
|
|
1981
|
+
|
|
1982
|
+
```typescript
|
|
1983
|
+
return {
|
|
1984
|
+
fileName,
|
|
1985
|
+
processedAt: new Date().toISOString(),
|
|
1986
|
+
summary: {
|
|
1987
|
+
totalItemsCounted: cycleCounts.length,
|
|
1988
|
+
mappingErrors: mappingErrors.length,
|
|
1989
|
+
itemsWithNoVariance: cycleCounts.length - variances.length,
|
|
1990
|
+
itemsWithVariance: variances.length,
|
|
1991
|
+
autoAdjustmentsCreated: batchResult?.successCount || 0,
|
|
1992
|
+
autoAdjustmentsFailed: batchResult?.failureCount || 0,
|
|
1993
|
+
manualReviewRequired: manualReview.length,
|
|
1994
|
+
},
|
|
1995
|
+
thresholds: {
|
|
1996
|
+
percentThreshold: parseFloat(activation.getVariable('variancePercentThreshold')),
|
|
1997
|
+
absoluteThreshold: parseFloat(activation.getVariable('varianceAbsoluteThreshold')),
|
|
1998
|
+
},
|
|
1999
|
+
variances: {
|
|
2000
|
+
autoAdjusted: [...],
|
|
2001
|
+
manualReview: [...],
|
|
2002
|
+
},
|
|
2003
|
+
};
|
|
2004
|
+
```
|
|
2005
|
+
|
|
2006
|
+
**Report usage:**
|
|
2007
|
+
|
|
2008
|
+
1. Operations dashboard - Display summary metrics
|
|
2009
|
+
2. Email notifications - Alert on large variances
|
|
2010
|
+
3. Audit trail - Store in S3/KV for compliance
|
|
2011
|
+
4. Manual review queue - Feed to approval UI
|
|
2012
|
+
|
|
2013
|
+
---
|
|
2014
|
+
|
|
2015
|
+
**End of Template**
|
|
2016
|
+
|
|
2017
|
+
## Related Templates
|
|
2018
|
+
|
|
2019
|
+
**For standard inventory batch ingestion** (without reconciliation logic), see:
|
|
2020
|
+
- [SFTP XML Inventory Batch Template](./template-ingestion-sftp-xml-inventory-batch.md) - Simpler workflow without variance calculation
|
|
2021
|
+
|
|
2022
|
+
**Key Differences:**
|
|
2023
|
+
- This template adds cycle count reconciliation with dual threshold variance detection
|
|
2024
|
+
- Standard template is for direct inventory ingestion without comparison logic
|
|
2025
|
+
- Use this template when comparing physical counts against system inventory
|
|
2026
|
+
- Use standard template for regular inventory file uploads
|
|
2027
|
+
|
|
2028
|
+
---
|
|
2029
|
+
|
|
2030
|
+
[← Back to Batch API Templates](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) | [Ingestion Templates Index →](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)
|