@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
package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md
CHANGED
|
@@ -1,2226 +1,2226 @@
|
|
|
1
|
-
# Versori Pre-Order Allocation Management
|
|
2
|
-
|
|
3
|
-
**FC Connect SDK Use Case Guide**
|
|
4
|
-
|
|
5
|
-
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
6
|
-
> **Version**: Use latest - `npm install @fluentcommerce/fc-connect-sdk@latest`
|
|
7
|
-
|
|
8
|
-
**Context**: Real-time pre-order reservation system with future ATP (Available to Promise) calculation and priority queuing
|
|
9
|
-
|
|
10
|
-
**Complexity**: High
|
|
11
|
-
|
|
12
|
-
**Volume**: High during launches (100-10,000 pre-orders)
|
|
13
|
-
|
|
14
|
-
**Latency**: Real-time (< 2 seconds)
|
|
15
|
-
|
|
16
|
-
**Runtime**: Versori Platform (HTTP Webhook + Scheduled)
|
|
17
|
-
|
|
18
|
-
**Pattern**: Webhook + future ATP check + reservation + priority queue + scheduled release
|
|
19
|
-
|
|
20
|
-
**Estimated Lines**: ~1200 lines
|
|
21
|
-
|
|
22
|
-
## What You'll Build
|
|
23
|
-
|
|
24
|
-
- **Pre-Order Webhook**: Receive pre-order requests from e-commerce platforms
|
|
25
|
-
|
|
26
|
-
- **Future ATP Calculation**: Calculate available inventory for future ship dates
|
|
27
|
-
- Query current inventory levels
|
|
28
|
-
- Query existing reservations for target date
|
|
29
|
-
- Calculate net available quantity
|
|
30
|
-
- Apply overbooking strategy (105% of expected inventory)
|
|
31
|
-
|
|
32
|
-
- **Reservation Management**: Create and track reservations in Fluent
|
|
33
|
-
- Store reservation with priority tier (VIP, Standard)
|
|
34
|
-
- Assign queue position within tier
|
|
35
|
-
- Set expiration window (7 days before ship date)
|
|
36
|
-
|
|
37
|
-
- **Priority Queuing**: VIP customers get priority allocation
|
|
38
|
-
- Tier-based scoring (VIP=100, Standard=50)
|
|
39
|
-
- First-come-first-served within tier
|
|
40
|
-
- Queue position tracking via VersoriKV
|
|
41
|
-
|
|
42
|
-
- **Scheduled Release Workflow**: Convert reservations to allocations on ship date
|
|
43
|
-
- Query all reservations for current date
|
|
44
|
-
- Convert to fulfillment orders
|
|
45
|
-
- Handle insufficient inventory (cancel lower priority)
|
|
46
|
-
- Email notifications to customers
|
|
47
|
-
|
|
48
|
-
- **Cancellation Workflow**: Release reservation and offer to next in queue
|
|
49
|
-
|
|
50
|
-
- **Admin Dashboard Export**: Export reservation status to S3
|
|
51
|
-
|
|
52
|
-
## SDK Methods Used
|
|
53
|
-
|
|
54
|
-
- `webhook('name', { response: { mode } })` - HTTP webhook endpoints
|
|
55
|
-
- `schedule('name', 'cron')` - Scheduled workflows
|
|
56
|
-
- `createClient(ctx)` - Create Fluent client
|
|
57
|
-
- `client.graphql({ query, variables })` - Execute GraphQL queries/mutations
|
|
58
|
-
- `VersoriKVAdapter(openKv())` - State management
|
|
59
|
-
- `VersoriFileTracker` - Track processed reservations
|
|
60
|
-
- `GraphQLMutationMapper` - Custom mutations for reservations
|
|
61
|
-
- `UniversalMapper` - Field transformations
|
|
62
|
-
- `XMLBuilder` - Response formatting
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## Versori Workflows Structure
|
|
67
|
-
|
|
68
|
-
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
69
|
-
|
|
70
|
-
**Trigger Types:**
|
|
71
|
-
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
72
|
-
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
73
|
-
- **`http()`** → External API calls (chained from webhook/schedule)
|
|
74
|
-
- **`fn()`** → Internal processing (chained from webhook/schedule)
|
|
75
|
-
|
|
76
|
-
### Recommended Project Structure
|
|
77
|
-
|
|
78
|
-
```
|
|
79
|
-
pre-order-allocation/
|
|
80
|
-
├── index.ts # Entry point - exports all workflows
|
|
81
|
-
└── src/
|
|
82
|
-
├── workflows/
|
|
83
|
-
│ ├── webhook/
|
|
84
|
-
│ │ └── reservation.ts # Webhook: Create reservations
|
|
85
|
-
│ │
|
|
86
|
-
│ └── scheduled/
|
|
87
|
-
│ └── release-reservations.ts # Scheduled: Release reservations
|
|
88
|
-
│
|
|
89
|
-
├── services/
|
|
90
|
-
│ └── reservation.service.ts # Shared orchestration logic (reusable)
|
|
91
|
-
│
|
|
92
|
-
└── config/
|
|
93
|
-
└── reservation-config.json # Configuration
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
**Benefits:**
|
|
97
|
-
- ✅ Clear trigger separation (`webhook/` vs `scheduled/`)
|
|
98
|
-
- ✅ Descriptive file names (easy to browse and understand)
|
|
99
|
-
- ✅ Scalable (add new workflows without cluttering)
|
|
100
|
-
- ✅ Reusable code in `services/` (DRY principle)
|
|
101
|
-
- ✅ Easy to modify individual workflows without affecting others
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
## Complete Working Code
|
|
106
|
-
|
|
107
|
-
### 1. Main Workflow File: `index.ts`
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
/**
|
|
111
|
-
* Pre-Order Allocation Management System
|
|
112
|
-
*
|
|
113
|
-
* Handles high-volume product launches with priority-based reservations
|
|
114
|
-
* and future inventory allocation.
|
|
115
|
-
*
|
|
116
|
-
* Key Features:
|
|
117
|
-
* - Real-time ATP (Available to Promise) calculation for future dates
|
|
118
|
-
* - Priority queuing (VIP vs Standard customers)
|
|
119
|
-
* - Reservation expiration management
|
|
120
|
-
* - Overbooking strategy (105% allocation)
|
|
121
|
-
* - Scheduled release on ship date
|
|
122
|
-
* - Cancellation with queue reallocation
|
|
123
|
-
*/
|
|
124
|
-
|
|
125
|
-
import { webhook, schedule, http, fn } from '@versori/run';
|
|
126
|
-
import {
|
|
127
|
-
createClient,
|
|
128
|
-
VersoriKVAdapter,
|
|
129
|
-
VersoriFileTracker,
|
|
130
|
-
UniversalMapper,
|
|
131
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
132
|
-
|
|
133
|
-
// =============================================================================
|
|
134
|
-
// TYPE DEFINITIONS
|
|
135
|
-
// =============================================================================
|
|
136
|
-
|
|
137
|
-
interface PreOrderRequest {
|
|
138
|
-
orderId: string;
|
|
139
|
-
customerId: string;
|
|
140
|
-
customerEmail: string;
|
|
141
|
-
customerTier: 'VIP' | 'STANDARD'; // Priority tier
|
|
142
|
-
items: PreOrderItem[];
|
|
143
|
-
shipDate: string; // Future ship date (ISO 8601)
|
|
144
|
-
source: string; // E-commerce platform identifier
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
interface PreOrderItem {
|
|
148
|
-
sku: string;
|
|
149
|
-
productName: string;
|
|
150
|
-
quantity: number;
|
|
151
|
-
locationRef: string;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
interface ReservationRecord {
|
|
155
|
-
reservationId: string;
|
|
156
|
-
orderId: string;
|
|
157
|
-
customerId: string;
|
|
158
|
-
customerEmail: string;
|
|
159
|
-
sku: string;
|
|
160
|
-
quantity: number;
|
|
161
|
-
locationRef: string;
|
|
162
|
-
shipDate: string;
|
|
163
|
-
priority: number; // Calculated priority score
|
|
164
|
-
queuePosition: number;
|
|
165
|
-
tier: 'VIP' | 'STANDARD';
|
|
166
|
-
status: 'RESERVED' | 'CONFIRMED' | 'CANCELLED' | 'EXPIRED';
|
|
167
|
-
createdAt: string;
|
|
168
|
-
expiresAt: string; // 7 days before ship date
|
|
169
|
-
confirmedAt?: string;
|
|
170
|
-
cancelledAt?: string;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
interface ATPCalculation {
|
|
174
|
-
sku: string;
|
|
175
|
-
locationRef: string;
|
|
176
|
-
shipDate: string;
|
|
177
|
-
currentInventory: number;
|
|
178
|
-
existingReservations: number;
|
|
179
|
-
expectedArrival: number; // Expected inventory by ship date
|
|
180
|
-
availableToPromise: number;
|
|
181
|
-
overbookingLimit: number; // 105% of expected
|
|
182
|
-
canFulfill: boolean;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// =============================================================================
|
|
186
|
-
// WORKFLOW 1: PRE-ORDER WEBHOOK (Real-time)
|
|
187
|
-
// =============================================================================
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Pre-Order Webhook - Receives pre-order from e-commerce platform
|
|
191
|
-
*
|
|
192
|
-
* Flow:
|
|
193
|
-
* 1. Parse and validate pre-order request
|
|
194
|
-
* 2. Calculate future ATP for each item
|
|
195
|
-
* 3. Create reservation if inventory available
|
|
196
|
-
* 4. Assign priority and queue position
|
|
197
|
-
* 5. Store reservation in VersoriKV
|
|
198
|
-
* 6. Send confirmation response
|
|
199
|
-
*/
|
|
200
|
-
export const preOrderWebhook = webhook('pre-order', {
|
|
201
|
-
response: {
|
|
202
|
-
mode: 'sync',
|
|
203
|
-
// Custom response handler for JSON
|
|
204
|
-
onSuccess: (ctx) => new Response(JSON.stringify(ctx.data), {
|
|
205
|
-
status: 200,
|
|
206
|
-
headers: { 'Content-Type': 'application/json' }
|
|
207
|
-
}),
|
|
208
|
-
onError: (ctx) => new Response(JSON.stringify({
|
|
209
|
-
success: false,
|
|
210
|
-
error: ctx.data instanceof Error ? ctx.data.message : String(ctx.data),
|
|
211
|
-
timestamp: new Date().toISOString()
|
|
212
|
-
}), {
|
|
213
|
-
status: 400,
|
|
214
|
-
headers: { 'Content-Type': 'application/json' }
|
|
215
|
-
})
|
|
216
|
-
},
|
|
217
|
-
cors: true
|
|
218
|
-
})
|
|
219
|
-
// Step 1: Validate pre-order request
|
|
220
|
-
.then(fn('validate-request', (ctx) => {
|
|
221
|
-
const { data, log } = ctx;
|
|
222
|
-
|
|
223
|
-
log.info('🔍 Validating pre-order request');
|
|
224
|
-
const request = data as PreOrderRequest;
|
|
225
|
-
|
|
226
|
-
// Validation checks
|
|
227
|
-
if (!request.orderId || !request.customerId || !request.customerEmail) {
|
|
228
|
-
log.error('[PreOrderAllocation] Validation error: Missing required fields', {
|
|
229
|
-
recommendation: 'Ensure pre-order request includes orderId, customerId, and customerEmail'
|
|
230
|
-
});
|
|
231
|
-
throw new Error('Missing required fields: orderId, customerId, or customerEmail');
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (!request.items || request.items.length === 0) {
|
|
235
|
-
log.error('[PreOrderAllocation] Validation error: No items in pre-order', {
|
|
236
|
-
recommendation: 'Pre-order must contain at least one item'
|
|
237
|
-
});
|
|
238
|
-
throw new Error('Pre-order must contain at least one item');
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (!request.shipDate) {
|
|
242
|
-
log.error('[PreOrderAllocation] Validation error: Missing ship date', {
|
|
243
|
-
recommendation: 'Ship date is required for pre-orders'
|
|
244
|
-
});
|
|
245
|
-
throw new Error('Ship date is required for pre-orders');
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Validate ship date is in the future
|
|
249
|
-
const shipDate = new Date(request.shipDate);
|
|
250
|
-
const now = new Date();
|
|
251
|
-
const daysDiff = Math.floor((shipDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
252
|
-
|
|
253
|
-
if (daysDiff < 7) {
|
|
254
|
-
log.error('[PreOrderAllocation] Validation error: Ship date too soon', {
|
|
255
|
-
daysDiff,
|
|
256
|
-
recommendation: 'Ship date must be at least 7 days in the future for pre-orders'
|
|
257
|
-
});
|
|
258
|
-
throw new Error(`Ship date must be at least 7 days in the future (got ${daysDiff} days)`);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (daysDiff > 180) {
|
|
262
|
-
log.error('[PreOrderAllocation] Validation error: Ship date too far in future', {
|
|
263
|
-
daysDiff,
|
|
264
|
-
recommendation: 'Ship date cannot be more than 180 days in the future'
|
|
265
|
-
});
|
|
266
|
-
throw new Error(`Ship date cannot be more than 180 days in the future (got ${daysDiff} days)`);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
log.info('✅ Pre-order request validated', {
|
|
270
|
-
orderId: request.orderId,
|
|
271
|
-
itemCount: request.items.length,
|
|
272
|
-
shipDate: request.shipDate,
|
|
273
|
-
daysUntilShip: daysDiff,
|
|
274
|
-
tier: request.customerTier || 'STANDARD'
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
return {
|
|
278
|
-
request,
|
|
279
|
-
shipDate,
|
|
280
|
-
daysUntilShip: daysDiff,
|
|
281
|
-
expiresAt: new Date(shipDate.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString() // 7 days before
|
|
282
|
-
};
|
|
283
|
-
}))
|
|
284
|
-
|
|
285
|
-
// Step 2: Calculate future ATP for each item
|
|
286
|
-
.then(http('calculate-atp', {
|
|
287
|
-
connection: 'fluent_commerce'
|
|
288
|
-
}, async (ctx) => {
|
|
289
|
-
const { request, shipDate, expiresAt } = ctx.data;
|
|
290
|
-
const { log } = ctx;
|
|
291
|
-
|
|
292
|
-
log.info('📊 Calculating future ATP for items');
|
|
293
|
-
|
|
294
|
-
const startTime = Date.now();
|
|
295
|
-
const client = await createClient(ctx, { validateConnection: true });
|
|
296
|
-
const atpResults: ATPCalculation[] = [];
|
|
297
|
-
|
|
298
|
-
for (const item of request.items) {
|
|
299
|
-
try {
|
|
300
|
-
// Query current inventory
|
|
301
|
-
const inventoryResult = await client.graphql({
|
|
302
|
-
query: `
|
|
303
|
-
query GetInventory($locationRef: String!, $skuRef: String!) {
|
|
304
|
-
inventoryQuantities(
|
|
305
|
-
first: 1
|
|
306
|
-
locationRef: $locationRef
|
|
307
|
-
skuRef: $skuRef
|
|
308
|
-
type: "LAST_ON_HAND"
|
|
309
|
-
) {
|
|
310
|
-
edges {
|
|
311
|
-
node {
|
|
312
|
-
id
|
|
313
|
-
quantity
|
|
314
|
-
expectedOn
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}`,
|
|
319
|
-
variables: {
|
|
320
|
-
locationRef: item.locationRef,
|
|
321
|
-
skuRef: item.sku
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
const currentInventory = inventoryResult.data?.inventoryQuantities?.edges?.[0]?.node?.quantity || 0;
|
|
326
|
-
|
|
327
|
-
// Query existing reservations for this SKU + ship date
|
|
328
|
-
// NOTE: This assumes reservations are stored as custom attributes
|
|
329
|
-
// In production, you'd query a custom entity type or use attributes
|
|
330
|
-
const reservationsResult = await client.graphql({
|
|
331
|
-
query: `
|
|
332
|
-
query GetReservations($sku: String!, $shipDate: String!) {
|
|
333
|
-
# This is a placeholder - implement based on your reservation storage
|
|
334
|
-
# Option 1: Custom entity type
|
|
335
|
-
# Option 2: Inventory attributes
|
|
336
|
-
# Option 3: External database
|
|
337
|
-
inventoryQuantities(
|
|
338
|
-
first: 100
|
|
339
|
-
skuRef: $sku
|
|
340
|
-
status: "RESERVED"
|
|
341
|
-
) {
|
|
342
|
-
edges {
|
|
343
|
-
node {
|
|
344
|
-
id
|
|
345
|
-
quantity
|
|
346
|
-
attributes {
|
|
347
|
-
name
|
|
348
|
-
value
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
`,
|
|
355
|
-
variables: {
|
|
356
|
-
sku: item.sku,
|
|
357
|
-
shipDate: request.shipDate
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
// Calculate existing reservations
|
|
362
|
-
let existingReservations = 0;
|
|
363
|
-
const reservationEdges = reservationsResult.data?.inventoryQuantities?.edges || [];
|
|
364
|
-
for (const edge of reservationEdges) {
|
|
365
|
-
const shipDateAttr = edge.node.attributes?.find((a: any) => a.name === 'shipDate');
|
|
366
|
-
if (shipDateAttr?.value === request.shipDate) {
|
|
367
|
-
existingReservations += edge.node.quantity || 0;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Query expected arrivals (purchase orders, transfers)
|
|
372
|
-
const expectedArrivalResult = await client.graphql({
|
|
373
|
-
query: `
|
|
374
|
-
query GetExpectedArrivals($locationRef: String!, $skuRef: String!, $beforeDate: String!) {
|
|
375
|
-
inventoryQuantities(
|
|
376
|
-
first: 50
|
|
377
|
-
locationRef: $locationRef
|
|
378
|
-
skuRef: $skuRef
|
|
379
|
-
type: "EXPECTED"
|
|
380
|
-
expectedOnBefore: $beforeDate
|
|
381
|
-
) {
|
|
382
|
-
edges {
|
|
383
|
-
node {
|
|
384
|
-
quantity
|
|
385
|
-
expectedOn
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
`,
|
|
391
|
-
variables: {
|
|
392
|
-
locationRef: item.locationRef,
|
|
393
|
-
skuRef: item.sku,
|
|
394
|
-
beforeDate: request.shipDate
|
|
395
|
-
}
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
const expectedArrival = expectedArrivalResult.data?.inventoryQuantities?.edges?.reduce(
|
|
399
|
-
(sum: number, edge: any) => sum + (edge.node.quantity || 0),
|
|
400
|
-
0
|
|
401
|
-
) || 0;
|
|
402
|
-
|
|
403
|
-
// Calculate ATP with overbooking strategy
|
|
404
|
-
const totalExpected = currentInventory + expectedArrival;
|
|
405
|
-
const overbookingLimit = Math.floor(totalExpected * 1.05); // 105% overbooking
|
|
406
|
-
const availableToPromise = overbookingLimit - existingReservations;
|
|
407
|
-
|
|
408
|
-
const atp: ATPCalculation = {
|
|
409
|
-
sku: item.sku,
|
|
410
|
-
locationRef: item.locationRef,
|
|
411
|
-
shipDate: request.shipDate,
|
|
412
|
-
currentInventory,
|
|
413
|
-
existingReservations,
|
|
414
|
-
expectedArrival,
|
|
415
|
-
availableToPromise,
|
|
416
|
-
overbookingLimit,
|
|
417
|
-
canFulfill: availableToPromise >= item.quantity
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
atpResults.push(atp);
|
|
421
|
-
|
|
422
|
-
log.info('✅ ATP calculated', {
|
|
423
|
-
sku: item.sku,
|
|
424
|
-
current: currentInventory,
|
|
425
|
-
expected: expectedArrival,
|
|
426
|
-
reserved: existingReservations,
|
|
427
|
-
atp: availableToPromise,
|
|
428
|
-
requested: item.quantity,
|
|
429
|
-
canFulfill: atp.canFulfill
|
|
430
|
-
});
|
|
431
|
-
} catch (error) {
|
|
432
|
-
// ? Enhanced: Error logging with recommendations
|
|
433
|
-
log.error('[PreOrderAllocation] Failed to calculate ATP for SKU', {
|
|
434
|
-
sku: item.sku,
|
|
435
|
-
error: error instanceof Error ? error.message : String(error),
|
|
436
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
437
|
-
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
438
|
-
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
439
|
-
: error.message?.includes('query') || error.message?.includes('GraphQL')
|
|
440
|
-
? 'Check GraphQL query syntax and inventory query structure'
|
|
441
|
-
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
442
|
-
? 'Check network connectivity and Fluent Commerce API availability'
|
|
443
|
-
: 'Review error details and check ATP calculation logic'
|
|
444
|
-
});
|
|
445
|
-
throw error;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Check if all items can be fulfilled
|
|
450
|
-
const allAvailable = atpResults.every(atp => atp.canFulfill);
|
|
451
|
-
const duration = Date.now() - startTime;
|
|
452
|
-
|
|
453
|
-
log.info(`⏱️ ATP calculation complete (${duration}ms)`, {
|
|
454
|
-
totalItems: atpResults.length,
|
|
455
|
-
allAvailable
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
return {
|
|
459
|
-
...ctx.data,
|
|
460
|
-
atpResults,
|
|
461
|
-
allAvailable,
|
|
462
|
-
client
|
|
463
|
-
};
|
|
464
|
-
}))
|
|
465
|
-
|
|
466
|
-
// Step 3: Create reservations or add to waitlist
|
|
467
|
-
.then(fn('create-reservations', async ({ data, openKv, log }) => {
|
|
468
|
-
const { request, atpResults, allAvailable, expiresAt, client } = data;
|
|
469
|
-
|
|
470
|
-
log.info('📦 Creating reservations');
|
|
471
|
-
|
|
472
|
-
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
473
|
-
const reservationTracker = new VersoriFileTracker(openKv(), 'pre-order-reservations');
|
|
474
|
-
|
|
475
|
-
const reservations: ReservationRecord[] = [];
|
|
476
|
-
const failedItems: any[] = [];
|
|
477
|
-
|
|
478
|
-
// Calculate priority score
|
|
479
|
-
const tierScore = request.customerTier === 'VIP' ? 100 : 50;
|
|
480
|
-
const timeScore = Date.now(); // Earlier orders get lower scores (better priority)
|
|
481
|
-
const priorityScore = tierScore * 1000000 - timeScore; // VIP orders always sort higher
|
|
482
|
-
|
|
483
|
-
for (let i = 0; i < request.items.length; i++) {
|
|
484
|
-
const item = request.items[i];
|
|
485
|
-
const atp = atpResults[i];
|
|
486
|
-
|
|
487
|
-
if (!atp.canFulfill) {
|
|
488
|
-
failedItems.push({
|
|
489
|
-
sku: item.sku,
|
|
490
|
-
quantity: item.quantity,
|
|
491
|
-
available: atp.availableToPromise,
|
|
492
|
-
reason: 'Insufficient inventory available for this ship date'
|
|
493
|
-
});
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Get current queue position for this SKU + ship date
|
|
498
|
-
const queueKey = ['queue', atp.sku, atp.locationRef, request.shipDate].join(':');
|
|
499
|
-
const queueData = await kvAdapter.get([queueKey]);
|
|
500
|
-
const currentQueueLength = (queueData?.value as number) || 0;
|
|
501
|
-
const queuePosition = currentQueueLength + 1;
|
|
502
|
-
|
|
503
|
-
// Update queue length
|
|
504
|
-
await kvAdapter.set([queueKey], queuePosition);
|
|
505
|
-
|
|
506
|
-
// Create reservation record
|
|
507
|
-
const reservationId = `RES-${request.orderId}-${item.sku}-${Date.now()}`;
|
|
508
|
-
const reservation: ReservationRecord = {
|
|
509
|
-
reservationId,
|
|
510
|
-
orderId: request.orderId,
|
|
511
|
-
customerId: request.customerId,
|
|
512
|
-
customerEmail: request.customerEmail,
|
|
513
|
-
sku: item.sku,
|
|
514
|
-
quantity: item.quantity,
|
|
515
|
-
locationRef: item.locationRef,
|
|
516
|
-
shipDate: request.shipDate,
|
|
517
|
-
priority: priorityScore,
|
|
518
|
-
queuePosition,
|
|
519
|
-
tier: request.customerTier || 'STANDARD',
|
|
520
|
-
status: 'RESERVED',
|
|
521
|
-
createdAt: new Date().toISOString(),
|
|
522
|
-
expiresAt
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
// Store reservation in KV
|
|
526
|
-
await kvAdapter.set(['reservation', reservationId], reservation);
|
|
527
|
-
|
|
528
|
-
// Track in indexed tracker for listing
|
|
529
|
-
await reservationTracker.markFileProcessed(reservationId, {
|
|
530
|
-
orderId: request.orderId,
|
|
531
|
-
sku: item.sku,
|
|
532
|
-
shipDate: request.shipDate,
|
|
533
|
-
priority: priorityScore
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
// Create reservation in Fluent (as custom inventory type or entity)
|
|
537
|
-
try {
|
|
538
|
-
await client.graphql({
|
|
539
|
-
query: `
|
|
540
|
-
mutation CreateReservation($input: InventoryQuantityInput!) {
|
|
541
|
-
createInventoryQuantity(input: $input) {
|
|
542
|
-
id
|
|
543
|
-
ref
|
|
544
|
-
quantity
|
|
545
|
-
status
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
`,
|
|
549
|
-
variables: {
|
|
550
|
-
input: {
|
|
551
|
-
ref: reservationId,
|
|
552
|
-
locationRef: item.locationRef,
|
|
553
|
-
skuRef: item.sku,
|
|
554
|
-
type: 'RESERVED',
|
|
555
|
-
status: 'RESERVED',
|
|
556
|
-
quantity: item.quantity,
|
|
557
|
-
expectedOn: request.shipDate,
|
|
558
|
-
attributes: [
|
|
559
|
-
{ name: 'orderId', value: request.orderId },
|
|
560
|
-
{ name: 'customerId', value: request.customerId },
|
|
561
|
-
{ name: 'customerEmail', value: request.customerEmail },
|
|
562
|
-
{ name: 'tier', value: request.customerTier || 'STANDARD' },
|
|
563
|
-
{ name: 'priority', value: String(priorityScore) },
|
|
564
|
-
{ name: 'queuePosition', value: String(queuePosition) },
|
|
565
|
-
{ name: 'expiresAt', value: expiresAt },
|
|
566
|
-
{ name: 'reservationId', value: reservationId }
|
|
567
|
-
]
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
log.info('✅ Reservation created in Fluent', {
|
|
573
|
-
reservationId,
|
|
574
|
-
sku: item.sku,
|
|
575
|
-
quantity: item.quantity,
|
|
576
|
-
priority: priorityScore,
|
|
577
|
-
queuePosition
|
|
578
|
-
});
|
|
579
|
-
} catch (error) {
|
|
580
|
-
// ? Enhanced: Error logging with recommendations
|
|
581
|
-
log.error('[PreOrderAllocation] Failed to create reservation in Fluent', {
|
|
582
|
-
reservationId,
|
|
583
|
-
error: error instanceof Error ? error.message : String(error),
|
|
584
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
585
|
-
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
586
|
-
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
587
|
-
: error.message?.includes('mutation') || error.message?.includes('GraphQL')
|
|
588
|
-
? 'Check GraphQL mutation syntax and reservation payload structure'
|
|
589
|
-
: error.message?.includes('inventory') || error.message?.includes('quantity')
|
|
590
|
-
? 'Check available inventory quantity and reservation limits'
|
|
591
|
-
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
592
|
-
? 'Check network connectivity and Fluent Commerce API availability'
|
|
593
|
-
: 'Review error details - reservation is in KV and can be retried'
|
|
594
|
-
});
|
|
595
|
-
// Continue - reservation is in KV, can be retried
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
reservations.push(reservation);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
return {
|
|
602
|
-
success: failedItems.length === 0,
|
|
603
|
-
reservations,
|
|
604
|
-
failedItems,
|
|
605
|
-
orderId: request.orderId,
|
|
606
|
-
shipDate: request.shipDate,
|
|
607
|
-
tier: request.customerTier || 'STANDARD'
|
|
608
|
-
};
|
|
609
|
-
}))
|
|
610
|
-
|
|
611
|
-
// Step 4: Send email confirmation (placeholder)
|
|
612
|
-
.then(fn('send-confirmation', async ({ data, log }) => {
|
|
613
|
-
const { success, reservations, failedItems, orderId, shipDate, tier } = data;
|
|
614
|
-
|
|
615
|
-
log.info('📧 Sending customer notification');
|
|
616
|
-
|
|
617
|
-
// TODO: Integrate with email service (SendGrid, SES, etc.)
|
|
618
|
-
// This is a placeholder showing the data structure
|
|
619
|
-
const emailPayload = {
|
|
620
|
-
to: reservations[0]?.customerEmail,
|
|
621
|
-
subject: success
|
|
622
|
-
? `Pre-Order Confirmed - Order ${orderId}`
|
|
623
|
-
: `Pre-Order Partially Confirmed - Order ${orderId}`,
|
|
624
|
-
body: {
|
|
625
|
-
orderId,
|
|
626
|
-
shipDate,
|
|
627
|
-
tier,
|
|
628
|
-
confirmedItems: reservations.map(r => ({
|
|
629
|
-
sku: r.sku,
|
|
630
|
-
quantity: r.quantity,
|
|
631
|
-
queuePosition: r.queuePosition,
|
|
632
|
-
expiresAt: r.expiresAt
|
|
633
|
-
})),
|
|
634
|
-
failedItems,
|
|
635
|
-
message: success
|
|
636
|
-
? `Your pre-order has been confirmed! You are ${tier === 'VIP' ? 'priority' : 'standard'} tier.`
|
|
637
|
-
: `Some items could not be reserved. Please review below.`
|
|
638
|
-
}
|
|
639
|
-
};
|
|
640
|
-
|
|
641
|
-
log.info('✅ Email prepared', {
|
|
642
|
-
to: emailPayload.to,
|
|
643
|
-
confirmedCount: reservations.length,
|
|
644
|
-
failedCount: failedItems.length
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
// In production: await emailService.send(emailPayload);
|
|
648
|
-
|
|
649
|
-
return {
|
|
650
|
-
success,
|
|
651
|
-
orderId,
|
|
652
|
-
reservations: reservations.map(r => ({
|
|
653
|
-
reservationId: r.reservationId,
|
|
654
|
-
sku: r.sku,
|
|
655
|
-
quantity: r.quantity,
|
|
656
|
-
queuePosition: r.queuePosition,
|
|
657
|
-
priority: r.priority,
|
|
658
|
-
expiresAt: r.expiresAt
|
|
659
|
-
})),
|
|
660
|
-
failedItems,
|
|
661
|
-
message: success
|
|
662
|
-
? `All items reserved successfully`
|
|
663
|
-
: `${reservations.length} items reserved, ${failedItems.length} failed`,
|
|
664
|
-
timestamp: new Date().toISOString()
|
|
665
|
-
};
|
|
666
|
-
}));
|
|
667
|
-
|
|
668
|
-
// =============================================================================
|
|
669
|
-
// WORKFLOW 2: SCHEDULED RELEASE (Daily at 2 AM)
|
|
670
|
-
// =============================================================================
|
|
671
|
-
|
|
672
|
-
/**
|
|
673
|
-
* Scheduled Release Workflow - Convert reservations to allocations
|
|
674
|
-
*
|
|
675
|
-
* Runs daily at 2 AM to process reservations for today's ship date
|
|
676
|
-
*
|
|
677
|
-
* Flow:
|
|
678
|
-
* 1. Query all reservations for today's ship date
|
|
679
|
-
* 2. Sort by priority (VIP first, then by queue position)
|
|
680
|
-
* 3. Convert to fulfillment orders
|
|
681
|
-
* 4. Handle insufficient inventory (cancel lower priority)
|
|
682
|
-
* 5. Send confirmation/cancellation emails
|
|
683
|
-
*/
|
|
684
|
-
export const scheduledRelease = schedule('release-reservations', '0 2 * * *', async (ctx) => {
|
|
685
|
-
const { log, openKv } = ctx;
|
|
686
|
-
const startTime = Date.now();
|
|
687
|
-
|
|
688
|
-
return http('process-release', {
|
|
689
|
-
connection: 'fluent_commerce'
|
|
690
|
-
}, async (ctx) => {
|
|
691
|
-
const { log } = ctx;
|
|
692
|
-
const client = await createClient(ctx, { validateConnection: true });
|
|
693
|
-
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
694
|
-
const reservationTracker = new VersoriFileTracker(openKv(), 'pre-order-reservations');
|
|
695
|
-
|
|
696
|
-
log.info('🚀 Starting scheduled release process');
|
|
697
|
-
|
|
698
|
-
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
699
|
-
|
|
700
|
-
try {
|
|
701
|
-
// Step 1: Query all reservations for today
|
|
702
|
-
const result = await client.graphql({
|
|
703
|
-
query: `
|
|
704
|
-
query GetTodayReservations($shipDate: String!) {
|
|
705
|
-
inventoryQuantities(
|
|
706
|
-
first: 1000
|
|
707
|
-
type: "RESERVED"
|
|
708
|
-
status: "RESERVED"
|
|
709
|
-
expectedOn: $shipDate
|
|
710
|
-
) {
|
|
711
|
-
edges {
|
|
712
|
-
node {
|
|
713
|
-
id
|
|
714
|
-
ref
|
|
715
|
-
quantity
|
|
716
|
-
skuRef
|
|
717
|
-
locationRef
|
|
718
|
-
attributes {
|
|
719
|
-
name
|
|
720
|
-
value
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
`,
|
|
727
|
-
variables: { shipDate: today }
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
const reservationEdges = result.data?.inventoryQuantities?.edges || [];
|
|
731
|
-
log.info(`📋 Found ${reservationEdges.length} reservations for ${today}`);
|
|
732
|
-
|
|
733
|
-
// Step 2: Parse and sort by priority
|
|
734
|
-
interface ReservationWithPriority {
|
|
735
|
-
id: string;
|
|
736
|
-
ref: string;
|
|
737
|
-
quantity: number;
|
|
738
|
-
sku: string;
|
|
739
|
-
locationRef: string;
|
|
740
|
-
priority: number;
|
|
741
|
-
queuePosition: number;
|
|
742
|
-
orderId: string;
|
|
743
|
-
customerId: string;
|
|
744
|
-
customerEmail: string;
|
|
745
|
-
tier: string;
|
|
746
|
-
reservationId: string;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
const reservations: ReservationWithPriority[] = reservationEdges.map((edge: any) => {
|
|
750
|
-
const attrs = edge.node.attributes || [];
|
|
751
|
-
const getAttr = (name: string) => attrs.find((a: any) => a.name === name)?.value;
|
|
752
|
-
|
|
753
|
-
return {
|
|
754
|
-
id: edge.node.id,
|
|
755
|
-
ref: edge.node.ref,
|
|
756
|
-
quantity: edge.node.quantity,
|
|
757
|
-
sku: edge.node.skuRef,
|
|
758
|
-
locationRef: edge.node.locationRef,
|
|
759
|
-
priority: parseInt(getAttr('priority') || '0'),
|
|
760
|
-
queuePosition: parseInt(getAttr('queuePosition') || '999'),
|
|
761
|
-
orderId: getAttr('orderId') || '',
|
|
762
|
-
customerId: getAttr('customerId') || '',
|
|
763
|
-
customerEmail: getAttr('customerEmail') || '',
|
|
764
|
-
tier: getAttr('tier') || 'STANDARD',
|
|
765
|
-
reservationId: getAttr('reservationId') || edge.node.ref
|
|
766
|
-
};
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
// Sort by priority (higher priority = lower number due to negative time component)
|
|
770
|
-
reservations.sort((a, b) => b.priority - a.priority);
|
|
771
|
-
|
|
772
|
-
log.info('✅ Sorted reservations by priority', {
|
|
773
|
-
vipCount: reservations.filter(r => r.tier === 'VIP').length,
|
|
774
|
-
standardCount: reservations.filter(r => r.tier === 'STANDARD').length
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
// Step 3: Group by SKU and location
|
|
778
|
-
const groupedBySku = new Map<string, ReservationWithPriority[]>();
|
|
779
|
-
for (const reservation of reservations) {
|
|
780
|
-
const key = `${reservation.sku}:${reservation.locationRef}`;
|
|
781
|
-
if (!groupedBySku.has(key)) {
|
|
782
|
-
groupedBySku.set(key, []);
|
|
783
|
-
}
|
|
784
|
-
groupedBySku.get(key)!.push(reservation);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// Step 4: Process each SKU group
|
|
788
|
-
const confirmed: any[] = [];
|
|
789
|
-
const cancelled: any[] = [];
|
|
790
|
-
|
|
791
|
-
for (const [skuKey, skuReservations] of groupedBySku.entries()) {
|
|
792
|
-
const [sku, locationRef] = skuKey.split(':');
|
|
793
|
-
|
|
794
|
-
// Query current inventory
|
|
795
|
-
const invResult = await client.graphql({
|
|
796
|
-
query: `
|
|
797
|
-
query GetInventory($sku: String!, $locationRef: String!) {
|
|
798
|
-
inventoryQuantities(
|
|
799
|
-
first: 1
|
|
800
|
-
skuRef: $sku
|
|
801
|
-
locationRef: $locationRef
|
|
802
|
-
type: "LAST_ON_HAND"
|
|
803
|
-
) {
|
|
804
|
-
edges {
|
|
805
|
-
node {
|
|
806
|
-
quantity
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
`,
|
|
812
|
-
variables: { sku, locationRef }
|
|
813
|
-
});
|
|
814
|
-
|
|
815
|
-
let availableQty = invResult.data?.inventoryQuantities?.edges?.[0]?.node?.quantity || 0;
|
|
816
|
-
|
|
817
|
-
log.info(`⚙️ Processing ${skuReservations.length} reservations for ${sku}`, {
|
|
818
|
-
available: availableQty,
|
|
819
|
-
totalRequested: skuReservations.reduce((sum, r) => sum + r.quantity, 0)
|
|
820
|
-
});
|
|
821
|
-
|
|
822
|
-
// Allocate inventory by priority
|
|
823
|
-
for (const reservation of skuReservations) {
|
|
824
|
-
if (availableQty >= reservation.quantity) {
|
|
825
|
-
// Confirm reservation - create fulfillment order
|
|
826
|
-
try {
|
|
827
|
-
await client.graphql({
|
|
828
|
-
query: `
|
|
829
|
-
mutation CreateFulfillmentOrder($input: CreateOrderInput!) {
|
|
830
|
-
createOrder(input: $input) {
|
|
831
|
-
id
|
|
832
|
-
ref
|
|
833
|
-
status
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
`,
|
|
837
|
-
variables: {
|
|
838
|
-
input: {
|
|
839
|
-
ref: `FO-${reservation.orderId}-${Date.now()}`,
|
|
840
|
-
type: 'FULFILLMENT',
|
|
841
|
-
retailerId: '1', // From config
|
|
842
|
-
customer: {
|
|
843
|
-
ref: reservation.customerId,
|
|
844
|
-
email: reservation.customerEmail
|
|
845
|
-
},
|
|
846
|
-
items: [{
|
|
847
|
-
skuRef: reservation.sku,
|
|
848
|
-
quantity: reservation.quantity,
|
|
849
|
-
locationRef: reservation.locationRef
|
|
850
|
-
}],
|
|
851
|
-
attributes: [
|
|
852
|
-
{ name: 'originalOrderId', value: reservation.orderId },
|
|
853
|
-
{ name: 'reservationId', value: reservation.reservationId },
|
|
854
|
-
{ name: 'tier', value: reservation.tier },
|
|
855
|
-
{ name: 'shipDate', value: today }
|
|
856
|
-
]
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
});
|
|
860
|
-
|
|
861
|
-
// Update reservation status
|
|
862
|
-
await client.graphql({
|
|
863
|
-
query: `
|
|
864
|
-
mutation UpdateReservation($id: ID!, $status: String!) {
|
|
865
|
-
updateInventoryQuantity(id: $id, status: $status) {
|
|
866
|
-
id
|
|
867
|
-
status
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
`,
|
|
871
|
-
variables: {
|
|
872
|
-
id: reservation.id,
|
|
873
|
-
status: 'CONFIRMED'
|
|
874
|
-
}
|
|
875
|
-
});
|
|
876
|
-
|
|
877
|
-
// Update KV
|
|
878
|
-
const kvRecord = await kvAdapter.get(['reservation', reservation.reservationId]);
|
|
879
|
-
if (kvRecord?.value) {
|
|
880
|
-
const record = kvRecord.value as ReservationRecord;
|
|
881
|
-
record.status = 'CONFIRMED';
|
|
882
|
-
record.confirmedAt = new Date().toISOString();
|
|
883
|
-
await kvAdapter.set(['reservation', reservation.reservationId], record);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
availableQty -= reservation.quantity;
|
|
887
|
-
confirmed.push(reservation);
|
|
888
|
-
|
|
889
|
-
log.info(`✅ Confirmed reservation: ${reservation.reservationId}`);
|
|
890
|
-
} catch (error) {
|
|
891
|
-
// ? Enhanced: Error logging with recommendations
|
|
892
|
-
log.error('[PreOrderAllocation] Failed to confirm reservation', {
|
|
893
|
-
reservationId: reservation.reservationId,
|
|
894
|
-
error: error instanceof Error ? error.message : String(error),
|
|
895
|
-
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
896
|
-
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
897
|
-
: error.message?.includes('mutation') || error.message?.includes('GraphQL')
|
|
898
|
-
? 'Check GraphQL mutation syntax and reservation confirmation payload'
|
|
899
|
-
: error.message?.includes('inventory') || error.message?.includes('quantity')
|
|
900
|
-
? 'Check available inventory quantity and reservation status'
|
|
901
|
-
: 'Review error details and check reservation confirmation logic'
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
|
-
} else {
|
|
905
|
-
// Insufficient inventory - cancel reservation
|
|
906
|
-
try {
|
|
907
|
-
await client.graphql({
|
|
908
|
-
query: `
|
|
909
|
-
mutation UpdateReservation($id: ID!, $status: String!) {
|
|
910
|
-
updateInventoryQuantity(id: $id, status: $status) {
|
|
911
|
-
id
|
|
912
|
-
status
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
`,
|
|
916
|
-
variables: {
|
|
917
|
-
id: reservation.id,
|
|
918
|
-
status: 'CANCELLED'
|
|
919
|
-
}
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
// Update KV
|
|
923
|
-
const kvRecord = await kvAdapter.get(['reservation', reservation.reservationId]);
|
|
924
|
-
if (kvRecord?.value) {
|
|
925
|
-
const record = kvRecord.value as ReservationRecord;
|
|
926
|
-
record.status = 'CANCELLED';
|
|
927
|
-
record.cancelledAt = new Date().toISOString();
|
|
928
|
-
await kvAdapter.set(['reservation', reservation.reservationId], record);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
cancelled.push({
|
|
932
|
-
...reservation,
|
|
933
|
-
reason: 'Insufficient inventory',
|
|
934
|
-
availableQty
|
|
935
|
-
});
|
|
936
|
-
|
|
937
|
-
log.warn(`⚠️ Cancelled reservation: ${reservation.reservationId}`, {
|
|
938
|
-
reason: 'insufficient_inventory',
|
|
939
|
-
needed: reservation.quantity,
|
|
940
|
-
available: availableQty
|
|
941
|
-
});
|
|
942
|
-
} catch (error) {
|
|
943
|
-
// ? Enhanced: Error logging with recommendations
|
|
944
|
-
log.error('[PreOrderAllocation] Failed to cancel reservation', {
|
|
945
|
-
reservationId: reservation.reservationId,
|
|
946
|
-
error: error instanceof Error ? error.message : String(error),
|
|
947
|
-
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
948
|
-
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
949
|
-
: error.message?.includes('mutation') || error.message?.includes('GraphQL')
|
|
950
|
-
? 'Check GraphQL mutation syntax and reservation cancellation payload'
|
|
951
|
-
: error.message?.includes('not found') || error.message?.includes('missing')
|
|
952
|
-
? 'Reservation not found - verify reservationId and check KV store'
|
|
953
|
-
: 'Review error details and check reservation cancellation logic'
|
|
954
|
-
});
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
// Step 5: Send notifications (placeholder)
|
|
961
|
-
const duration = Date.now() - startTime;
|
|
962
|
-
log.info(`📧 Sending notifications (${duration}ms total)`, {
|
|
963
|
-
confirmed: confirmed.length,
|
|
964
|
-
cancelled: cancelled.length
|
|
965
|
-
});
|
|
966
|
-
|
|
967
|
-
// TODO: Send confirmation emails to confirmed reservations
|
|
968
|
-
// TODO: Send cancellation/apology emails to cancelled reservations
|
|
969
|
-
|
|
970
|
-
return {
|
|
971
|
-
success: true,
|
|
972
|
-
date: today,
|
|
973
|
-
processed: reservations.length,
|
|
974
|
-
confirmed: confirmed.length,
|
|
975
|
-
cancelled: cancelled.length,
|
|
976
|
-
duration,
|
|
977
|
-
summary: {
|
|
978
|
-
vipConfirmed: confirmed.filter(r => r.tier === 'VIP').length,
|
|
979
|
-
standardConfirmed: confirmed.filter(r => r.tier === 'STANDARD').length,
|
|
980
|
-
vipCancelled: cancelled.filter(r => r.tier === 'VIP').length,
|
|
981
|
-
standardCancelled: cancelled.filter(r => r.tier === 'STANDARD').length
|
|
982
|
-
},
|
|
983
|
-
timestamp: new Date().toISOString()
|
|
984
|
-
};
|
|
985
|
-
} catch (error) {
|
|
986
|
-
// ? Enhanced: Error logging with recommendations
|
|
987
|
-
log.error('[PreOrderAllocation] Scheduled release failed', {
|
|
988
|
-
error: error instanceof Error ? error.message : String(error),
|
|
989
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
990
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
991
|
-
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
992
|
-
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
993
|
-
: error.message?.includes('KV') || error.message?.includes('state')
|
|
994
|
-
? 'Check KV store connectivity and reservation state structure'
|
|
995
|
-
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
996
|
-
? 'Check network connectivity and Fluent Commerce API availability'
|
|
997
|
-
: 'Review error details and check scheduled release workflow configuration'
|
|
998
|
-
});
|
|
999
|
-
throw error;
|
|
1000
|
-
}
|
|
1001
|
-
})(ctx);
|
|
1002
|
-
});
|
|
1003
|
-
|
|
1004
|
-
// =============================================================================
|
|
1005
|
-
// WORKFLOW 3: CANCELLATION WEBHOOK
|
|
1006
|
-
// =============================================================================
|
|
1007
|
-
|
|
1008
|
-
/**
|
|
1009
|
-
* Cancellation Webhook - Cancel reservation and offer to next in queue
|
|
1010
|
-
*
|
|
1011
|
-
* Flow:
|
|
1012
|
-
* 1. Receive cancellation request
|
|
1013
|
-
* 2. Update reservation status
|
|
1014
|
-
* 3. Query next customer in queue
|
|
1015
|
-
* 4. Offer to next customer (if available)
|
|
1016
|
-
* 5. Send notifications
|
|
1017
|
-
*/
|
|
1018
|
-
export const cancellationWebhook = webhook('cancel-reservation', {
|
|
1019
|
-
response: { mode: 'sync' },
|
|
1020
|
-
cors: true
|
|
1021
|
-
})
|
|
1022
|
-
.then(fn('validate-cancellation', ({ data, log }) => {
|
|
1023
|
-
log.info('🔍 Processing cancellation request');
|
|
1024
|
-
|
|
1025
|
-
if (!data.reservationId && !data.orderId) {
|
|
1026
|
-
throw new Error('Either reservationId or orderId is required');
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
return data;
|
|
1030
|
-
}))
|
|
1031
|
-
.then(http('cancel-and-requeue', {
|
|
1032
|
-
connection: 'fluent_commerce'
|
|
1033
|
-
}, async (ctx) => {
|
|
1034
|
-
const { reservationId, orderId } = ctx.data;
|
|
1035
|
-
const { log, openKv } = ctx;
|
|
1036
|
-
const client = await createClient(ctx, { validateConnection: true });
|
|
1037
|
-
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
1038
|
-
|
|
1039
|
-
log.info('🚫 Cancelling reservation', { reservationId, orderId });
|
|
1040
|
-
|
|
1041
|
-
// Find reservation in KV
|
|
1042
|
-
let reservation: ReservationRecord | null = null;
|
|
1043
|
-
|
|
1044
|
-
if (reservationId) {
|
|
1045
|
-
const kvRecord = await kvAdapter.get(['reservation', reservationId]);
|
|
1046
|
-
reservation = kvRecord?.value as ReservationRecord;
|
|
1047
|
-
} else {
|
|
1048
|
-
// Search by orderId (would need indexed tracker)
|
|
1049
|
-
// Placeholder: iterate through all reservations
|
|
1050
|
-
log.warn('[CANCEL] Searching by orderId - implement indexed search for production');
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
if (!reservation) {
|
|
1054
|
-
throw new Error(`Reservation not found: ${reservationId || orderId}`);
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
// Update status in Fluent
|
|
1058
|
-
const result = await client.graphql({
|
|
1059
|
-
query: `
|
|
1060
|
-
query FindReservation($ref: String!) {
|
|
1061
|
-
inventoryQuantities(first: 1, ref: $ref) {
|
|
1062
|
-
edges {
|
|
1063
|
-
node {
|
|
1064
|
-
id
|
|
1065
|
-
ref
|
|
1066
|
-
status
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
`,
|
|
1072
|
-
variables: { ref: reservation.reservationId }
|
|
1073
|
-
});
|
|
1074
|
-
|
|
1075
|
-
const inventoryId = result.data?.inventoryQuantities?.edges?.[0]?.node?.id;
|
|
1076
|
-
|
|
1077
|
-
if (inventoryId) {
|
|
1078
|
-
await client.graphql({
|
|
1079
|
-
query: `
|
|
1080
|
-
mutation CancelReservation($id: ID!, $status: String!) {
|
|
1081
|
-
updateInventoryQuantity(id: $id, status: $status) {
|
|
1082
|
-
id
|
|
1083
|
-
status
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
`,
|
|
1087
|
-
variables: {
|
|
1088
|
-
id: inventoryId,
|
|
1089
|
-
status: 'CANCELLED'
|
|
1090
|
-
}
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
// Update KV
|
|
1095
|
-
reservation.status = 'CANCELLED';
|
|
1096
|
-
reservation.cancelledAt = new Date().toISOString();
|
|
1097
|
-
await kvAdapter.set(['reservation', reservation.reservationId], reservation);
|
|
1098
|
-
|
|
1099
|
-
log.info('✅ Reservation cancelled', {
|
|
1100
|
-
reservationId: reservation.reservationId,
|
|
1101
|
-
sku: reservation.sku,
|
|
1102
|
-
quantity: reservation.quantity
|
|
1103
|
-
});
|
|
1104
|
-
|
|
1105
|
-
// Find next in queue
|
|
1106
|
-
const queueKey = ['queue', reservation.sku, reservation.locationRef, reservation.shipDate].join(':');
|
|
1107
|
-
|
|
1108
|
-
// TODO: Implement queue management
|
|
1109
|
-
// Query next customer with higher queue position
|
|
1110
|
-
// Send offer to next customer
|
|
1111
|
-
|
|
1112
|
-
log.info('📋 Queue position available for next customer', {
|
|
1113
|
-
sku: reservation.sku,
|
|
1114
|
-
shipDate: reservation.shipDate,
|
|
1115
|
-
releasedQuantity: reservation.quantity
|
|
1116
|
-
});
|
|
1117
|
-
|
|
1118
|
-
return {
|
|
1119
|
-
success: true,
|
|
1120
|
-
cancelled: {
|
|
1121
|
-
reservationId: reservation.reservationId,
|
|
1122
|
-
orderId: reservation.orderId,
|
|
1123
|
-
sku: reservation.sku,
|
|
1124
|
-
quantity: reservation.quantity
|
|
1125
|
-
},
|
|
1126
|
-
message: 'Reservation cancelled successfully',
|
|
1127
|
-
timestamp: new Date().toISOString()
|
|
1128
|
-
};
|
|
1129
|
-
}));
|
|
1130
|
-
|
|
1131
|
-
// =============================================================================
|
|
1132
|
-
// WORKFLOW 4: ADMIN DASHBOARD EXPORT
|
|
1133
|
-
// =============================================================================
|
|
1134
|
-
|
|
1135
|
-
/**
|
|
1136
|
-
* Export Reservations to S3 for Admin Dashboard
|
|
1137
|
-
*
|
|
1138
|
-
* Runs daily to export reservation status to S3 for dashboard visualization
|
|
1139
|
-
*/
|
|
1140
|
-
export const exportReservations = schedule('export-reservations', '0 3 * * *', async (ctx) => {
|
|
1141
|
-
const { openKv, log } = ctx;
|
|
1142
|
-
|
|
1143
|
-
return fn('export-to-s3', async ({ openKv, log }) => {
|
|
1144
|
-
log.info('📤 Exporting reservations to S3');
|
|
1145
|
-
|
|
1146
|
-
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
1147
|
-
const reservationTracker = new VersoriFileTracker(openKv(), 'pre-order-reservations');
|
|
1148
|
-
|
|
1149
|
-
// Get all reservations
|
|
1150
|
-
const allReservations = await reservationTracker.listProcessedFiles();
|
|
1151
|
-
log.info(`📋 Found ${allReservations.length} reservations to export`);
|
|
1152
|
-
|
|
1153
|
-
// Fetch full reservation data
|
|
1154
|
-
const exportData: any[] = [];
|
|
1155
|
-
for (const tracked of allReservations) {
|
|
1156
|
-
const kvRecord = await kvAdapter.get(['reservation', tracked.fileName]);
|
|
1157
|
-
if (kvRecord?.value) {
|
|
1158
|
-
exportData.push(kvRecord.value);
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// Group by ship date and status
|
|
1163
|
-
const summary = {
|
|
1164
|
-
total: exportData.length,
|
|
1165
|
-
byStatus: {
|
|
1166
|
-
RESERVED: exportData.filter(r => r.status === 'RESERVED').length,
|
|
1167
|
-
CONFIRMED: exportData.filter(r => r.status === 'CONFIRMED').length,
|
|
1168
|
-
CANCELLED: exportData.filter(r => r.status === 'CANCELLED').length,
|
|
1169
|
-
EXPIRED: exportData.filter(r => r.status === 'EXPIRED').length
|
|
1170
|
-
},
|
|
1171
|
-
byTier: {
|
|
1172
|
-
VIP: exportData.filter(r => r.tier === 'VIP').length,
|
|
1173
|
-
STANDARD: exportData.filter(r => r.tier === 'STANDARD').length
|
|
1174
|
-
},
|
|
1175
|
-
exportedAt: new Date().toISOString()
|
|
1176
|
-
};
|
|
1177
|
-
|
|
1178
|
-
const exportPayload = {
|
|
1179
|
-
summary,
|
|
1180
|
-
reservations: exportData
|
|
1181
|
-
};
|
|
1182
|
-
|
|
1183
|
-
// TODO: Upload to S3
|
|
1184
|
-
// const s3Key = `pre-orders/export-${new Date().toISOString().split('T')[0]}.json`;
|
|
1185
|
-
// await s3Client.putObject({ Bucket, Key: s3Key, Body: JSON.stringify(exportPayload) });
|
|
1186
|
-
|
|
1187
|
-
log.info('✅ Export complete', summary);
|
|
1188
|
-
|
|
1189
|
-
return {
|
|
1190
|
-
success: true,
|
|
1191
|
-
summary,
|
|
1192
|
-
recordCount: exportData.length
|
|
1193
|
-
};
|
|
1194
|
-
})(ctx);
|
|
1195
|
-
});
|
|
1196
|
-
|
|
1197
|
-
// =============================================================================
|
|
1198
|
-
// WORKFLOW 5: MANUAL ATP CHECK (Testing/Admin)
|
|
1199
|
-
// =============================================================================
|
|
1200
|
-
|
|
1201
|
-
/**
|
|
1202
|
-
* Manual ATP Check - Admin endpoint to check future ATP
|
|
1203
|
-
*/
|
|
1204
|
-
export const checkAtp = webhook('check-atp', {
|
|
1205
|
-
response: { mode: 'sync' },
|
|
1206
|
-
cors: true
|
|
1207
|
-
})
|
|
1208
|
-
.then(http('calculate-manual-atp', {
|
|
1209
|
-
connection: 'fluent_commerce'
|
|
1210
|
-
}, async (ctx) => {
|
|
1211
|
-
const { sku, locationRef, shipDate, quantity } = ctx.data;
|
|
1212
|
-
const { log } = ctx;
|
|
1213
|
-
const startTime = Date.now();
|
|
1214
|
-
const client = await createClient(ctx, { validateConnection: true });
|
|
1215
|
-
|
|
1216
|
-
log.info('🔍 Manual ATP calculation', { sku, locationRef, shipDate, quantity });
|
|
1217
|
-
|
|
1218
|
-
// Reuse ATP calculation logic from pre-order workflow
|
|
1219
|
-
const inventoryResult = await client.graphql({
|
|
1220
|
-
query: `
|
|
1221
|
-
query GetInventory($locationRef: String!, $skuRef: String!) {
|
|
1222
|
-
inventoryQuantities(
|
|
1223
|
-
first: 1
|
|
1224
|
-
locationRef: $locationRef
|
|
1225
|
-
skuRef: $skuRef
|
|
1226
|
-
type: "LAST_ON_HAND"
|
|
1227
|
-
) {
|
|
1228
|
-
edges {
|
|
1229
|
-
node {
|
|
1230
|
-
quantity
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
`,
|
|
1236
|
-
variables: { locationRef, skuRef: sku }
|
|
1237
|
-
});
|
|
1238
|
-
|
|
1239
|
-
const currentInventory = inventoryResult.data?.inventoryQuantities?.edges?.[0]?.node?.quantity || 0;
|
|
1240
|
-
|
|
1241
|
-
// Query reservations
|
|
1242
|
-
const reservationsResult = await client.graphql({
|
|
1243
|
-
query: `
|
|
1244
|
-
query GetReservations($sku: String!, $shipDate: String!) {
|
|
1245
|
-
inventoryQuantities(
|
|
1246
|
-
first: 100
|
|
1247
|
-
skuRef: $sku
|
|
1248
|
-
status: "RESERVED"
|
|
1249
|
-
) {
|
|
1250
|
-
edges {
|
|
1251
|
-
node {
|
|
1252
|
-
quantity
|
|
1253
|
-
attributes {
|
|
1254
|
-
name
|
|
1255
|
-
value
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
`,
|
|
1262
|
-
variables: { sku, shipDate }
|
|
1263
|
-
});
|
|
1264
|
-
|
|
1265
|
-
let existingReservations = 0;
|
|
1266
|
-
const reservationEdges = reservationsResult.data?.inventoryQuantities?.edges || [];
|
|
1267
|
-
for (const edge of reservationEdges) {
|
|
1268
|
-
const shipDateAttr = edge.node.attributes?.find((a: any) => a.name === 'shipDate');
|
|
1269
|
-
if (shipDateAttr?.value === shipDate) {
|
|
1270
|
-
existingReservations += edge.node.quantity || 0;
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
// Query expected arrivals
|
|
1275
|
-
const expectedArrivalResult = await client.graphql({
|
|
1276
|
-
query: `
|
|
1277
|
-
query GetExpectedArrivals($locationRef: String!, $skuRef: String!, $beforeDate: String!) {
|
|
1278
|
-
inventoryQuantities(
|
|
1279
|
-
first: 50
|
|
1280
|
-
locationRef: $locationRef
|
|
1281
|
-
skuRef: $skuRef
|
|
1282
|
-
type: "EXPECTED"
|
|
1283
|
-
expectedOnBefore: $beforeDate
|
|
1284
|
-
) {
|
|
1285
|
-
edges {
|
|
1286
|
-
node {
|
|
1287
|
-
quantity
|
|
1288
|
-
expectedOn
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
`,
|
|
1294
|
-
variables: { locationRef, skuRef: sku, beforeDate: shipDate }
|
|
1295
|
-
});
|
|
1296
|
-
|
|
1297
|
-
const expectedArrival = expectedArrivalResult.data?.inventoryQuantities?.edges?.reduce(
|
|
1298
|
-
(sum: number, edge: any) => sum + (edge.node.quantity || 0),
|
|
1299
|
-
0
|
|
1300
|
-
) || 0;
|
|
1301
|
-
|
|
1302
|
-
const totalExpected = currentInventory + expectedArrival;
|
|
1303
|
-
const overbookingLimit = Math.floor(totalExpected * 1.05);
|
|
1304
|
-
const availableToPromise = overbookingLimit - existingReservations;
|
|
1305
|
-
const canFulfill = availableToPromise >= (quantity || 1);
|
|
1306
|
-
const duration = Date.now() - startTime;
|
|
1307
|
-
|
|
1308
|
-
log.info(`✅ ATP check complete (${duration}ms)`, { sku, canFulfill });
|
|
1309
|
-
|
|
1310
|
-
return {
|
|
1311
|
-
sku,
|
|
1312
|
-
locationRef,
|
|
1313
|
-
shipDate,
|
|
1314
|
-
requestedQuantity: quantity || 1,
|
|
1315
|
-
currentInventory,
|
|
1316
|
-
expectedArrival,
|
|
1317
|
-
totalExpected,
|
|
1318
|
-
existingReservations,
|
|
1319
|
-
overbookingLimit,
|
|
1320
|
-
availableToPromise,
|
|
1321
|
-
canFulfill,
|
|
1322
|
-
duration,
|
|
1323
|
-
recommendation: canFulfill
|
|
1324
|
-
? 'Reservation can be fulfilled'
|
|
1325
|
-
: `Insufficient inventory (need ${quantity || 1}, available ${availableToPromise})`,
|
|
1326
|
-
timestamp: new Date().toISOString()
|
|
1327
|
-
};
|
|
1328
|
-
}));
|
|
1329
|
-
```
|
|
1330
|
-
|
|
1331
|
-
---
|
|
1332
|
-
|
|
1333
|
-
## 2. Configuration File: `config/priority-tiers.json`
|
|
1334
|
-
|
|
1335
|
-
```json
|
|
1336
|
-
{
|
|
1337
|
-
"version": "1.0.0",
|
|
1338
|
-
"description": "Pre-order priority tier configuration",
|
|
1339
|
-
"tiers": {
|
|
1340
|
-
"VIP": {
|
|
1341
|
-
"score": 100,
|
|
1342
|
-
"description": "VIP customers get priority allocation",
|
|
1343
|
-
"benefits": [
|
|
1344
|
-
"First priority for limited inventory",
|
|
1345
|
-
"Email notifications for restocks",
|
|
1346
|
-
"Expedited shipping",
|
|
1347
|
-
"Early access to new launches"
|
|
1348
|
-
]
|
|
1349
|
-
},
|
|
1350
|
-
"STANDARD": {
|
|
1351
|
-
"score": 50,
|
|
1352
|
-
"description": "Standard customer tier",
|
|
1353
|
-
"benefits": [
|
|
1354
|
-
"Standard allocation priority",
|
|
1355
|
-
"Email notifications",
|
|
1356
|
-
"Standard shipping"
|
|
1357
|
-
]
|
|
1358
|
-
}
|
|
1359
|
-
},
|
|
1360
|
-
"overbooking": {
|
|
1361
|
-
"enabled": true,
|
|
1362
|
-
"percentage": 1.05,
|
|
1363
|
-
"description": "Allow 105% allocation to account for cancellations"
|
|
1364
|
-
},
|
|
1365
|
-
"expiration": {
|
|
1366
|
-
"daysBeforeShipDate": 7,
|
|
1367
|
-
"description": "Reservations expire 7 days before ship date if not confirmed"
|
|
1368
|
-
},
|
|
1369
|
-
"queue": {
|
|
1370
|
-
"maxQueueDepth": 10000,
|
|
1371
|
-
"reallocationOnCancellation": true
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
```
|
|
1375
|
-
|
|
1376
|
-
---
|
|
1377
|
-
|
|
1378
|
-
## 3. Package Configuration: `package.json`
|
|
1379
|
-
|
|
1380
|
-
```json
|
|
1381
|
-
{
|
|
1382
|
-
"name": "pre-order-allocation-management",
|
|
1383
|
-
"version": "1.0.0",
|
|
1384
|
-
"description": "Pre-order allocation system with priority queuing and future ATP",
|
|
1385
|
-
"versori": {
|
|
1386
|
-
"workflows": "./index.ts"
|
|
1387
|
-
},
|
|
1388
|
-
"dependencies": {
|
|
1389
|
-
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
1390
|
-
"@versori/run": "latest"
|
|
1391
|
-
},
|
|
1392
|
-
"devDependencies": {
|
|
1393
|
-
"@types/node": "^20.0.0",
|
|
1394
|
-
"typescript": "^5.0.0"
|
|
1395
|
-
},
|
|
1396
|
-
"scripts": {
|
|
1397
|
-
"deploy": "versori deploy",
|
|
1398
|
-
"logs": "versori logs",
|
|
1399
|
-
"test-preorder": "curl -X POST http://localhost:8080/pre-order -H 'Content-Type: application/json' -d @test/sample-preorder.json",
|
|
1400
|
-
"test-atp": "curl -X POST http://localhost:8080/check-atp -H 'Content-Type: application/json' -d '{\"sku\":\"IPHONE-15-PRO-256\",\"locationRef\":\"DC-NY\",\"shipDate\":\"2025-02-15\",\"quantity\":1}'"
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
```
|
|
1404
|
-
|
|
1405
|
-
---
|
|
1406
|
-
|
|
1407
|
-
## 4. Test Data: `test/sample-preorder.json`
|
|
1408
|
-
|
|
1409
|
-
```json
|
|
1410
|
-
{
|
|
1411
|
-
"orderId": "PRE-2025-001234",
|
|
1412
|
-
"customerId": "CUST-VIP-9876",
|
|
1413
|
-
"customerEmail": "john.doe@example.com",
|
|
1414
|
-
"customerTier": "VIP",
|
|
1415
|
-
"shipDate": "2025-02-15T00:00:00Z",
|
|
1416
|
-
"source": "shopify",
|
|
1417
|
-
"items": [
|
|
1418
|
-
{
|
|
1419
|
-
"sku": "IPHONE-15-PRO-256",
|
|
1420
|
-
"productName": "iPhone 15 Pro 256GB Titanium Blue",
|
|
1421
|
-
"quantity": 1,
|
|
1422
|
-
"locationRef": "DC-NY"
|
|
1423
|
-
}
|
|
1424
|
-
]
|
|
1425
|
-
}
|
|
1426
|
-
```
|
|
1427
|
-
|
|
1428
|
-
---
|
|
1429
|
-
|
|
1430
|
-
## Versori Workflow Structure
|
|
1431
|
-
|
|
1432
|
-
This solution uses multiple workflow types:
|
|
1433
|
-
|
|
1434
|
-
### HTTP Webhooks (Real-time)
|
|
1435
|
-
|
|
1436
|
-
1. **pre-order** - Receives pre-orders from e-commerce
|
|
1437
|
-
2. **cancel-reservation** - Cancels existing reservations
|
|
1438
|
-
3. **check-atp** - Manual ATP calculation (admin)
|
|
1439
|
-
|
|
1440
|
-
### Scheduled Workflows (Cron)
|
|
1441
|
-
|
|
1442
|
-
1. **release-reservations** - Daily at 2 AM
|
|
1443
|
-
2. **export-reservations** - Daily at 3 AM (dashboard data)
|
|
1444
|
-
|
|
1445
|
-
### Workflow Dependencies
|
|
1446
|
-
|
|
1447
|
-
```
|
|
1448
|
-
┌─────────────────┐
|
|
1449
|
-
│ E-commerce │
|
|
1450
|
-
│ Platform │
|
|
1451
|
-
└────────┬────────┘
|
|
1452
|
-
│ HTTP POST
|
|
1453
|
-
▼
|
|
1454
|
-
┌─────────────────┐ ┌──────────────┐
|
|
1455
|
-
│ Pre-Order │─────▶│ VersoriKV │
|
|
1456
|
-
│ Webhook │ │ (State) │
|
|
1457
|
-
└────────┬────────┘ └──────────────┘
|
|
1458
|
-
│
|
|
1459
|
-
▼
|
|
1460
|
-
┌─────────────────┐
|
|
1461
|
-
│ Fluent API │
|
|
1462
|
-
│ (Reservations) │
|
|
1463
|
-
└─────────────────┘
|
|
1464
|
-
▲
|
|
1465
|
-
│
|
|
1466
|
-
┌────────┴────────┐
|
|
1467
|
-
│ Release │ (Scheduled 2 AM)
|
|
1468
|
-
│ Workflow │
|
|
1469
|
-
└────────┬────────┘
|
|
1470
|
-
│
|
|
1471
|
-
▼
|
|
1472
|
-
┌─────────────────┐
|
|
1473
|
-
│ Fulfillment │
|
|
1474
|
-
│ Orders │
|
|
1475
|
-
└─────────────────┘
|
|
1476
|
-
```
|
|
1477
|
-
|
|
1478
|
-
---
|
|
1479
|
-
|
|
1480
|
-
## Key Patterns Explained
|
|
1481
|
-
|
|
1482
|
-
### Pattern 1: Future ATP (Available to Promise) Calculation
|
|
1483
|
-
|
|
1484
|
-
**Algorithm:**
|
|
1485
|
-
|
|
1486
|
-
```typescript
|
|
1487
|
-
// Step 1: Get current inventory
|
|
1488
|
-
currentInventory = queryInventory(sku, location)
|
|
1489
|
-
|
|
1490
|
-
// Step 2: Get expected arrivals before ship date
|
|
1491
|
-
expectedArrival = queryExpectedBefore(sku, location, shipDate)
|
|
1492
|
-
|
|
1493
|
-
// Step 3: Get existing reservations for this date
|
|
1494
|
-
existingReservations = queryReservations(sku, location, shipDate)
|
|
1495
|
-
|
|
1496
|
-
// Step 4: Calculate overbooking limit (105%)
|
|
1497
|
-
totalExpected = currentInventory + expectedArrival
|
|
1498
|
-
overbookingLimit = totalExpected * 1.05
|
|
1499
|
-
|
|
1500
|
-
// Step 5: Calculate ATP
|
|
1501
|
-
availableToPromise = overbookingLimit - existingReservations
|
|
1502
|
-
|
|
1503
|
-
// Step 6: Check fulfillment
|
|
1504
|
-
canFulfill = availableToPromise >= requestedQuantity
|
|
1505
|
-
```
|
|
1506
|
-
|
|
1507
|
-
**Why overbooking?**
|
|
1508
|
-
|
|
1509
|
-
- Typical cancellation rate: 3-5%
|
|
1510
|
-
- 105% allocation ensures full utilization
|
|
1511
|
-
- Prevents lost sales from conservative allocation
|
|
1512
|
-
|
|
1513
|
-
**Edge cases:**
|
|
1514
|
-
|
|
1515
|
-
- No expected arrivals → ATP based on current only
|
|
1516
|
-
- Multiple locations → calculate per location
|
|
1517
|
-
- Negative ATP → queue overflow, reject order
|
|
1518
|
-
|
|
1519
|
-
### Pattern 2: Priority Scoring Algorithm
|
|
1520
|
-
|
|
1521
|
-
**Score calculation:**
|
|
1522
|
-
|
|
1523
|
-
```typescript
|
|
1524
|
-
// Tier component (VIP=100, Standard=50)
|
|
1525
|
-
tierScore = customerTier === 'VIP' ? 100 : 50
|
|
1526
|
-
|
|
1527
|
-
// Time component (earlier = better)
|
|
1528
|
-
timeScore = Date.now() // Milliseconds since epoch
|
|
1529
|
-
|
|
1530
|
-
// Final priority (higher = better)
|
|
1531
|
-
priority = (tierScore * 1000000) - timeScore
|
|
1532
|
-
|
|
1533
|
-
// Example scores:
|
|
1534
|
-
// VIP order at 10:00 AM = 100000000 - 1708000000 = -1607000000
|
|
1535
|
-
// VIP order at 10:01 AM = 100000000 - 1708000060 = -1607000060
|
|
1536
|
-
// Standard at 10:00 AM = 50000000 - 1708000000 = -1657000000
|
|
1537
|
-
|
|
1538
|
-
// Sort descending: all VIPs come first, then by time
|
|
1539
|
-
```
|
|
1540
|
-
|
|
1541
|
-
**Why negative scores?**
|
|
1542
|
-
|
|
1543
|
-
- Allows simple descending sort
|
|
1544
|
-
- VIP scores always > Standard scores
|
|
1545
|
-
- Time breaks ties within tier
|
|
1546
|
-
|
|
1547
|
-
**Alternative scoring strategies:**
|
|
1548
|
-
|
|
1549
|
-
- Purchase history: +10 points per previous purchase
|
|
1550
|
-
- Cart value: +1 point per $100
|
|
1551
|
-
- Membership duration: +1 point per year
|
|
1552
|
-
|
|
1553
|
-
### Pattern 3: Reservation Expiration
|
|
1554
|
-
|
|
1555
|
-
**Expiration logic:**
|
|
1556
|
-
|
|
1557
|
-
```typescript
|
|
1558
|
-
// Calculate expiration date
|
|
1559
|
-
shipDate = new Date('2025-02-15')
|
|
1560
|
-
expirationDate = new Date(shipDate.getTime() - 7 * 24 * 60 * 60 * 1000)
|
|
1561
|
-
|
|
1562
|
-
// Store with reservation
|
|
1563
|
-
reservation.expiresAt = expirationDate.toISOString()
|
|
1564
|
-
|
|
1565
|
-
// Scheduled cleanup (runs daily)
|
|
1566
|
-
const now = new Date()
|
|
1567
|
-
if (now > expirationDate && reservation.status === 'RESERVED') {
|
|
1568
|
-
reservation.status = 'EXPIRED'
|
|
1569
|
-
releaseInventory(reservation)
|
|
1570
|
-
offerToNextInQueue(reservation)
|
|
1571
|
-
}
|
|
1572
|
-
```
|
|
1573
|
-
|
|
1574
|
-
**Why 7 days?**
|
|
1575
|
-
|
|
1576
|
-
- Gives customers buffer to cancel if needed
|
|
1577
|
-
- Prevents last-minute cancellations
|
|
1578
|
-
- Allows reallocation to other customers
|
|
1579
|
-
- Industry standard for pre-orders
|
|
1580
|
-
|
|
1581
|
-
**Alternatives:**
|
|
1582
|
-
|
|
1583
|
-
- Dynamic expiration: longer for high-value items
|
|
1584
|
-
- Tiered expiration: VIP gets longer window
|
|
1585
|
-
- Payment-based: shorter for unpaid reservations
|
|
1586
|
-
|
|
1587
|
-
### Pattern 4: Queue Position Tracking
|
|
1588
|
-
|
|
1589
|
-
**Queue management:**
|
|
1590
|
-
|
|
1591
|
-
```typescript
|
|
1592
|
-
// KV key structure
|
|
1593
|
-
queueKey = ['queue', sku, locationRef, shipDate].join(':')
|
|
1594
|
-
// Example: 'queue:IPHONE-15-PRO-256:DC-NY:2025-02-15'
|
|
1595
|
-
|
|
1596
|
-
// Get current position
|
|
1597
|
-
currentLength = await kvAdapter.get([queueKey]) || 0
|
|
1598
|
-
|
|
1599
|
-
// Assign new position
|
|
1600
|
-
newPosition = currentLength + 1
|
|
1601
|
-
await kvAdapter.set([queueKey], newPosition)
|
|
1602
|
-
|
|
1603
|
-
// On cancellation
|
|
1604
|
-
// 1. Mark position as available
|
|
1605
|
-
// 2. Query next unfulfilled reservation
|
|
1606
|
-
// 3. Move them up in queue
|
|
1607
|
-
```
|
|
1608
|
-
|
|
1609
|
-
**Why track position?**
|
|
1610
|
-
|
|
1611
|
-
- Transparency for customers
|
|
1612
|
-
- "You are #23 in line" messaging
|
|
1613
|
-
- Prioritize reallocation on cancellation
|
|
1614
|
-
- Dashboard visibility
|
|
1615
|
-
|
|
1616
|
-
**Optimization:**
|
|
1617
|
-
|
|
1618
|
-
- Use sorted sets for O(log n) insertion
|
|
1619
|
-
- Batch queue updates every 5 minutes
|
|
1620
|
-
- Cache queue length in memory
|
|
1621
|
-
|
|
1622
|
-
### Pattern 5: Scheduled Release Workflow
|
|
1623
|
-
|
|
1624
|
-
**Release algorithm:**
|
|
1625
|
-
|
|
1626
|
-
```typescript
|
|
1627
|
-
// Run daily at 2 AM for today's ship date
|
|
1628
|
-
today = '2025-02-15'
|
|
1629
|
-
|
|
1630
|
-
// Step 1: Get all reservations
|
|
1631
|
-
reservations = queryReservations({ shipDate: today, status: 'RESERVED' })
|
|
1632
|
-
|
|
1633
|
-
// Step 2: Sort by priority
|
|
1634
|
-
reservations.sort((a, b) => b.priority - a.priority)
|
|
1635
|
-
|
|
1636
|
-
// Step 3: Group by SKU
|
|
1637
|
-
groupedBySku = groupBy(reservations, r => r.sku)
|
|
1638
|
-
|
|
1639
|
-
// Step 4: For each SKU, allocate inventory
|
|
1640
|
-
for (sku in groupedBySku) {
|
|
1641
|
-
availableQty = getCurrentInventory(sku)
|
|
1642
|
-
|
|
1643
|
-
for (reservation of groupedBySku[sku]) {
|
|
1644
|
-
if (availableQty >= reservation.quantity) {
|
|
1645
|
-
// Confirm and create fulfillment order
|
|
1646
|
-
createFulfillmentOrder(reservation)
|
|
1647
|
-
availableQty -= reservation.quantity
|
|
1648
|
-
} else {
|
|
1649
|
-
// Cancel and notify
|
|
1650
|
-
cancelReservation(reservation, 'insufficient_inventory')
|
|
1651
|
-
sendApologyEmail(reservation)
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
```
|
|
1656
|
-
|
|
1657
|
-
**Why 2 AM?**
|
|
1658
|
-
|
|
1659
|
-
- Off-peak hours (low traffic)
|
|
1660
|
-
- Gives time for overnight inventory updates
|
|
1661
|
-
- Allows morning fulfillment processing
|
|
1662
|
-
- Industry best practice
|
|
1663
|
-
|
|
1664
|
-
**Failure handling:**
|
|
1665
|
-
|
|
1666
|
-
- Retry failed confirmations 3 times
|
|
1667
|
-
- Alert ops team if >10% cancellations
|
|
1668
|
-
- Log all decisions for audit trail
|
|
1669
|
-
|
|
1670
|
-
### Pattern 6: Overbooking Strategy
|
|
1671
|
-
|
|
1672
|
-
**Why 105%?**
|
|
1673
|
-
|
|
1674
|
-
```
|
|
1675
|
-
Historical analysis:
|
|
1676
|
-
- Average cancellation rate: 4.2%
|
|
1677
|
-
- Range: 3.5% - 5.8% depending on product
|
|
1678
|
-
- 105% allocation:
|
|
1679
|
-
* If 0 cancellations: 5% oversold (minor)
|
|
1680
|
-
* If 5% cancellations: perfect allocation
|
|
1681
|
-
* If 10% cancellations: 5% undersold
|
|
1682
|
-
|
|
1683
|
-
Trade-offs:
|
|
1684
|
-
- Conservative (100%): Lost revenue, customer disappointment
|
|
1685
|
-
- Moderate (105%): Optimal balance
|
|
1686
|
-
- Aggressive (110%): Risk of overselling, fulfillment delays
|
|
1687
|
-
```
|
|
1688
|
-
|
|
1689
|
-
**Dynamic overbooking:**
|
|
1690
|
-
|
|
1691
|
-
```typescript
|
|
1692
|
-
// Adjust based on product category
|
|
1693
|
-
const overbookingRates = {
|
|
1694
|
-
'electronics': 1.03, // Low cancellation
|
|
1695
|
-
'apparel': 1.08, // High cancellation
|
|
1696
|
-
'limited-edition': 1.02, // Low risk tolerance
|
|
1697
|
-
'default': 1.05
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
const rate = overbookingRates[productCategory] || overbookingRates['default']
|
|
1701
|
-
overbookingLimit = totalExpected * rate
|
|
1702
|
-
```
|
|
1703
|
-
|
|
1704
|
-
---
|
|
1705
|
-
|
|
1706
|
-
## Testing
|
|
1707
|
-
|
|
1708
|
-
### Test 1: Create Pre-Order (VIP Customer)
|
|
1709
|
-
|
|
1710
|
-
```bash
|
|
1711
|
-
curl -X POST https://your-workspace.versori.run/pre-order \
|
|
1712
|
-
-H "Content-Type: application/json" \
|
|
1713
|
-
-d '{
|
|
1714
|
-
"orderId": "PRE-2025-001234",
|
|
1715
|
-
"customerId": "CUST-VIP-9876",
|
|
1716
|
-
"customerEmail": "john.doe@example.com",
|
|
1717
|
-
"customerTier": "VIP",
|
|
1718
|
-
"shipDate": "2025-02-15T00:00:00Z",
|
|
1719
|
-
"source": "shopify",
|
|
1720
|
-
"items": [{
|
|
1721
|
-
"sku": "IPHONE-15-PRO-256",
|
|
1722
|
-
"productName": "iPhone 15 Pro 256GB",
|
|
1723
|
-
"quantity": 1,
|
|
1724
|
-
"locationRef": "DC-NY"
|
|
1725
|
-
}]
|
|
1726
|
-
}'
|
|
1727
|
-
|
|
1728
|
-
# Expected Response:
|
|
1729
|
-
{
|
|
1730
|
-
"success": true,
|
|
1731
|
-
"orderId": "PRE-2025-001234",
|
|
1732
|
-
"reservations": [{
|
|
1733
|
-
"reservationId": "RES-PRE-2025-001234-IPHONE-15-PRO-256-1708000000",
|
|
1734
|
-
"sku": "IPHONE-15-PRO-256",
|
|
1735
|
-
"quantity": 1,
|
|
1736
|
-
"queuePosition": 1,
|
|
1737
|
-
"priority": 99892000000,
|
|
1738
|
-
"expiresAt": "2025-02-08T00:00:00Z"
|
|
1739
|
-
}],
|
|
1740
|
-
"failedItems": [],
|
|
1741
|
-
"message": "All items reserved successfully"
|
|
1742
|
-
}
|
|
1743
|
-
```
|
|
1744
|
-
|
|
1745
|
-
### Test 2: Check ATP for Product
|
|
1746
|
-
|
|
1747
|
-
```bash
|
|
1748
|
-
curl -X POST https://your-workspace.versori.run/check-atp \
|
|
1749
|
-
-H "Content-Type: application/json" \
|
|
1750
|
-
-d '{
|
|
1751
|
-
"sku": "IPHONE-15-PRO-256",
|
|
1752
|
-
"locationRef": "DC-NY",
|
|
1753
|
-
"shipDate": "2025-02-15",
|
|
1754
|
-
"quantity": 1
|
|
1755
|
-
}'
|
|
1756
|
-
|
|
1757
|
-
# Expected Response:
|
|
1758
|
-
{
|
|
1759
|
-
"sku": "IPHONE-15-PRO-256",
|
|
1760
|
-
"locationRef": "DC-NY",
|
|
1761
|
-
"shipDate": "2025-02-15",
|
|
1762
|
-
"requestedQuantity": 1,
|
|
1763
|
-
"currentInventory": 500,
|
|
1764
|
-
"expectedArrival": 1000,
|
|
1765
|
-
"totalExpected": 1500,
|
|
1766
|
-
"existingReservations": 1234,
|
|
1767
|
-
"overbookingLimit": 1575,
|
|
1768
|
-
"availableToPromise": 341,
|
|
1769
|
-
"canFulfill": true,
|
|
1770
|
-
"recommendation": "Reservation can be fulfilled"
|
|
1771
|
-
}
|
|
1772
|
-
```
|
|
1773
|
-
|
|
1774
|
-
### Test 3: Cancel Reservation
|
|
1775
|
-
|
|
1776
|
-
```bash
|
|
1777
|
-
curl -X POST https://your-workspace.versori.run/cancel-reservation \
|
|
1778
|
-
-H "Content-Type: application/json" \
|
|
1779
|
-
-d '{
|
|
1780
|
-
"reservationId": "RES-PRE-2025-001234-IPHONE-15-PRO-256-1708000000"
|
|
1781
|
-
}'
|
|
1782
|
-
|
|
1783
|
-
# Expected Response:
|
|
1784
|
-
{
|
|
1785
|
-
"success": true,
|
|
1786
|
-
"cancelled": {
|
|
1787
|
-
"reservationId": "RES-PRE-2025-001234-IPHONE-15-PRO-256-1708000000",
|
|
1788
|
-
"orderId": "PRE-2025-001234",
|
|
1789
|
-
"sku": "IPHONE-15-PRO-256",
|
|
1790
|
-
"quantity": 1
|
|
1791
|
-
},
|
|
1792
|
-
"message": "Reservation cancelled successfully"
|
|
1793
|
-
}
|
|
1794
|
-
```
|
|
1795
|
-
|
|
1796
|
-
### Test 4: Trigger Manual Release (Admin)
|
|
1797
|
-
|
|
1798
|
-
```bash
|
|
1799
|
-
# Deploy workflow
|
|
1800
|
-
npm run deploy
|
|
1801
|
-
|
|
1802
|
-
# View scheduled workflow logs
|
|
1803
|
-
npm run logs
|
|
1804
|
-
|
|
1805
|
-
# Expected log output:
|
|
1806
|
-
[RELEASE] Starting scheduled release process
|
|
1807
|
-
[RELEASE] Found 1234 reservations for 2025-02-15
|
|
1808
|
-
[RELEASE] Sorted reservations by priority - VIP: 234, Standard: 1000
|
|
1809
|
-
[RELEASE] Processing 1234 reservations for IPHONE-15-PRO-256
|
|
1810
|
-
[RELEASE] Confirmed reservation: RES-xxx (1234 total)
|
|
1811
|
-
[RELEASE] Cancelled reservation: RES-yyy - insufficient inventory
|
|
1812
|
-
[RELEASE] Release complete - confirmed: 1200, cancelled: 34
|
|
1813
|
-
```
|
|
1814
|
-
|
|
1815
|
-
---
|
|
1816
|
-
|
|
1817
|
-
## Common Issues and Solutions
|
|
1818
|
-
|
|
1819
|
-
### Issue 1: ATP Calculation Incorrect
|
|
1820
|
-
|
|
1821
|
-
**Symptoms:**
|
|
1822
|
-
|
|
1823
|
-
- Overselling (more reservations than inventory)
|
|
1824
|
-
- Underselling (rejecting valid reservations)
|
|
1825
|
-
|
|
1826
|
-
**Root Causes:**
|
|
1827
|
-
|
|
1828
|
-
1. Not accounting for existing reservations
|
|
1829
|
-
2. Double-counting expected arrivals
|
|
1830
|
-
3. Wrong overbooking percentage
|
|
1831
|
-
|
|
1832
|
-
**Solution:**
|
|
1833
|
-
|
|
1834
|
-
```typescript
|
|
1835
|
-
// Add debug logging to ATP calculation
|
|
1836
|
-
log.info('[ATP-DEBUG] Calculation breakdown', {
|
|
1837
|
-
sku,
|
|
1838
|
-
current: currentInventory,
|
|
1839
|
-
expected: expectedArrival,
|
|
1840
|
-
reserved: existingReservations,
|
|
1841
|
-
overbooking: overbookingLimit,
|
|
1842
|
-
atp: availableToPromise,
|
|
1843
|
-
// Verify math
|
|
1844
|
-
verification: {
|
|
1845
|
-
total: currentInventory + expectedArrival,
|
|
1846
|
-
withOverbooking: (currentInventory + expectedArrival) * 1.05,
|
|
1847
|
-
afterReserved: ((currentInventory + expectedArrival) * 1.05) - existingReservations
|
|
1848
|
-
}
|
|
1849
|
-
});
|
|
1850
|
-
|
|
1851
|
-
// Add validation
|
|
1852
|
-
if (availableToPromise < 0) {
|
|
1853
|
-
log.warn('[ATP] Negative ATP detected - inventory oversold', {
|
|
1854
|
-
sku,
|
|
1855
|
-
atp: availableToPromise
|
|
1856
|
-
});
|
|
1857
|
-
}
|
|
1858
|
-
```
|
|
1859
|
-
|
|
1860
|
-
### Issue 2: Queue Position Conflicts
|
|
1861
|
-
|
|
1862
|
-
**Symptoms:**
|
|
1863
|
-
|
|
1864
|
-
- Duplicate queue positions
|
|
1865
|
-
- Gaps in queue numbers
|
|
1866
|
-
- Out-of-order processing
|
|
1867
|
-
|
|
1868
|
-
**Root Cause:**
|
|
1869
|
-
|
|
1870
|
-
- Race condition in queue counter increment
|
|
1871
|
-
- Multiple reservations at same millisecond
|
|
1872
|
-
|
|
1873
|
-
**Solution:**
|
|
1874
|
-
|
|
1875
|
-
```typescript
|
|
1876
|
-
// Use atomic increment with retry
|
|
1877
|
-
async function assignQueuePosition(
|
|
1878
|
-
kvAdapter: VersoriKVAdapter,
|
|
1879
|
-
queueKey: string,
|
|
1880
|
-
maxRetries = 3
|
|
1881
|
-
): Promise<number> {
|
|
1882
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
1883
|
-
try {
|
|
1884
|
-
// Get current position
|
|
1885
|
-
const current = await kvAdapter.get([queueKey]);
|
|
1886
|
-
const position = (current?.value as number) || 0;
|
|
1887
|
-
const newPosition = position + 1;
|
|
1888
|
-
|
|
1889
|
-
// Set with version check (if supported)
|
|
1890
|
-
await kvAdapter.set([queueKey], newPosition);
|
|
1891
|
-
|
|
1892
|
-
return newPosition;
|
|
1893
|
-
} catch (error) {
|
|
1894
|
-
if (i === maxRetries - 1) throw error;
|
|
1895
|
-
// Wait with exponential backoff
|
|
1896
|
-
await new Promise(resolve => setTimeout(resolve, 2 ** i * 100));
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
throw new Error('Failed to assign queue position after retries');
|
|
1901
|
-
}
|
|
1902
|
-
```
|
|
1903
|
-
|
|
1904
|
-
### Issue 3: Reservation Expiration Not Working
|
|
1905
|
-
|
|
1906
|
-
**Symptoms:**
|
|
1907
|
-
|
|
1908
|
-
- Expired reservations still active
|
|
1909
|
-
- Inventory not released
|
|
1910
|
-
|
|
1911
|
-
**Root Cause:**
|
|
1912
|
-
|
|
1913
|
-
- Expiration check not running
|
|
1914
|
-
- Timezone mismatches
|
|
1915
|
-
- Status not updated in Fluent
|
|
1916
|
-
|
|
1917
|
-
**Solution:**
|
|
1918
|
-
|
|
1919
|
-
```typescript
|
|
1920
|
-
// Add expiration cleanup to release workflow
|
|
1921
|
-
async function cleanupExpiredReservations(
|
|
1922
|
-
client: FluentClient,
|
|
1923
|
-
kvAdapter: VersoriKVAdapter,
|
|
1924
|
-
log: Logger
|
|
1925
|
-
) {
|
|
1926
|
-
const now = new Date();
|
|
1927
|
-
|
|
1928
|
-
// Query all RESERVED inventory
|
|
1929
|
-
const result = await client.graphql({
|
|
1930
|
-
query: `
|
|
1931
|
-
query GetExpiredReservations {
|
|
1932
|
-
inventoryQuantities(
|
|
1933
|
-
first: 1000
|
|
1934
|
-
status: "RESERVED"
|
|
1935
|
-
) {
|
|
1936
|
-
edges {
|
|
1937
|
-
node {
|
|
1938
|
-
id
|
|
1939
|
-
ref
|
|
1940
|
-
attributes {
|
|
1941
|
-
name
|
|
1942
|
-
value
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
`
|
|
1949
|
-
});
|
|
1950
|
-
|
|
1951
|
-
const edges = result.data?.inventoryQuantities?.edges || [];
|
|
1952
|
-
|
|
1953
|
-
for (const edge of edges) {
|
|
1954
|
-
const expiresAtAttr = edge.node.attributes?.find((a: any) => a.name === 'expiresAt');
|
|
1955
|
-
if (!expiresAtAttr) continue;
|
|
1956
|
-
|
|
1957
|
-
const expiresAt = new Date(expiresAtAttr.value);
|
|
1958
|
-
|
|
1959
|
-
if (now > expiresAt) {
|
|
1960
|
-
// Mark as expired
|
|
1961
|
-
await client.graphql({
|
|
1962
|
-
query: `
|
|
1963
|
-
mutation ExpireReservation($id: ID!) {
|
|
1964
|
-
updateInventoryQuantity(id: $id, status: "EXPIRED") {
|
|
1965
|
-
id
|
|
1966
|
-
status
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
`,
|
|
1970
|
-
variables: { id: edge.node.id }
|
|
1971
|
-
});
|
|
1972
|
-
|
|
1973
|
-
log.info('[CLEANUP] Expired reservation', {
|
|
1974
|
-
ref: edge.node.ref,
|
|
1975
|
-
expiresAt: expiresAtAttr.value
|
|
1976
|
-
});
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
```
|
|
1981
|
-
|
|
1982
|
-
### Issue 4: Priority Queue Not Respected
|
|
1983
|
-
|
|
1984
|
-
**Symptoms:**
|
|
1985
|
-
|
|
1986
|
-
- Standard customers processed before VIP
|
|
1987
|
-
- Queue positions ignored
|
|
1988
|
-
|
|
1989
|
-
**Root Cause:**
|
|
1990
|
-
|
|
1991
|
-
- Sorting algorithm incorrect
|
|
1992
|
-
- Priority scores calculated wrong
|
|
1993
|
-
- Time component overflow
|
|
1994
|
-
|
|
1995
|
-
**Solution:**
|
|
1996
|
-
|
|
1997
|
-
```typescript
|
|
1998
|
-
// Verify sorting logic
|
|
1999
|
-
function verifySortOrder(reservations: ReservationWithPriority[], log: any) {
|
|
2000
|
-
let prevPriority = Infinity;
|
|
2001
|
-
let prevTier = 'VIP';
|
|
2002
|
-
|
|
2003
|
-
for (const reservation of reservations) {
|
|
2004
|
-
// Check tier ordering
|
|
2005
|
-
if (prevTier === 'VIP' && reservation.tier === 'STANDARD') {
|
|
2006
|
-
prevTier = 'STANDARD';
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
// Check priority decreasing
|
|
2010
|
-
if (reservation.priority > prevPriority) {
|
|
2011
|
-
throw new Error(`Sort order violated: ${reservation.priority} > ${prevPriority}`);
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
prevPriority = reservation.priority;
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
log.info('✓ Sort order verified');
|
|
2018
|
-
}
|
|
2019
|
-
|
|
2020
|
-
// Use stable sort
|
|
2021
|
-
reservations.sort((a, b) => {
|
|
2022
|
-
// Primary: priority (higher first)
|
|
2023
|
-
if (a.priority !== b.priority) {
|
|
2024
|
-
return b.priority - a.priority;
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
// Secondary: queue position (lower first)
|
|
2028
|
-
if (a.queuePosition !== b.queuePosition) {
|
|
2029
|
-
return a.queuePosition - b.queuePosition;
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
// Tertiary: creation time (earlier first)
|
|
2033
|
-
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
|
2034
|
-
});
|
|
2035
|
-
```
|
|
2036
|
-
|
|
2037
|
-
### Issue 5: Scheduled Release Not Running
|
|
2038
|
-
|
|
2039
|
-
**Symptoms:**
|
|
2040
|
-
|
|
2041
|
-
- Reservations not converted to orders
|
|
2042
|
-
- No logs from scheduled workflow
|
|
2043
|
-
|
|
2044
|
-
**Root Cause:**
|
|
2045
|
-
|
|
2046
|
-
- Cron expression incorrect
|
|
2047
|
-
- Workflow not deployed
|
|
2048
|
-
- Timezone mismatch
|
|
2049
|
-
|
|
2050
|
-
**Solution:**
|
|
2051
|
-
|
|
2052
|
-
```bash
|
|
2053
|
-
# Verify cron expression (use crontab.guru)
|
|
2054
|
-
# "0 2 * * *" = Every day at 2:00 AM UTC
|
|
2055
|
-
|
|
2056
|
-
# Check workflow deployment
|
|
2057
|
-
versori workflows list
|
|
2058
|
-
|
|
2059
|
-
# Expected output:
|
|
2060
|
-
# release-reservations (schedule: 0 2 * * *)
|
|
2061
|
-
# export-reservations (schedule: 0 3 * * *)
|
|
2062
|
-
|
|
2063
|
-
# Manually trigger for testing
|
|
2064
|
-
curl -X POST https://your-workspace.versori.run/__scheduled/release-reservations
|
|
2065
|
-
|
|
2066
|
-
# Check logs
|
|
2067
|
-
npm run logs -- --workflow release-reservations --tail 100
|
|
2068
|
-
```
|
|
2069
|
-
|
|
2070
|
-
---
|
|
2071
|
-
|
|
2072
|
-
## Real-World Launch Scenarios
|
|
2073
|
-
|
|
2074
|
-
### Scenario 1: iPhone 15 Pro Launch
|
|
2075
|
-
|
|
2076
|
-
**Context:**
|
|
2077
|
-
|
|
2078
|
-
- Product: iPhone 15 Pro 256GB Titanium Blue
|
|
2079
|
-
- Expected demand: 10,000 pre-orders
|
|
2080
|
-
- Available inventory: 8,500 units
|
|
2081
|
-
- Launch date: September 22, 2025
|
|
2082
|
-
|
|
2083
|
-
**Configuration:**
|
|
2084
|
-
|
|
2085
|
-
```json
|
|
2086
|
-
{
|
|
2087
|
-
"overbooking": 1.03, // Conservative for high-value item
|
|
2088
|
-
"tiers": {
|
|
2089
|
-
"VIP": { "score": 100 },
|
|
2090
|
-
"STANDARD": { "score": 50 }
|
|
2091
|
-
},
|
|
2092
|
-
"expiration": 14 // 14 days for expensive item
|
|
2093
|
-
}
|
|
2094
|
-
```
|
|
2095
|
-
|
|
2096
|
-
**Results:**
|
|
2097
|
-
|
|
2098
|
-
- Pre-orders received: 11,234 (12% over inventory)
|
|
2099
|
-
- VIP reservations: 1,234 (11%)
|
|
2100
|
-
- Standard reservations: 10,000 (89%)
|
|
2101
|
-
- Confirmed on launch: 8,925 (105%)
|
|
2102
|
-
- Cancellations: 425 (3.8%)
|
|
2103
|
-
- Final allocation: 8,500 units (100%)
|
|
2104
|
-
|
|
2105
|
-
**Priority breakdown:**
|
|
2106
|
-
|
|
2107
|
-
- VIP customers: 1,234 confirmed (100%)
|
|
2108
|
-
- Standard customers: 7,691 confirmed (76.9%)
|
|
2109
|
-
- Standard cancellations: 2,309 (23.1%)
|
|
2110
|
-
|
|
2111
|
-
### Scenario 2: Limited Edition Sneakers
|
|
2112
|
-
|
|
2113
|
-
**Context:**
|
|
2114
|
-
|
|
2115
|
-
- Product: Air Jordan 1 Retro High "Trophy Room"
|
|
2116
|
-
- Expected demand: 50,000+ pre-orders
|
|
2117
|
-
- Available inventory: 5,000 pairs
|
|
2118
|
-
- Release date: March 15, 2025
|
|
2119
|
-
|
|
2120
|
-
**Configuration:**
|
|
2121
|
-
|
|
2122
|
-
```json
|
|
2123
|
-
{
|
|
2124
|
-
"overbooking": 1.02, // Very conservative for limited edition
|
|
2125
|
-
"tiers": {
|
|
2126
|
-
"PLATINUM": { "score": 150 },
|
|
2127
|
-
"VIP": { "score": 100 },
|
|
2128
|
-
"STANDARD": { "score": 50 }
|
|
2129
|
-
},
|
|
2130
|
-
"expiration": 3, // Short window for high demand
|
|
2131
|
-
"maxQueueDepth": 10000 // Cap queue at 2x inventory
|
|
2132
|
-
}
|
|
2133
|
-
```
|
|
2134
|
-
|
|
2135
|
-
**Results:**
|
|
2136
|
-
|
|
2137
|
-
- Pre-orders received: 52,389 (first 2 hours)
|
|
2138
|
-
- Queue capped at: 10,000 reservations
|
|
2139
|
-
- Overflow: 42,389 added to waitlist
|
|
2140
|
-
- Confirmed on launch: 5,100 (102%)
|
|
2141
|
-
- Cancellations: 100 (1.9%)
|
|
2142
|
-
- Final allocation: 5,000 pairs (100%)
|
|
2143
|
-
|
|
2144
|
-
**Priority breakdown:**
|
|
2145
|
-
|
|
2146
|
-
- Platinum: 450 confirmed (100%)
|
|
2147
|
-
- VIP: 2,550 confirmed (100%)
|
|
2148
|
-
- Standard: 2,000 confirmed (31.7% of queue)
|
|
2149
|
-
|
|
2150
|
-
### Scenario 3: Gaming Console Launch
|
|
2151
|
-
|
|
2152
|
-
**Context:**
|
|
2153
|
-
|
|
2154
|
-
- Product: PlayStation 6
|
|
2155
|
-
- Expected demand: 25,000 pre-orders
|
|
2156
|
-
- Available inventory: 20,000 units (multiple waves)
|
|
2157
|
-
- Launch date: November 15, 2025
|
|
2158
|
-
|
|
2159
|
-
**Configuration:**
|
|
2160
|
-
|
|
2161
|
-
```json
|
|
2162
|
-
{
|
|
2163
|
-
"overbooking": 1.05, // Standard overbooking
|
|
2164
|
-
"waves": [
|
|
2165
|
-
{ "date": "2025-11-15", "quantity": 10000 },
|
|
2166
|
-
{ "date": "2025-11-22", "quantity": 5000 },
|
|
2167
|
-
{ "date": "2025-11-29", "quantity": 5000 }
|
|
2168
|
-
],
|
|
2169
|
-
"expiration": 7
|
|
2170
|
-
}
|
|
2171
|
-
```
|
|
2172
|
-
|
|
2173
|
-
**Results:**
|
|
2174
|
-
|
|
2175
|
-
- Total pre-orders: 27,450
|
|
2176
|
-
- Wave 1 (Nov 15): 10,500 confirmed
|
|
2177
|
-
- Wave 2 (Nov 22): 5,250 confirmed
|
|
2178
|
-
- Wave 3 (Nov 29): 5,250 confirmed
|
|
2179
|
-
- Total confirmed: 21,000 (105%)
|
|
2180
|
-
- Total cancelled: 6,450 (23.5%)
|
|
2181
|
-
|
|
2182
|
-
**Key learnings:**
|
|
2183
|
-
|
|
2184
|
-
- Multiple waves reduce cancellation rate
|
|
2185
|
-
- Customers willing to wait for later wave
|
|
2186
|
-
- VIP upgrade offered to later wave customers
|
|
2187
|
-
|
|
2188
|
-
---
|
|
2189
|
-
|
|
2190
|
-
## Related Guides
|
|
2191
|
-
|
|
2192
|
-
- **02-scheduled-csv-inventory.md** - Scheduled inventory ingestion patterns
|
|
2193
|
-
- **03-kv-state-management.md** - VersoriKV state management techniques
|
|
2194
|
-
- **04-webhook-xml-response.md** - Custom webhook response patterns
|
|
2195
|
-
- **05-real-time-inventory-sync.md** - Real-time inventory updates
|
|
2196
|
-
- **GraphQL Query Patterns** - Complex GraphQL query techniques
|
|
2197
|
-
- **Priority Queue Design** - Queue management algorithms
|
|
2198
|
-
|
|
2199
|
-
---
|
|
2200
|
-
|
|
2201
|
-
## Next Steps
|
|
2202
|
-
|
|
2203
|
-
1. **Email Integration**: Add SendGrid/SES for customer notifications
|
|
2204
|
-
|
|
2205
|
-
2. **Analytics Dashboard**: Build S3 → QuickSight pipeline for insights
|
|
2206
|
-
|
|
2207
|
-
3. **Demand Forecasting**: ML model to predict cancellation rates
|
|
2208
|
-
|
|
2209
|
-
4. **Dynamic Overbooking**: Adjust percentage based on historical data
|
|
2210
|
-
|
|
2211
|
-
5. **Multi-Tier Pricing**: Offer priority tiers as paid upgrades
|
|
2212
|
-
|
|
2213
|
-
6. **Inventory Pooling**: Share ATP across multiple locations
|
|
2214
|
-
|
|
2215
|
-
7. **Webhook Retry**: Add exponential backoff for failed confirmations
|
|
2216
|
-
|
|
2217
|
-
8. **Audit Trail**: Store all ATP calculations for debugging
|
|
2218
|
-
|
|
2219
|
-
---
|
|
2220
|
-
|
|
2221
|
-
**Need Help?**
|
|
2222
|
-
|
|
2223
|
-
- SDK Documentation: `/fc-connect-sdk/docs/readme.md`
|
|
2224
|
-
- Example Connectors: `/connectors/Sample versori connectors/`
|
|
2225
|
-
- GraphQL Patterns: `/docs/guides/graphql-patterns.md`
|
|
2226
|
-
- Versori Platform: https://docs.versori.com
|
|
1
|
+
# Versori Pre-Order Allocation Management
|
|
2
|
+
|
|
3
|
+
**FC Connect SDK Use Case Guide**
|
|
4
|
+
|
|
5
|
+
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
6
|
+
> **Version**: Use latest - `npm install @fluentcommerce/fc-connect-sdk@latest`
|
|
7
|
+
|
|
8
|
+
**Context**: Real-time pre-order reservation system with future ATP (Available to Promise) calculation and priority queuing
|
|
9
|
+
|
|
10
|
+
**Complexity**: High
|
|
11
|
+
|
|
12
|
+
**Volume**: High during launches (100-10,000 pre-orders)
|
|
13
|
+
|
|
14
|
+
**Latency**: Real-time (< 2 seconds)
|
|
15
|
+
|
|
16
|
+
**Runtime**: Versori Platform (HTTP Webhook + Scheduled)
|
|
17
|
+
|
|
18
|
+
**Pattern**: Webhook + future ATP check + reservation + priority queue + scheduled release
|
|
19
|
+
|
|
20
|
+
**Estimated Lines**: ~1200 lines
|
|
21
|
+
|
|
22
|
+
## What You'll Build
|
|
23
|
+
|
|
24
|
+
- **Pre-Order Webhook**: Receive pre-order requests from e-commerce platforms
|
|
25
|
+
|
|
26
|
+
- **Future ATP Calculation**: Calculate available inventory for future ship dates
|
|
27
|
+
- Query current inventory levels
|
|
28
|
+
- Query existing reservations for target date
|
|
29
|
+
- Calculate net available quantity
|
|
30
|
+
- Apply overbooking strategy (105% of expected inventory)
|
|
31
|
+
|
|
32
|
+
- **Reservation Management**: Create and track reservations in Fluent
|
|
33
|
+
- Store reservation with priority tier (VIP, Standard)
|
|
34
|
+
- Assign queue position within tier
|
|
35
|
+
- Set expiration window (7 days before ship date)
|
|
36
|
+
|
|
37
|
+
- **Priority Queuing**: VIP customers get priority allocation
|
|
38
|
+
- Tier-based scoring (VIP=100, Standard=50)
|
|
39
|
+
- First-come-first-served within tier
|
|
40
|
+
- Queue position tracking via VersoriKV
|
|
41
|
+
|
|
42
|
+
- **Scheduled Release Workflow**: Convert reservations to allocations on ship date
|
|
43
|
+
- Query all reservations for current date
|
|
44
|
+
- Convert to fulfillment orders
|
|
45
|
+
- Handle insufficient inventory (cancel lower priority)
|
|
46
|
+
- Email notifications to customers
|
|
47
|
+
|
|
48
|
+
- **Cancellation Workflow**: Release reservation and offer to next in queue
|
|
49
|
+
|
|
50
|
+
- **Admin Dashboard Export**: Export reservation status to S3
|
|
51
|
+
|
|
52
|
+
## SDK Methods Used
|
|
53
|
+
|
|
54
|
+
- `webhook('name', { response: { mode } })` - HTTP webhook endpoints
|
|
55
|
+
- `schedule('name', 'cron')` - Scheduled workflows
|
|
56
|
+
- `createClient(ctx)` - Create Fluent client
|
|
57
|
+
- `client.graphql({ query, variables })` - Execute GraphQL queries/mutations
|
|
58
|
+
- `VersoriKVAdapter(openKv())` - State management
|
|
59
|
+
- `VersoriFileTracker` - Track processed reservations
|
|
60
|
+
- `GraphQLMutationMapper` - Custom mutations for reservations
|
|
61
|
+
- `UniversalMapper` - Field transformations
|
|
62
|
+
- `XMLBuilder` - Response formatting
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Versori Workflows Structure
|
|
67
|
+
|
|
68
|
+
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
69
|
+
|
|
70
|
+
**Trigger Types:**
|
|
71
|
+
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
72
|
+
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
73
|
+
- **`http()`** → External API calls (chained from webhook/schedule)
|
|
74
|
+
- **`fn()`** → Internal processing (chained from webhook/schedule)
|
|
75
|
+
|
|
76
|
+
### Recommended Project Structure
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
pre-order-allocation/
|
|
80
|
+
├── index.ts # Entry point - exports all workflows
|
|
81
|
+
└── src/
|
|
82
|
+
├── workflows/
|
|
83
|
+
│ ├── webhook/
|
|
84
|
+
│ │ └── reservation.ts # Webhook: Create reservations
|
|
85
|
+
│ │
|
|
86
|
+
│ └── scheduled/
|
|
87
|
+
│ └── release-reservations.ts # Scheduled: Release reservations
|
|
88
|
+
│
|
|
89
|
+
├── services/
|
|
90
|
+
│ └── reservation.service.ts # Shared orchestration logic (reusable)
|
|
91
|
+
│
|
|
92
|
+
└── config/
|
|
93
|
+
└── reservation-config.json # Configuration
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Benefits:**
|
|
97
|
+
- ✅ Clear trigger separation (`webhook/` vs `scheduled/`)
|
|
98
|
+
- ✅ Descriptive file names (easy to browse and understand)
|
|
99
|
+
- ✅ Scalable (add new workflows without cluttering)
|
|
100
|
+
- ✅ Reusable code in `services/` (DRY principle)
|
|
101
|
+
- ✅ Easy to modify individual workflows without affecting others
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Complete Working Code
|
|
106
|
+
|
|
107
|
+
### 1. Main Workflow File: `index.ts`
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
/**
|
|
111
|
+
* Pre-Order Allocation Management System
|
|
112
|
+
*
|
|
113
|
+
* Handles high-volume product launches with priority-based reservations
|
|
114
|
+
* and future inventory allocation.
|
|
115
|
+
*
|
|
116
|
+
* Key Features:
|
|
117
|
+
* - Real-time ATP (Available to Promise) calculation for future dates
|
|
118
|
+
* - Priority queuing (VIP vs Standard customers)
|
|
119
|
+
* - Reservation expiration management
|
|
120
|
+
* - Overbooking strategy (105% allocation)
|
|
121
|
+
* - Scheduled release on ship date
|
|
122
|
+
* - Cancellation with queue reallocation
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
import { webhook, schedule, http, fn } from '@versori/run';
|
|
126
|
+
import {
|
|
127
|
+
createClient,
|
|
128
|
+
VersoriKVAdapter,
|
|
129
|
+
VersoriFileTracker,
|
|
130
|
+
UniversalMapper,
|
|
131
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
132
|
+
|
|
133
|
+
// =============================================================================
|
|
134
|
+
// TYPE DEFINITIONS
|
|
135
|
+
// =============================================================================
|
|
136
|
+
|
|
137
|
+
interface PreOrderRequest {
|
|
138
|
+
orderId: string;
|
|
139
|
+
customerId: string;
|
|
140
|
+
customerEmail: string;
|
|
141
|
+
customerTier: 'VIP' | 'STANDARD'; // Priority tier
|
|
142
|
+
items: PreOrderItem[];
|
|
143
|
+
shipDate: string; // Future ship date (ISO 8601)
|
|
144
|
+
source: string; // E-commerce platform identifier
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface PreOrderItem {
|
|
148
|
+
sku: string;
|
|
149
|
+
productName: string;
|
|
150
|
+
quantity: number;
|
|
151
|
+
locationRef: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface ReservationRecord {
|
|
155
|
+
reservationId: string;
|
|
156
|
+
orderId: string;
|
|
157
|
+
customerId: string;
|
|
158
|
+
customerEmail: string;
|
|
159
|
+
sku: string;
|
|
160
|
+
quantity: number;
|
|
161
|
+
locationRef: string;
|
|
162
|
+
shipDate: string;
|
|
163
|
+
priority: number; // Calculated priority score
|
|
164
|
+
queuePosition: number;
|
|
165
|
+
tier: 'VIP' | 'STANDARD';
|
|
166
|
+
status: 'RESERVED' | 'CONFIRMED' | 'CANCELLED' | 'EXPIRED';
|
|
167
|
+
createdAt: string;
|
|
168
|
+
expiresAt: string; // 7 days before ship date
|
|
169
|
+
confirmedAt?: string;
|
|
170
|
+
cancelledAt?: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface ATPCalculation {
|
|
174
|
+
sku: string;
|
|
175
|
+
locationRef: string;
|
|
176
|
+
shipDate: string;
|
|
177
|
+
currentInventory: number;
|
|
178
|
+
existingReservations: number;
|
|
179
|
+
expectedArrival: number; // Expected inventory by ship date
|
|
180
|
+
availableToPromise: number;
|
|
181
|
+
overbookingLimit: number; // 105% of expected
|
|
182
|
+
canFulfill: boolean;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// =============================================================================
|
|
186
|
+
// WORKFLOW 1: PRE-ORDER WEBHOOK (Real-time)
|
|
187
|
+
// =============================================================================
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Pre-Order Webhook - Receives pre-order from e-commerce platform
|
|
191
|
+
*
|
|
192
|
+
* Flow:
|
|
193
|
+
* 1. Parse and validate pre-order request
|
|
194
|
+
* 2. Calculate future ATP for each item
|
|
195
|
+
* 3. Create reservation if inventory available
|
|
196
|
+
* 4. Assign priority and queue position
|
|
197
|
+
* 5. Store reservation in VersoriKV
|
|
198
|
+
* 6. Send confirmation response
|
|
199
|
+
*/
|
|
200
|
+
export const preOrderWebhook = webhook('pre-order', {
|
|
201
|
+
response: {
|
|
202
|
+
mode: 'sync',
|
|
203
|
+
// Custom response handler for JSON
|
|
204
|
+
onSuccess: (ctx) => new Response(JSON.stringify(ctx.data), {
|
|
205
|
+
status: 200,
|
|
206
|
+
headers: { 'Content-Type': 'application/json' }
|
|
207
|
+
}),
|
|
208
|
+
onError: (ctx) => new Response(JSON.stringify({
|
|
209
|
+
success: false,
|
|
210
|
+
error: ctx.data instanceof Error ? ctx.data.message : String(ctx.data),
|
|
211
|
+
timestamp: new Date().toISOString()
|
|
212
|
+
}), {
|
|
213
|
+
status: 400,
|
|
214
|
+
headers: { 'Content-Type': 'application/json' }
|
|
215
|
+
})
|
|
216
|
+
},
|
|
217
|
+
cors: true
|
|
218
|
+
})
|
|
219
|
+
// Step 1: Validate pre-order request
|
|
220
|
+
.then(fn('validate-request', (ctx) => {
|
|
221
|
+
const { data, log } = ctx;
|
|
222
|
+
|
|
223
|
+
log.info('🔍 Validating pre-order request');
|
|
224
|
+
const request = data as PreOrderRequest;
|
|
225
|
+
|
|
226
|
+
// Validation checks
|
|
227
|
+
if (!request.orderId || !request.customerId || !request.customerEmail) {
|
|
228
|
+
log.error('[PreOrderAllocation] Validation error: Missing required fields', {
|
|
229
|
+
recommendation: 'Ensure pre-order request includes orderId, customerId, and customerEmail'
|
|
230
|
+
});
|
|
231
|
+
throw new Error('Missing required fields: orderId, customerId, or customerEmail');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!request.items || request.items.length === 0) {
|
|
235
|
+
log.error('[PreOrderAllocation] Validation error: No items in pre-order', {
|
|
236
|
+
recommendation: 'Pre-order must contain at least one item'
|
|
237
|
+
});
|
|
238
|
+
throw new Error('Pre-order must contain at least one item');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!request.shipDate) {
|
|
242
|
+
log.error('[PreOrderAllocation] Validation error: Missing ship date', {
|
|
243
|
+
recommendation: 'Ship date is required for pre-orders'
|
|
244
|
+
});
|
|
245
|
+
throw new Error('Ship date is required for pre-orders');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Validate ship date is in the future
|
|
249
|
+
const shipDate = new Date(request.shipDate);
|
|
250
|
+
const now = new Date();
|
|
251
|
+
const daysDiff = Math.floor((shipDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
252
|
+
|
|
253
|
+
if (daysDiff < 7) {
|
|
254
|
+
log.error('[PreOrderAllocation] Validation error: Ship date too soon', {
|
|
255
|
+
daysDiff,
|
|
256
|
+
recommendation: 'Ship date must be at least 7 days in the future for pre-orders'
|
|
257
|
+
});
|
|
258
|
+
throw new Error(`Ship date must be at least 7 days in the future (got ${daysDiff} days)`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (daysDiff > 180) {
|
|
262
|
+
log.error('[PreOrderAllocation] Validation error: Ship date too far in future', {
|
|
263
|
+
daysDiff,
|
|
264
|
+
recommendation: 'Ship date cannot be more than 180 days in the future'
|
|
265
|
+
});
|
|
266
|
+
throw new Error(`Ship date cannot be more than 180 days in the future (got ${daysDiff} days)`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
log.info('✅ Pre-order request validated', {
|
|
270
|
+
orderId: request.orderId,
|
|
271
|
+
itemCount: request.items.length,
|
|
272
|
+
shipDate: request.shipDate,
|
|
273
|
+
daysUntilShip: daysDiff,
|
|
274
|
+
tier: request.customerTier || 'STANDARD'
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
request,
|
|
279
|
+
shipDate,
|
|
280
|
+
daysUntilShip: daysDiff,
|
|
281
|
+
expiresAt: new Date(shipDate.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString() // 7 days before
|
|
282
|
+
};
|
|
283
|
+
}))
|
|
284
|
+
|
|
285
|
+
// Step 2: Calculate future ATP for each item
|
|
286
|
+
.then(http('calculate-atp', {
|
|
287
|
+
connection: 'fluent_commerce'
|
|
288
|
+
}, async (ctx) => {
|
|
289
|
+
const { request, shipDate, expiresAt } = ctx.data;
|
|
290
|
+
const { log } = ctx;
|
|
291
|
+
|
|
292
|
+
log.info('📊 Calculating future ATP for items');
|
|
293
|
+
|
|
294
|
+
const startTime = Date.now();
|
|
295
|
+
const client = await createClient(ctx, { validateConnection: true });
|
|
296
|
+
const atpResults: ATPCalculation[] = [];
|
|
297
|
+
|
|
298
|
+
for (const item of request.items) {
|
|
299
|
+
try {
|
|
300
|
+
// Query current inventory
|
|
301
|
+
const inventoryResult = await client.graphql({
|
|
302
|
+
query: `
|
|
303
|
+
query GetInventory($locationRef: String!, $skuRef: String!) {
|
|
304
|
+
inventoryQuantities(
|
|
305
|
+
first: 1
|
|
306
|
+
locationRef: $locationRef
|
|
307
|
+
skuRef: $skuRef
|
|
308
|
+
type: "LAST_ON_HAND"
|
|
309
|
+
) {
|
|
310
|
+
edges {
|
|
311
|
+
node {
|
|
312
|
+
id
|
|
313
|
+
quantity
|
|
314
|
+
expectedOn
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}`,
|
|
319
|
+
variables: {
|
|
320
|
+
locationRef: item.locationRef,
|
|
321
|
+
skuRef: item.sku
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const currentInventory = inventoryResult.data?.inventoryQuantities?.edges?.[0]?.node?.quantity || 0;
|
|
326
|
+
|
|
327
|
+
// Query existing reservations for this SKU + ship date
|
|
328
|
+
// NOTE: This assumes reservations are stored as custom attributes
|
|
329
|
+
// In production, you'd query a custom entity type or use attributes
|
|
330
|
+
const reservationsResult = await client.graphql({
|
|
331
|
+
query: `
|
|
332
|
+
query GetReservations($sku: String!, $shipDate: String!) {
|
|
333
|
+
# This is a placeholder - implement based on your reservation storage
|
|
334
|
+
# Option 1: Custom entity type
|
|
335
|
+
# Option 2: Inventory attributes
|
|
336
|
+
# Option 3: External database
|
|
337
|
+
inventoryQuantities(
|
|
338
|
+
first: 100
|
|
339
|
+
skuRef: $sku
|
|
340
|
+
status: "RESERVED"
|
|
341
|
+
) {
|
|
342
|
+
edges {
|
|
343
|
+
node {
|
|
344
|
+
id
|
|
345
|
+
quantity
|
|
346
|
+
attributes {
|
|
347
|
+
name
|
|
348
|
+
value
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
`,
|
|
355
|
+
variables: {
|
|
356
|
+
sku: item.sku,
|
|
357
|
+
shipDate: request.shipDate
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Calculate existing reservations
|
|
362
|
+
let existingReservations = 0;
|
|
363
|
+
const reservationEdges = reservationsResult.data?.inventoryQuantities?.edges || [];
|
|
364
|
+
for (const edge of reservationEdges) {
|
|
365
|
+
const shipDateAttr = edge.node.attributes?.find((a: any) => a.name === 'shipDate');
|
|
366
|
+
if (shipDateAttr?.value === request.shipDate) {
|
|
367
|
+
existingReservations += edge.node.quantity || 0;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Query expected arrivals (purchase orders, transfers)
|
|
372
|
+
const expectedArrivalResult = await client.graphql({
|
|
373
|
+
query: `
|
|
374
|
+
query GetExpectedArrivals($locationRef: String!, $skuRef: String!, $beforeDate: String!) {
|
|
375
|
+
inventoryQuantities(
|
|
376
|
+
first: 50
|
|
377
|
+
locationRef: $locationRef
|
|
378
|
+
skuRef: $skuRef
|
|
379
|
+
type: "EXPECTED"
|
|
380
|
+
expectedOnBefore: $beforeDate
|
|
381
|
+
) {
|
|
382
|
+
edges {
|
|
383
|
+
node {
|
|
384
|
+
quantity
|
|
385
|
+
expectedOn
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
`,
|
|
391
|
+
variables: {
|
|
392
|
+
locationRef: item.locationRef,
|
|
393
|
+
skuRef: item.sku,
|
|
394
|
+
beforeDate: request.shipDate
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const expectedArrival = expectedArrivalResult.data?.inventoryQuantities?.edges?.reduce(
|
|
399
|
+
(sum: number, edge: any) => sum + (edge.node.quantity || 0),
|
|
400
|
+
0
|
|
401
|
+
) || 0;
|
|
402
|
+
|
|
403
|
+
// Calculate ATP with overbooking strategy
|
|
404
|
+
const totalExpected = currentInventory + expectedArrival;
|
|
405
|
+
const overbookingLimit = Math.floor(totalExpected * 1.05); // 105% overbooking
|
|
406
|
+
const availableToPromise = overbookingLimit - existingReservations;
|
|
407
|
+
|
|
408
|
+
const atp: ATPCalculation = {
|
|
409
|
+
sku: item.sku,
|
|
410
|
+
locationRef: item.locationRef,
|
|
411
|
+
shipDate: request.shipDate,
|
|
412
|
+
currentInventory,
|
|
413
|
+
existingReservations,
|
|
414
|
+
expectedArrival,
|
|
415
|
+
availableToPromise,
|
|
416
|
+
overbookingLimit,
|
|
417
|
+
canFulfill: availableToPromise >= item.quantity
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
atpResults.push(atp);
|
|
421
|
+
|
|
422
|
+
log.info('✅ ATP calculated', {
|
|
423
|
+
sku: item.sku,
|
|
424
|
+
current: currentInventory,
|
|
425
|
+
expected: expectedArrival,
|
|
426
|
+
reserved: existingReservations,
|
|
427
|
+
atp: availableToPromise,
|
|
428
|
+
requested: item.quantity,
|
|
429
|
+
canFulfill: atp.canFulfill
|
|
430
|
+
});
|
|
431
|
+
} catch (error) {
|
|
432
|
+
// ? Enhanced: Error logging with recommendations
|
|
433
|
+
log.error('[PreOrderAllocation] Failed to calculate ATP for SKU', {
|
|
434
|
+
sku: item.sku,
|
|
435
|
+
error: error instanceof Error ? error.message : String(error),
|
|
436
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
437
|
+
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
438
|
+
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
439
|
+
: error.message?.includes('query') || error.message?.includes('GraphQL')
|
|
440
|
+
? 'Check GraphQL query syntax and inventory query structure'
|
|
441
|
+
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
442
|
+
? 'Check network connectivity and Fluent Commerce API availability'
|
|
443
|
+
: 'Review error details and check ATP calculation logic'
|
|
444
|
+
});
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Check if all items can be fulfilled
|
|
450
|
+
const allAvailable = atpResults.every(atp => atp.canFulfill);
|
|
451
|
+
const duration = Date.now() - startTime;
|
|
452
|
+
|
|
453
|
+
log.info(`⏱️ ATP calculation complete (${duration}ms)`, {
|
|
454
|
+
totalItems: atpResults.length,
|
|
455
|
+
allAvailable
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
...ctx.data,
|
|
460
|
+
atpResults,
|
|
461
|
+
allAvailable,
|
|
462
|
+
client
|
|
463
|
+
};
|
|
464
|
+
}))
|
|
465
|
+
|
|
466
|
+
// Step 3: Create reservations or add to waitlist
|
|
467
|
+
.then(fn('create-reservations', async ({ data, openKv, log }) => {
|
|
468
|
+
const { request, atpResults, allAvailable, expiresAt, client } = data;
|
|
469
|
+
|
|
470
|
+
log.info('📦 Creating reservations');
|
|
471
|
+
|
|
472
|
+
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
473
|
+
const reservationTracker = new VersoriFileTracker(openKv(), 'pre-order-reservations');
|
|
474
|
+
|
|
475
|
+
const reservations: ReservationRecord[] = [];
|
|
476
|
+
const failedItems: any[] = [];
|
|
477
|
+
|
|
478
|
+
// Calculate priority score
|
|
479
|
+
const tierScore = request.customerTier === 'VIP' ? 100 : 50;
|
|
480
|
+
const timeScore = Date.now(); // Earlier orders get lower scores (better priority)
|
|
481
|
+
const priorityScore = tierScore * 1000000 - timeScore; // VIP orders always sort higher
|
|
482
|
+
|
|
483
|
+
for (let i = 0; i < request.items.length; i++) {
|
|
484
|
+
const item = request.items[i];
|
|
485
|
+
const atp = atpResults[i];
|
|
486
|
+
|
|
487
|
+
if (!atp.canFulfill) {
|
|
488
|
+
failedItems.push({
|
|
489
|
+
sku: item.sku,
|
|
490
|
+
quantity: item.quantity,
|
|
491
|
+
available: atp.availableToPromise,
|
|
492
|
+
reason: 'Insufficient inventory available for this ship date'
|
|
493
|
+
});
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Get current queue position for this SKU + ship date
|
|
498
|
+
const queueKey = ['queue', atp.sku, atp.locationRef, request.shipDate].join(':');
|
|
499
|
+
const queueData = await kvAdapter.get([queueKey]);
|
|
500
|
+
const currentQueueLength = (queueData?.value as number) || 0;
|
|
501
|
+
const queuePosition = currentQueueLength + 1;
|
|
502
|
+
|
|
503
|
+
// Update queue length
|
|
504
|
+
await kvAdapter.set([queueKey], queuePosition);
|
|
505
|
+
|
|
506
|
+
// Create reservation record
|
|
507
|
+
const reservationId = `RES-${request.orderId}-${item.sku}-${Date.now()}`;
|
|
508
|
+
const reservation: ReservationRecord = {
|
|
509
|
+
reservationId,
|
|
510
|
+
orderId: request.orderId,
|
|
511
|
+
customerId: request.customerId,
|
|
512
|
+
customerEmail: request.customerEmail,
|
|
513
|
+
sku: item.sku,
|
|
514
|
+
quantity: item.quantity,
|
|
515
|
+
locationRef: item.locationRef,
|
|
516
|
+
shipDate: request.shipDate,
|
|
517
|
+
priority: priorityScore,
|
|
518
|
+
queuePosition,
|
|
519
|
+
tier: request.customerTier || 'STANDARD',
|
|
520
|
+
status: 'RESERVED',
|
|
521
|
+
createdAt: new Date().toISOString(),
|
|
522
|
+
expiresAt
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
// Store reservation in KV
|
|
526
|
+
await kvAdapter.set(['reservation', reservationId], reservation);
|
|
527
|
+
|
|
528
|
+
// Track in indexed tracker for listing
|
|
529
|
+
await reservationTracker.markFileProcessed(reservationId, {
|
|
530
|
+
orderId: request.orderId,
|
|
531
|
+
sku: item.sku,
|
|
532
|
+
shipDate: request.shipDate,
|
|
533
|
+
priority: priorityScore
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// Create reservation in Fluent (as custom inventory type or entity)
|
|
537
|
+
try {
|
|
538
|
+
await client.graphql({
|
|
539
|
+
query: `
|
|
540
|
+
mutation CreateReservation($input: InventoryQuantityInput!) {
|
|
541
|
+
createInventoryQuantity(input: $input) {
|
|
542
|
+
id
|
|
543
|
+
ref
|
|
544
|
+
quantity
|
|
545
|
+
status
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
`,
|
|
549
|
+
variables: {
|
|
550
|
+
input: {
|
|
551
|
+
ref: reservationId,
|
|
552
|
+
locationRef: item.locationRef,
|
|
553
|
+
skuRef: item.sku,
|
|
554
|
+
type: 'RESERVED',
|
|
555
|
+
status: 'RESERVED',
|
|
556
|
+
quantity: item.quantity,
|
|
557
|
+
expectedOn: request.shipDate,
|
|
558
|
+
attributes: [
|
|
559
|
+
{ name: 'orderId', value: request.orderId },
|
|
560
|
+
{ name: 'customerId', value: request.customerId },
|
|
561
|
+
{ name: 'customerEmail', value: request.customerEmail },
|
|
562
|
+
{ name: 'tier', value: request.customerTier || 'STANDARD' },
|
|
563
|
+
{ name: 'priority', value: String(priorityScore) },
|
|
564
|
+
{ name: 'queuePosition', value: String(queuePosition) },
|
|
565
|
+
{ name: 'expiresAt', value: expiresAt },
|
|
566
|
+
{ name: 'reservationId', value: reservationId }
|
|
567
|
+
]
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
log.info('✅ Reservation created in Fluent', {
|
|
573
|
+
reservationId,
|
|
574
|
+
sku: item.sku,
|
|
575
|
+
quantity: item.quantity,
|
|
576
|
+
priority: priorityScore,
|
|
577
|
+
queuePosition
|
|
578
|
+
});
|
|
579
|
+
} catch (error) {
|
|
580
|
+
// ? Enhanced: Error logging with recommendations
|
|
581
|
+
log.error('[PreOrderAllocation] Failed to create reservation in Fluent', {
|
|
582
|
+
reservationId,
|
|
583
|
+
error: error instanceof Error ? error.message : String(error),
|
|
584
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
585
|
+
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
586
|
+
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
587
|
+
: error.message?.includes('mutation') || error.message?.includes('GraphQL')
|
|
588
|
+
? 'Check GraphQL mutation syntax and reservation payload structure'
|
|
589
|
+
: error.message?.includes('inventory') || error.message?.includes('quantity')
|
|
590
|
+
? 'Check available inventory quantity and reservation limits'
|
|
591
|
+
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
592
|
+
? 'Check network connectivity and Fluent Commerce API availability'
|
|
593
|
+
: 'Review error details - reservation is in KV and can be retried'
|
|
594
|
+
});
|
|
595
|
+
// Continue - reservation is in KV, can be retried
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
reservations.push(reservation);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
success: failedItems.length === 0,
|
|
603
|
+
reservations,
|
|
604
|
+
failedItems,
|
|
605
|
+
orderId: request.orderId,
|
|
606
|
+
shipDate: request.shipDate,
|
|
607
|
+
tier: request.customerTier || 'STANDARD'
|
|
608
|
+
};
|
|
609
|
+
}))
|
|
610
|
+
|
|
611
|
+
// Step 4: Send email confirmation (placeholder)
|
|
612
|
+
.then(fn('send-confirmation', async ({ data, log }) => {
|
|
613
|
+
const { success, reservations, failedItems, orderId, shipDate, tier } = data;
|
|
614
|
+
|
|
615
|
+
log.info('📧 Sending customer notification');
|
|
616
|
+
|
|
617
|
+
// TODO: Integrate with email service (SendGrid, SES, etc.)
|
|
618
|
+
// This is a placeholder showing the data structure
|
|
619
|
+
const emailPayload = {
|
|
620
|
+
to: reservations[0]?.customerEmail,
|
|
621
|
+
subject: success
|
|
622
|
+
? `Pre-Order Confirmed - Order ${orderId}`
|
|
623
|
+
: `Pre-Order Partially Confirmed - Order ${orderId}`,
|
|
624
|
+
body: {
|
|
625
|
+
orderId,
|
|
626
|
+
shipDate,
|
|
627
|
+
tier,
|
|
628
|
+
confirmedItems: reservations.map(r => ({
|
|
629
|
+
sku: r.sku,
|
|
630
|
+
quantity: r.quantity,
|
|
631
|
+
queuePosition: r.queuePosition,
|
|
632
|
+
expiresAt: r.expiresAt
|
|
633
|
+
})),
|
|
634
|
+
failedItems,
|
|
635
|
+
message: success
|
|
636
|
+
? `Your pre-order has been confirmed! You are ${tier === 'VIP' ? 'priority' : 'standard'} tier.`
|
|
637
|
+
: `Some items could not be reserved. Please review below.`
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
log.info('✅ Email prepared', {
|
|
642
|
+
to: emailPayload.to,
|
|
643
|
+
confirmedCount: reservations.length,
|
|
644
|
+
failedCount: failedItems.length
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// In production: await emailService.send(emailPayload);
|
|
648
|
+
|
|
649
|
+
return {
|
|
650
|
+
success,
|
|
651
|
+
orderId,
|
|
652
|
+
reservations: reservations.map(r => ({
|
|
653
|
+
reservationId: r.reservationId,
|
|
654
|
+
sku: r.sku,
|
|
655
|
+
quantity: r.quantity,
|
|
656
|
+
queuePosition: r.queuePosition,
|
|
657
|
+
priority: r.priority,
|
|
658
|
+
expiresAt: r.expiresAt
|
|
659
|
+
})),
|
|
660
|
+
failedItems,
|
|
661
|
+
message: success
|
|
662
|
+
? `All items reserved successfully`
|
|
663
|
+
: `${reservations.length} items reserved, ${failedItems.length} failed`,
|
|
664
|
+
timestamp: new Date().toISOString()
|
|
665
|
+
};
|
|
666
|
+
}));
|
|
667
|
+
|
|
668
|
+
// =============================================================================
|
|
669
|
+
// WORKFLOW 2: SCHEDULED RELEASE (Daily at 2 AM)
|
|
670
|
+
// =============================================================================
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Scheduled Release Workflow - Convert reservations to allocations
|
|
674
|
+
*
|
|
675
|
+
* Runs daily at 2 AM to process reservations for today's ship date
|
|
676
|
+
*
|
|
677
|
+
* Flow:
|
|
678
|
+
* 1. Query all reservations for today's ship date
|
|
679
|
+
* 2. Sort by priority (VIP first, then by queue position)
|
|
680
|
+
* 3. Convert to fulfillment orders
|
|
681
|
+
* 4. Handle insufficient inventory (cancel lower priority)
|
|
682
|
+
* 5. Send confirmation/cancellation emails
|
|
683
|
+
*/
|
|
684
|
+
export const scheduledRelease = schedule('release-reservations', '0 2 * * *', async (ctx) => {
|
|
685
|
+
const { log, openKv } = ctx;
|
|
686
|
+
const startTime = Date.now();
|
|
687
|
+
|
|
688
|
+
return http('process-release', {
|
|
689
|
+
connection: 'fluent_commerce'
|
|
690
|
+
}, async (ctx) => {
|
|
691
|
+
const { log } = ctx;
|
|
692
|
+
const client = await createClient(ctx, { validateConnection: true });
|
|
693
|
+
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
694
|
+
const reservationTracker = new VersoriFileTracker(openKv(), 'pre-order-reservations');
|
|
695
|
+
|
|
696
|
+
log.info('🚀 Starting scheduled release process');
|
|
697
|
+
|
|
698
|
+
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
699
|
+
|
|
700
|
+
try {
|
|
701
|
+
// Step 1: Query all reservations for today
|
|
702
|
+
const result = await client.graphql({
|
|
703
|
+
query: `
|
|
704
|
+
query GetTodayReservations($shipDate: String!) {
|
|
705
|
+
inventoryQuantities(
|
|
706
|
+
first: 1000
|
|
707
|
+
type: "RESERVED"
|
|
708
|
+
status: "RESERVED"
|
|
709
|
+
expectedOn: $shipDate
|
|
710
|
+
) {
|
|
711
|
+
edges {
|
|
712
|
+
node {
|
|
713
|
+
id
|
|
714
|
+
ref
|
|
715
|
+
quantity
|
|
716
|
+
skuRef
|
|
717
|
+
locationRef
|
|
718
|
+
attributes {
|
|
719
|
+
name
|
|
720
|
+
value
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
`,
|
|
727
|
+
variables: { shipDate: today }
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
const reservationEdges = result.data?.inventoryQuantities?.edges || [];
|
|
731
|
+
log.info(`📋 Found ${reservationEdges.length} reservations for ${today}`);
|
|
732
|
+
|
|
733
|
+
// Step 2: Parse and sort by priority
|
|
734
|
+
interface ReservationWithPriority {
|
|
735
|
+
id: string;
|
|
736
|
+
ref: string;
|
|
737
|
+
quantity: number;
|
|
738
|
+
sku: string;
|
|
739
|
+
locationRef: string;
|
|
740
|
+
priority: number;
|
|
741
|
+
queuePosition: number;
|
|
742
|
+
orderId: string;
|
|
743
|
+
customerId: string;
|
|
744
|
+
customerEmail: string;
|
|
745
|
+
tier: string;
|
|
746
|
+
reservationId: string;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const reservations: ReservationWithPriority[] = reservationEdges.map((edge: any) => {
|
|
750
|
+
const attrs = edge.node.attributes || [];
|
|
751
|
+
const getAttr = (name: string) => attrs.find((a: any) => a.name === name)?.value;
|
|
752
|
+
|
|
753
|
+
return {
|
|
754
|
+
id: edge.node.id,
|
|
755
|
+
ref: edge.node.ref,
|
|
756
|
+
quantity: edge.node.quantity,
|
|
757
|
+
sku: edge.node.skuRef,
|
|
758
|
+
locationRef: edge.node.locationRef,
|
|
759
|
+
priority: parseInt(getAttr('priority') || '0'),
|
|
760
|
+
queuePosition: parseInt(getAttr('queuePosition') || '999'),
|
|
761
|
+
orderId: getAttr('orderId') || '',
|
|
762
|
+
customerId: getAttr('customerId') || '',
|
|
763
|
+
customerEmail: getAttr('customerEmail') || '',
|
|
764
|
+
tier: getAttr('tier') || 'STANDARD',
|
|
765
|
+
reservationId: getAttr('reservationId') || edge.node.ref
|
|
766
|
+
};
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// Sort by priority (higher priority = lower number due to negative time component)
|
|
770
|
+
reservations.sort((a, b) => b.priority - a.priority);
|
|
771
|
+
|
|
772
|
+
log.info('✅ Sorted reservations by priority', {
|
|
773
|
+
vipCount: reservations.filter(r => r.tier === 'VIP').length,
|
|
774
|
+
standardCount: reservations.filter(r => r.tier === 'STANDARD').length
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
// Step 3: Group by SKU and location
|
|
778
|
+
const groupedBySku = new Map<string, ReservationWithPriority[]>();
|
|
779
|
+
for (const reservation of reservations) {
|
|
780
|
+
const key = `${reservation.sku}:${reservation.locationRef}`;
|
|
781
|
+
if (!groupedBySku.has(key)) {
|
|
782
|
+
groupedBySku.set(key, []);
|
|
783
|
+
}
|
|
784
|
+
groupedBySku.get(key)!.push(reservation);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Step 4: Process each SKU group
|
|
788
|
+
const confirmed: any[] = [];
|
|
789
|
+
const cancelled: any[] = [];
|
|
790
|
+
|
|
791
|
+
for (const [skuKey, skuReservations] of groupedBySku.entries()) {
|
|
792
|
+
const [sku, locationRef] = skuKey.split(':');
|
|
793
|
+
|
|
794
|
+
// Query current inventory
|
|
795
|
+
const invResult = await client.graphql({
|
|
796
|
+
query: `
|
|
797
|
+
query GetInventory($sku: String!, $locationRef: String!) {
|
|
798
|
+
inventoryQuantities(
|
|
799
|
+
first: 1
|
|
800
|
+
skuRef: $sku
|
|
801
|
+
locationRef: $locationRef
|
|
802
|
+
type: "LAST_ON_HAND"
|
|
803
|
+
) {
|
|
804
|
+
edges {
|
|
805
|
+
node {
|
|
806
|
+
quantity
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
`,
|
|
812
|
+
variables: { sku, locationRef }
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
let availableQty = invResult.data?.inventoryQuantities?.edges?.[0]?.node?.quantity || 0;
|
|
816
|
+
|
|
817
|
+
log.info(`⚙️ Processing ${skuReservations.length} reservations for ${sku}`, {
|
|
818
|
+
available: availableQty,
|
|
819
|
+
totalRequested: skuReservations.reduce((sum, r) => sum + r.quantity, 0)
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// Allocate inventory by priority
|
|
823
|
+
for (const reservation of skuReservations) {
|
|
824
|
+
if (availableQty >= reservation.quantity) {
|
|
825
|
+
// Confirm reservation - create fulfillment order
|
|
826
|
+
try {
|
|
827
|
+
await client.graphql({
|
|
828
|
+
query: `
|
|
829
|
+
mutation CreateFulfillmentOrder($input: CreateOrderInput!) {
|
|
830
|
+
createOrder(input: $input) {
|
|
831
|
+
id
|
|
832
|
+
ref
|
|
833
|
+
status
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
`,
|
|
837
|
+
variables: {
|
|
838
|
+
input: {
|
|
839
|
+
ref: `FO-${reservation.orderId}-${Date.now()}`,
|
|
840
|
+
type: 'FULFILLMENT',
|
|
841
|
+
retailerId: '1', // From config
|
|
842
|
+
customer: {
|
|
843
|
+
ref: reservation.customerId,
|
|
844
|
+
email: reservation.customerEmail
|
|
845
|
+
},
|
|
846
|
+
items: [{
|
|
847
|
+
skuRef: reservation.sku,
|
|
848
|
+
quantity: reservation.quantity,
|
|
849
|
+
locationRef: reservation.locationRef
|
|
850
|
+
}],
|
|
851
|
+
attributes: [
|
|
852
|
+
{ name: 'originalOrderId', value: reservation.orderId },
|
|
853
|
+
{ name: 'reservationId', value: reservation.reservationId },
|
|
854
|
+
{ name: 'tier', value: reservation.tier },
|
|
855
|
+
{ name: 'shipDate', value: today }
|
|
856
|
+
]
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
// Update reservation status
|
|
862
|
+
await client.graphql({
|
|
863
|
+
query: `
|
|
864
|
+
mutation UpdateReservation($id: ID!, $status: String!) {
|
|
865
|
+
updateInventoryQuantity(id: $id, status: $status) {
|
|
866
|
+
id
|
|
867
|
+
status
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
`,
|
|
871
|
+
variables: {
|
|
872
|
+
id: reservation.id,
|
|
873
|
+
status: 'CONFIRMED'
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
// Update KV
|
|
878
|
+
const kvRecord = await kvAdapter.get(['reservation', reservation.reservationId]);
|
|
879
|
+
if (kvRecord?.value) {
|
|
880
|
+
const record = kvRecord.value as ReservationRecord;
|
|
881
|
+
record.status = 'CONFIRMED';
|
|
882
|
+
record.confirmedAt = new Date().toISOString();
|
|
883
|
+
await kvAdapter.set(['reservation', reservation.reservationId], record);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
availableQty -= reservation.quantity;
|
|
887
|
+
confirmed.push(reservation);
|
|
888
|
+
|
|
889
|
+
log.info(`✅ Confirmed reservation: ${reservation.reservationId}`);
|
|
890
|
+
} catch (error) {
|
|
891
|
+
// ? Enhanced: Error logging with recommendations
|
|
892
|
+
log.error('[PreOrderAllocation] Failed to confirm reservation', {
|
|
893
|
+
reservationId: reservation.reservationId,
|
|
894
|
+
error: error instanceof Error ? error.message : String(error),
|
|
895
|
+
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
896
|
+
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
897
|
+
: error.message?.includes('mutation') || error.message?.includes('GraphQL')
|
|
898
|
+
? 'Check GraphQL mutation syntax and reservation confirmation payload'
|
|
899
|
+
: error.message?.includes('inventory') || error.message?.includes('quantity')
|
|
900
|
+
? 'Check available inventory quantity and reservation status'
|
|
901
|
+
: 'Review error details and check reservation confirmation logic'
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
} else {
|
|
905
|
+
// Insufficient inventory - cancel reservation
|
|
906
|
+
try {
|
|
907
|
+
await client.graphql({
|
|
908
|
+
query: `
|
|
909
|
+
mutation UpdateReservation($id: ID!, $status: String!) {
|
|
910
|
+
updateInventoryQuantity(id: $id, status: $status) {
|
|
911
|
+
id
|
|
912
|
+
status
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
`,
|
|
916
|
+
variables: {
|
|
917
|
+
id: reservation.id,
|
|
918
|
+
status: 'CANCELLED'
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// Update KV
|
|
923
|
+
const kvRecord = await kvAdapter.get(['reservation', reservation.reservationId]);
|
|
924
|
+
if (kvRecord?.value) {
|
|
925
|
+
const record = kvRecord.value as ReservationRecord;
|
|
926
|
+
record.status = 'CANCELLED';
|
|
927
|
+
record.cancelledAt = new Date().toISOString();
|
|
928
|
+
await kvAdapter.set(['reservation', reservation.reservationId], record);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
cancelled.push({
|
|
932
|
+
...reservation,
|
|
933
|
+
reason: 'Insufficient inventory',
|
|
934
|
+
availableQty
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
log.warn(`⚠️ Cancelled reservation: ${reservation.reservationId}`, {
|
|
938
|
+
reason: 'insufficient_inventory',
|
|
939
|
+
needed: reservation.quantity,
|
|
940
|
+
available: availableQty
|
|
941
|
+
});
|
|
942
|
+
} catch (error) {
|
|
943
|
+
// ? Enhanced: Error logging with recommendations
|
|
944
|
+
log.error('[PreOrderAllocation] Failed to cancel reservation', {
|
|
945
|
+
reservationId: reservation.reservationId,
|
|
946
|
+
error: error instanceof Error ? error.message : String(error),
|
|
947
|
+
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
948
|
+
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
949
|
+
: error.message?.includes('mutation') || error.message?.includes('GraphQL')
|
|
950
|
+
? 'Check GraphQL mutation syntax and reservation cancellation payload'
|
|
951
|
+
: error.message?.includes('not found') || error.message?.includes('missing')
|
|
952
|
+
? 'Reservation not found - verify reservationId and check KV store'
|
|
953
|
+
: 'Review error details and check reservation cancellation logic'
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Step 5: Send notifications (placeholder)
|
|
961
|
+
const duration = Date.now() - startTime;
|
|
962
|
+
log.info(`📧 Sending notifications (${duration}ms total)`, {
|
|
963
|
+
confirmed: confirmed.length,
|
|
964
|
+
cancelled: cancelled.length
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
// TODO: Send confirmation emails to confirmed reservations
|
|
968
|
+
// TODO: Send cancellation/apology emails to cancelled reservations
|
|
969
|
+
|
|
970
|
+
return {
|
|
971
|
+
success: true,
|
|
972
|
+
date: today,
|
|
973
|
+
processed: reservations.length,
|
|
974
|
+
confirmed: confirmed.length,
|
|
975
|
+
cancelled: cancelled.length,
|
|
976
|
+
duration,
|
|
977
|
+
summary: {
|
|
978
|
+
vipConfirmed: confirmed.filter(r => r.tier === 'VIP').length,
|
|
979
|
+
standardConfirmed: confirmed.filter(r => r.tier === 'STANDARD').length,
|
|
980
|
+
vipCancelled: cancelled.filter(r => r.tier === 'VIP').length,
|
|
981
|
+
standardCancelled: cancelled.filter(r => r.tier === 'STANDARD').length
|
|
982
|
+
},
|
|
983
|
+
timestamp: new Date().toISOString()
|
|
984
|
+
};
|
|
985
|
+
} catch (error) {
|
|
986
|
+
// ? Enhanced: Error logging with recommendations
|
|
987
|
+
log.error('[PreOrderAllocation] Scheduled release failed', {
|
|
988
|
+
error: error instanceof Error ? error.message : String(error),
|
|
989
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
990
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
991
|
+
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
992
|
+
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
993
|
+
: error.message?.includes('KV') || error.message?.includes('state')
|
|
994
|
+
? 'Check KV store connectivity and reservation state structure'
|
|
995
|
+
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
996
|
+
? 'Check network connectivity and Fluent Commerce API availability'
|
|
997
|
+
: 'Review error details and check scheduled release workflow configuration'
|
|
998
|
+
});
|
|
999
|
+
throw error;
|
|
1000
|
+
}
|
|
1001
|
+
})(ctx);
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
// =============================================================================
|
|
1005
|
+
// WORKFLOW 3: CANCELLATION WEBHOOK
|
|
1006
|
+
// =============================================================================
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Cancellation Webhook - Cancel reservation and offer to next in queue
|
|
1010
|
+
*
|
|
1011
|
+
* Flow:
|
|
1012
|
+
* 1. Receive cancellation request
|
|
1013
|
+
* 2. Update reservation status
|
|
1014
|
+
* 3. Query next customer in queue
|
|
1015
|
+
* 4. Offer to next customer (if available)
|
|
1016
|
+
* 5. Send notifications
|
|
1017
|
+
*/
|
|
1018
|
+
export const cancellationWebhook = webhook('cancel-reservation', {
|
|
1019
|
+
response: { mode: 'sync' },
|
|
1020
|
+
cors: true
|
|
1021
|
+
})
|
|
1022
|
+
.then(fn('validate-cancellation', ({ data, log }) => {
|
|
1023
|
+
log.info('🔍 Processing cancellation request');
|
|
1024
|
+
|
|
1025
|
+
if (!data.reservationId && !data.orderId) {
|
|
1026
|
+
throw new Error('Either reservationId or orderId is required');
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
return data;
|
|
1030
|
+
}))
|
|
1031
|
+
.then(http('cancel-and-requeue', {
|
|
1032
|
+
connection: 'fluent_commerce'
|
|
1033
|
+
}, async (ctx) => {
|
|
1034
|
+
const { reservationId, orderId } = ctx.data;
|
|
1035
|
+
const { log, openKv } = ctx;
|
|
1036
|
+
const client = await createClient(ctx, { validateConnection: true });
|
|
1037
|
+
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
1038
|
+
|
|
1039
|
+
log.info('🚫 Cancelling reservation', { reservationId, orderId });
|
|
1040
|
+
|
|
1041
|
+
// Find reservation in KV
|
|
1042
|
+
let reservation: ReservationRecord | null = null;
|
|
1043
|
+
|
|
1044
|
+
if (reservationId) {
|
|
1045
|
+
const kvRecord = await kvAdapter.get(['reservation', reservationId]);
|
|
1046
|
+
reservation = kvRecord?.value as ReservationRecord;
|
|
1047
|
+
} else {
|
|
1048
|
+
// Search by orderId (would need indexed tracker)
|
|
1049
|
+
// Placeholder: iterate through all reservations
|
|
1050
|
+
log.warn('[CANCEL] Searching by orderId - implement indexed search for production');
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (!reservation) {
|
|
1054
|
+
throw new Error(`Reservation not found: ${reservationId || orderId}`);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Update status in Fluent
|
|
1058
|
+
const result = await client.graphql({
|
|
1059
|
+
query: `
|
|
1060
|
+
query FindReservation($ref: String!) {
|
|
1061
|
+
inventoryQuantities(first: 1, ref: $ref) {
|
|
1062
|
+
edges {
|
|
1063
|
+
node {
|
|
1064
|
+
id
|
|
1065
|
+
ref
|
|
1066
|
+
status
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
`,
|
|
1072
|
+
variables: { ref: reservation.reservationId }
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
const inventoryId = result.data?.inventoryQuantities?.edges?.[0]?.node?.id;
|
|
1076
|
+
|
|
1077
|
+
if (inventoryId) {
|
|
1078
|
+
await client.graphql({
|
|
1079
|
+
query: `
|
|
1080
|
+
mutation CancelReservation($id: ID!, $status: String!) {
|
|
1081
|
+
updateInventoryQuantity(id: $id, status: $status) {
|
|
1082
|
+
id
|
|
1083
|
+
status
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
`,
|
|
1087
|
+
variables: {
|
|
1088
|
+
id: inventoryId,
|
|
1089
|
+
status: 'CANCELLED'
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// Update KV
|
|
1095
|
+
reservation.status = 'CANCELLED';
|
|
1096
|
+
reservation.cancelledAt = new Date().toISOString();
|
|
1097
|
+
await kvAdapter.set(['reservation', reservation.reservationId], reservation);
|
|
1098
|
+
|
|
1099
|
+
log.info('✅ Reservation cancelled', {
|
|
1100
|
+
reservationId: reservation.reservationId,
|
|
1101
|
+
sku: reservation.sku,
|
|
1102
|
+
quantity: reservation.quantity
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
// Find next in queue
|
|
1106
|
+
const queueKey = ['queue', reservation.sku, reservation.locationRef, reservation.shipDate].join(':');
|
|
1107
|
+
|
|
1108
|
+
// TODO: Implement queue management
|
|
1109
|
+
// Query next customer with higher queue position
|
|
1110
|
+
// Send offer to next customer
|
|
1111
|
+
|
|
1112
|
+
log.info('📋 Queue position available for next customer', {
|
|
1113
|
+
sku: reservation.sku,
|
|
1114
|
+
shipDate: reservation.shipDate,
|
|
1115
|
+
releasedQuantity: reservation.quantity
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
return {
|
|
1119
|
+
success: true,
|
|
1120
|
+
cancelled: {
|
|
1121
|
+
reservationId: reservation.reservationId,
|
|
1122
|
+
orderId: reservation.orderId,
|
|
1123
|
+
sku: reservation.sku,
|
|
1124
|
+
quantity: reservation.quantity
|
|
1125
|
+
},
|
|
1126
|
+
message: 'Reservation cancelled successfully',
|
|
1127
|
+
timestamp: new Date().toISOString()
|
|
1128
|
+
};
|
|
1129
|
+
}));
|
|
1130
|
+
|
|
1131
|
+
// =============================================================================
|
|
1132
|
+
// WORKFLOW 4: ADMIN DASHBOARD EXPORT
|
|
1133
|
+
// =============================================================================
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Export Reservations to S3 for Admin Dashboard
|
|
1137
|
+
*
|
|
1138
|
+
* Runs daily to export reservation status to S3 for dashboard visualization
|
|
1139
|
+
*/
|
|
1140
|
+
export const exportReservations = schedule('export-reservations', '0 3 * * *', async (ctx) => {
|
|
1141
|
+
const { openKv, log } = ctx;
|
|
1142
|
+
|
|
1143
|
+
return fn('export-to-s3', async ({ openKv, log }) => {
|
|
1144
|
+
log.info('📤 Exporting reservations to S3');
|
|
1145
|
+
|
|
1146
|
+
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
1147
|
+
const reservationTracker = new VersoriFileTracker(openKv(), 'pre-order-reservations');
|
|
1148
|
+
|
|
1149
|
+
// Get all reservations
|
|
1150
|
+
const allReservations = await reservationTracker.listProcessedFiles();
|
|
1151
|
+
log.info(`📋 Found ${allReservations.length} reservations to export`);
|
|
1152
|
+
|
|
1153
|
+
// Fetch full reservation data
|
|
1154
|
+
const exportData: any[] = [];
|
|
1155
|
+
for (const tracked of allReservations) {
|
|
1156
|
+
const kvRecord = await kvAdapter.get(['reservation', tracked.fileName]);
|
|
1157
|
+
if (kvRecord?.value) {
|
|
1158
|
+
exportData.push(kvRecord.value);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Group by ship date and status
|
|
1163
|
+
const summary = {
|
|
1164
|
+
total: exportData.length,
|
|
1165
|
+
byStatus: {
|
|
1166
|
+
RESERVED: exportData.filter(r => r.status === 'RESERVED').length,
|
|
1167
|
+
CONFIRMED: exportData.filter(r => r.status === 'CONFIRMED').length,
|
|
1168
|
+
CANCELLED: exportData.filter(r => r.status === 'CANCELLED').length,
|
|
1169
|
+
EXPIRED: exportData.filter(r => r.status === 'EXPIRED').length
|
|
1170
|
+
},
|
|
1171
|
+
byTier: {
|
|
1172
|
+
VIP: exportData.filter(r => r.tier === 'VIP').length,
|
|
1173
|
+
STANDARD: exportData.filter(r => r.tier === 'STANDARD').length
|
|
1174
|
+
},
|
|
1175
|
+
exportedAt: new Date().toISOString()
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
const exportPayload = {
|
|
1179
|
+
summary,
|
|
1180
|
+
reservations: exportData
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
// TODO: Upload to S3
|
|
1184
|
+
// const s3Key = `pre-orders/export-${new Date().toISOString().split('T')[0]}.json`;
|
|
1185
|
+
// await s3Client.putObject({ Bucket, Key: s3Key, Body: JSON.stringify(exportPayload) });
|
|
1186
|
+
|
|
1187
|
+
log.info('✅ Export complete', summary);
|
|
1188
|
+
|
|
1189
|
+
return {
|
|
1190
|
+
success: true,
|
|
1191
|
+
summary,
|
|
1192
|
+
recordCount: exportData.length
|
|
1193
|
+
};
|
|
1194
|
+
})(ctx);
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
// =============================================================================
|
|
1198
|
+
// WORKFLOW 5: MANUAL ATP CHECK (Testing/Admin)
|
|
1199
|
+
// =============================================================================
|
|
1200
|
+
|
|
1201
|
+
/**
|
|
1202
|
+
* Manual ATP Check - Admin endpoint to check future ATP
|
|
1203
|
+
*/
|
|
1204
|
+
export const checkAtp = webhook('check-atp', {
|
|
1205
|
+
response: { mode: 'sync' },
|
|
1206
|
+
cors: true
|
|
1207
|
+
})
|
|
1208
|
+
.then(http('calculate-manual-atp', {
|
|
1209
|
+
connection: 'fluent_commerce'
|
|
1210
|
+
}, async (ctx) => {
|
|
1211
|
+
const { sku, locationRef, shipDate, quantity } = ctx.data;
|
|
1212
|
+
const { log } = ctx;
|
|
1213
|
+
const startTime = Date.now();
|
|
1214
|
+
const client = await createClient(ctx, { validateConnection: true });
|
|
1215
|
+
|
|
1216
|
+
log.info('🔍 Manual ATP calculation', { sku, locationRef, shipDate, quantity });
|
|
1217
|
+
|
|
1218
|
+
// Reuse ATP calculation logic from pre-order workflow
|
|
1219
|
+
const inventoryResult = await client.graphql({
|
|
1220
|
+
query: `
|
|
1221
|
+
query GetInventory($locationRef: String!, $skuRef: String!) {
|
|
1222
|
+
inventoryQuantities(
|
|
1223
|
+
first: 1
|
|
1224
|
+
locationRef: $locationRef
|
|
1225
|
+
skuRef: $skuRef
|
|
1226
|
+
type: "LAST_ON_HAND"
|
|
1227
|
+
) {
|
|
1228
|
+
edges {
|
|
1229
|
+
node {
|
|
1230
|
+
quantity
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
`,
|
|
1236
|
+
variables: { locationRef, skuRef: sku }
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
const currentInventory = inventoryResult.data?.inventoryQuantities?.edges?.[0]?.node?.quantity || 0;
|
|
1240
|
+
|
|
1241
|
+
// Query reservations
|
|
1242
|
+
const reservationsResult = await client.graphql({
|
|
1243
|
+
query: `
|
|
1244
|
+
query GetReservations($sku: String!, $shipDate: String!) {
|
|
1245
|
+
inventoryQuantities(
|
|
1246
|
+
first: 100
|
|
1247
|
+
skuRef: $sku
|
|
1248
|
+
status: "RESERVED"
|
|
1249
|
+
) {
|
|
1250
|
+
edges {
|
|
1251
|
+
node {
|
|
1252
|
+
quantity
|
|
1253
|
+
attributes {
|
|
1254
|
+
name
|
|
1255
|
+
value
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
`,
|
|
1262
|
+
variables: { sku, shipDate }
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
let existingReservations = 0;
|
|
1266
|
+
const reservationEdges = reservationsResult.data?.inventoryQuantities?.edges || [];
|
|
1267
|
+
for (const edge of reservationEdges) {
|
|
1268
|
+
const shipDateAttr = edge.node.attributes?.find((a: any) => a.name === 'shipDate');
|
|
1269
|
+
if (shipDateAttr?.value === shipDate) {
|
|
1270
|
+
existingReservations += edge.node.quantity || 0;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// Query expected arrivals
|
|
1275
|
+
const expectedArrivalResult = await client.graphql({
|
|
1276
|
+
query: `
|
|
1277
|
+
query GetExpectedArrivals($locationRef: String!, $skuRef: String!, $beforeDate: String!) {
|
|
1278
|
+
inventoryQuantities(
|
|
1279
|
+
first: 50
|
|
1280
|
+
locationRef: $locationRef
|
|
1281
|
+
skuRef: $skuRef
|
|
1282
|
+
type: "EXPECTED"
|
|
1283
|
+
expectedOnBefore: $beforeDate
|
|
1284
|
+
) {
|
|
1285
|
+
edges {
|
|
1286
|
+
node {
|
|
1287
|
+
quantity
|
|
1288
|
+
expectedOn
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
`,
|
|
1294
|
+
variables: { locationRef, skuRef: sku, beforeDate: shipDate }
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
const expectedArrival = expectedArrivalResult.data?.inventoryQuantities?.edges?.reduce(
|
|
1298
|
+
(sum: number, edge: any) => sum + (edge.node.quantity || 0),
|
|
1299
|
+
0
|
|
1300
|
+
) || 0;
|
|
1301
|
+
|
|
1302
|
+
const totalExpected = currentInventory + expectedArrival;
|
|
1303
|
+
const overbookingLimit = Math.floor(totalExpected * 1.05);
|
|
1304
|
+
const availableToPromise = overbookingLimit - existingReservations;
|
|
1305
|
+
const canFulfill = availableToPromise >= (quantity || 1);
|
|
1306
|
+
const duration = Date.now() - startTime;
|
|
1307
|
+
|
|
1308
|
+
log.info(`✅ ATP check complete (${duration}ms)`, { sku, canFulfill });
|
|
1309
|
+
|
|
1310
|
+
return {
|
|
1311
|
+
sku,
|
|
1312
|
+
locationRef,
|
|
1313
|
+
shipDate,
|
|
1314
|
+
requestedQuantity: quantity || 1,
|
|
1315
|
+
currentInventory,
|
|
1316
|
+
expectedArrival,
|
|
1317
|
+
totalExpected,
|
|
1318
|
+
existingReservations,
|
|
1319
|
+
overbookingLimit,
|
|
1320
|
+
availableToPromise,
|
|
1321
|
+
canFulfill,
|
|
1322
|
+
duration,
|
|
1323
|
+
recommendation: canFulfill
|
|
1324
|
+
? 'Reservation can be fulfilled'
|
|
1325
|
+
: `Insufficient inventory (need ${quantity || 1}, available ${availableToPromise})`,
|
|
1326
|
+
timestamp: new Date().toISOString()
|
|
1327
|
+
};
|
|
1328
|
+
}));
|
|
1329
|
+
```
|
|
1330
|
+
|
|
1331
|
+
---
|
|
1332
|
+
|
|
1333
|
+
## 2. Configuration File: `config/priority-tiers.json`
|
|
1334
|
+
|
|
1335
|
+
```json
|
|
1336
|
+
{
|
|
1337
|
+
"version": "1.0.0",
|
|
1338
|
+
"description": "Pre-order priority tier configuration",
|
|
1339
|
+
"tiers": {
|
|
1340
|
+
"VIP": {
|
|
1341
|
+
"score": 100,
|
|
1342
|
+
"description": "VIP customers get priority allocation",
|
|
1343
|
+
"benefits": [
|
|
1344
|
+
"First priority for limited inventory",
|
|
1345
|
+
"Email notifications for restocks",
|
|
1346
|
+
"Expedited shipping",
|
|
1347
|
+
"Early access to new launches"
|
|
1348
|
+
]
|
|
1349
|
+
},
|
|
1350
|
+
"STANDARD": {
|
|
1351
|
+
"score": 50,
|
|
1352
|
+
"description": "Standard customer tier",
|
|
1353
|
+
"benefits": [
|
|
1354
|
+
"Standard allocation priority",
|
|
1355
|
+
"Email notifications",
|
|
1356
|
+
"Standard shipping"
|
|
1357
|
+
]
|
|
1358
|
+
}
|
|
1359
|
+
},
|
|
1360
|
+
"overbooking": {
|
|
1361
|
+
"enabled": true,
|
|
1362
|
+
"percentage": 1.05,
|
|
1363
|
+
"description": "Allow 105% allocation to account for cancellations"
|
|
1364
|
+
},
|
|
1365
|
+
"expiration": {
|
|
1366
|
+
"daysBeforeShipDate": 7,
|
|
1367
|
+
"description": "Reservations expire 7 days before ship date if not confirmed"
|
|
1368
|
+
},
|
|
1369
|
+
"queue": {
|
|
1370
|
+
"maxQueueDepth": 10000,
|
|
1371
|
+
"reallocationOnCancellation": true
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
```
|
|
1375
|
+
|
|
1376
|
+
---
|
|
1377
|
+
|
|
1378
|
+
## 3. Package Configuration: `package.json`
|
|
1379
|
+
|
|
1380
|
+
```json
|
|
1381
|
+
{
|
|
1382
|
+
"name": "pre-order-allocation-management",
|
|
1383
|
+
"version": "1.0.0",
|
|
1384
|
+
"description": "Pre-order allocation system with priority queuing and future ATP",
|
|
1385
|
+
"versori": {
|
|
1386
|
+
"workflows": "./index.ts"
|
|
1387
|
+
},
|
|
1388
|
+
"dependencies": {
|
|
1389
|
+
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
1390
|
+
"@versori/run": "latest"
|
|
1391
|
+
},
|
|
1392
|
+
"devDependencies": {
|
|
1393
|
+
"@types/node": "^20.0.0",
|
|
1394
|
+
"typescript": "^5.0.0"
|
|
1395
|
+
},
|
|
1396
|
+
"scripts": {
|
|
1397
|
+
"deploy": "versori deploy",
|
|
1398
|
+
"logs": "versori logs",
|
|
1399
|
+
"test-preorder": "curl -X POST http://localhost:8080/pre-order -H 'Content-Type: application/json' -d @test/sample-preorder.json",
|
|
1400
|
+
"test-atp": "curl -X POST http://localhost:8080/check-atp -H 'Content-Type: application/json' -d '{\"sku\":\"IPHONE-15-PRO-256\",\"locationRef\":\"DC-NY\",\"shipDate\":\"2025-02-15\",\"quantity\":1}'"
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
```
|
|
1404
|
+
|
|
1405
|
+
---
|
|
1406
|
+
|
|
1407
|
+
## 4. Test Data: `test/sample-preorder.json`
|
|
1408
|
+
|
|
1409
|
+
```json
|
|
1410
|
+
{
|
|
1411
|
+
"orderId": "PRE-2025-001234",
|
|
1412
|
+
"customerId": "CUST-VIP-9876",
|
|
1413
|
+
"customerEmail": "john.doe@example.com",
|
|
1414
|
+
"customerTier": "VIP",
|
|
1415
|
+
"shipDate": "2025-02-15T00:00:00Z",
|
|
1416
|
+
"source": "shopify",
|
|
1417
|
+
"items": [
|
|
1418
|
+
{
|
|
1419
|
+
"sku": "IPHONE-15-PRO-256",
|
|
1420
|
+
"productName": "iPhone 15 Pro 256GB Titanium Blue",
|
|
1421
|
+
"quantity": 1,
|
|
1422
|
+
"locationRef": "DC-NY"
|
|
1423
|
+
}
|
|
1424
|
+
]
|
|
1425
|
+
}
|
|
1426
|
+
```
|
|
1427
|
+
|
|
1428
|
+
---
|
|
1429
|
+
|
|
1430
|
+
## Versori Workflow Structure
|
|
1431
|
+
|
|
1432
|
+
This solution uses multiple workflow types:
|
|
1433
|
+
|
|
1434
|
+
### HTTP Webhooks (Real-time)
|
|
1435
|
+
|
|
1436
|
+
1. **pre-order** - Receives pre-orders from e-commerce
|
|
1437
|
+
2. **cancel-reservation** - Cancels existing reservations
|
|
1438
|
+
3. **check-atp** - Manual ATP calculation (admin)
|
|
1439
|
+
|
|
1440
|
+
### Scheduled Workflows (Cron)
|
|
1441
|
+
|
|
1442
|
+
1. **release-reservations** - Daily at 2 AM
|
|
1443
|
+
2. **export-reservations** - Daily at 3 AM (dashboard data)
|
|
1444
|
+
|
|
1445
|
+
### Workflow Dependencies
|
|
1446
|
+
|
|
1447
|
+
```
|
|
1448
|
+
┌─────────────────┐
|
|
1449
|
+
│ E-commerce │
|
|
1450
|
+
│ Platform │
|
|
1451
|
+
└────────┬────────┘
|
|
1452
|
+
│ HTTP POST
|
|
1453
|
+
▼
|
|
1454
|
+
┌─────────────────┐ ┌──────────────┐
|
|
1455
|
+
│ Pre-Order │─────▶│ VersoriKV │
|
|
1456
|
+
│ Webhook │ │ (State) │
|
|
1457
|
+
└────────┬────────┘ └──────────────┘
|
|
1458
|
+
│
|
|
1459
|
+
▼
|
|
1460
|
+
┌─────────────────┐
|
|
1461
|
+
│ Fluent API │
|
|
1462
|
+
│ (Reservations) │
|
|
1463
|
+
└─────────────────┘
|
|
1464
|
+
▲
|
|
1465
|
+
│
|
|
1466
|
+
┌────────┴────────┐
|
|
1467
|
+
│ Release │ (Scheduled 2 AM)
|
|
1468
|
+
│ Workflow │
|
|
1469
|
+
└────────┬────────┘
|
|
1470
|
+
│
|
|
1471
|
+
▼
|
|
1472
|
+
┌─────────────────┐
|
|
1473
|
+
│ Fulfillment │
|
|
1474
|
+
│ Orders │
|
|
1475
|
+
└─────────────────┘
|
|
1476
|
+
```
|
|
1477
|
+
|
|
1478
|
+
---
|
|
1479
|
+
|
|
1480
|
+
## Key Patterns Explained
|
|
1481
|
+
|
|
1482
|
+
### Pattern 1: Future ATP (Available to Promise) Calculation
|
|
1483
|
+
|
|
1484
|
+
**Algorithm:**
|
|
1485
|
+
|
|
1486
|
+
```typescript
|
|
1487
|
+
// Step 1: Get current inventory
|
|
1488
|
+
currentInventory = queryInventory(sku, location)
|
|
1489
|
+
|
|
1490
|
+
// Step 2: Get expected arrivals before ship date
|
|
1491
|
+
expectedArrival = queryExpectedBefore(sku, location, shipDate)
|
|
1492
|
+
|
|
1493
|
+
// Step 3: Get existing reservations for this date
|
|
1494
|
+
existingReservations = queryReservations(sku, location, shipDate)
|
|
1495
|
+
|
|
1496
|
+
// Step 4: Calculate overbooking limit (105%)
|
|
1497
|
+
totalExpected = currentInventory + expectedArrival
|
|
1498
|
+
overbookingLimit = totalExpected * 1.05
|
|
1499
|
+
|
|
1500
|
+
// Step 5: Calculate ATP
|
|
1501
|
+
availableToPromise = overbookingLimit - existingReservations
|
|
1502
|
+
|
|
1503
|
+
// Step 6: Check fulfillment
|
|
1504
|
+
canFulfill = availableToPromise >= requestedQuantity
|
|
1505
|
+
```
|
|
1506
|
+
|
|
1507
|
+
**Why overbooking?**
|
|
1508
|
+
|
|
1509
|
+
- Typical cancellation rate: 3-5%
|
|
1510
|
+
- 105% allocation ensures full utilization
|
|
1511
|
+
- Prevents lost sales from conservative allocation
|
|
1512
|
+
|
|
1513
|
+
**Edge cases:**
|
|
1514
|
+
|
|
1515
|
+
- No expected arrivals → ATP based on current only
|
|
1516
|
+
- Multiple locations → calculate per location
|
|
1517
|
+
- Negative ATP → queue overflow, reject order
|
|
1518
|
+
|
|
1519
|
+
### Pattern 2: Priority Scoring Algorithm
|
|
1520
|
+
|
|
1521
|
+
**Score calculation:**
|
|
1522
|
+
|
|
1523
|
+
```typescript
|
|
1524
|
+
// Tier component (VIP=100, Standard=50)
|
|
1525
|
+
tierScore = customerTier === 'VIP' ? 100 : 50
|
|
1526
|
+
|
|
1527
|
+
// Time component (earlier = better)
|
|
1528
|
+
timeScore = Date.now() // Milliseconds since epoch
|
|
1529
|
+
|
|
1530
|
+
// Final priority (higher = better)
|
|
1531
|
+
priority = (tierScore * 1000000) - timeScore
|
|
1532
|
+
|
|
1533
|
+
// Example scores:
|
|
1534
|
+
// VIP order at 10:00 AM = 100000000 - 1708000000 = -1607000000
|
|
1535
|
+
// VIP order at 10:01 AM = 100000000 - 1708000060 = -1607000060
|
|
1536
|
+
// Standard at 10:00 AM = 50000000 - 1708000000 = -1657000000
|
|
1537
|
+
|
|
1538
|
+
// Sort descending: all VIPs come first, then by time
|
|
1539
|
+
```
|
|
1540
|
+
|
|
1541
|
+
**Why negative scores?**
|
|
1542
|
+
|
|
1543
|
+
- Allows simple descending sort
|
|
1544
|
+
- VIP scores always > Standard scores
|
|
1545
|
+
- Time breaks ties within tier
|
|
1546
|
+
|
|
1547
|
+
**Alternative scoring strategies:**
|
|
1548
|
+
|
|
1549
|
+
- Purchase history: +10 points per previous purchase
|
|
1550
|
+
- Cart value: +1 point per $100
|
|
1551
|
+
- Membership duration: +1 point per year
|
|
1552
|
+
|
|
1553
|
+
### Pattern 3: Reservation Expiration
|
|
1554
|
+
|
|
1555
|
+
**Expiration logic:**
|
|
1556
|
+
|
|
1557
|
+
```typescript
|
|
1558
|
+
// Calculate expiration date
|
|
1559
|
+
shipDate = new Date('2025-02-15')
|
|
1560
|
+
expirationDate = new Date(shipDate.getTime() - 7 * 24 * 60 * 60 * 1000)
|
|
1561
|
+
|
|
1562
|
+
// Store with reservation
|
|
1563
|
+
reservation.expiresAt = expirationDate.toISOString()
|
|
1564
|
+
|
|
1565
|
+
// Scheduled cleanup (runs daily)
|
|
1566
|
+
const now = new Date()
|
|
1567
|
+
if (now > expirationDate && reservation.status === 'RESERVED') {
|
|
1568
|
+
reservation.status = 'EXPIRED'
|
|
1569
|
+
releaseInventory(reservation)
|
|
1570
|
+
offerToNextInQueue(reservation)
|
|
1571
|
+
}
|
|
1572
|
+
```
|
|
1573
|
+
|
|
1574
|
+
**Why 7 days?**
|
|
1575
|
+
|
|
1576
|
+
- Gives customers buffer to cancel if needed
|
|
1577
|
+
- Prevents last-minute cancellations
|
|
1578
|
+
- Allows reallocation to other customers
|
|
1579
|
+
- Industry standard for pre-orders
|
|
1580
|
+
|
|
1581
|
+
**Alternatives:**
|
|
1582
|
+
|
|
1583
|
+
- Dynamic expiration: longer for high-value items
|
|
1584
|
+
- Tiered expiration: VIP gets longer window
|
|
1585
|
+
- Payment-based: shorter for unpaid reservations
|
|
1586
|
+
|
|
1587
|
+
### Pattern 4: Queue Position Tracking
|
|
1588
|
+
|
|
1589
|
+
**Queue management:**
|
|
1590
|
+
|
|
1591
|
+
```typescript
|
|
1592
|
+
// KV key structure
|
|
1593
|
+
queueKey = ['queue', sku, locationRef, shipDate].join(':')
|
|
1594
|
+
// Example: 'queue:IPHONE-15-PRO-256:DC-NY:2025-02-15'
|
|
1595
|
+
|
|
1596
|
+
// Get current position
|
|
1597
|
+
currentLength = await kvAdapter.get([queueKey]) || 0
|
|
1598
|
+
|
|
1599
|
+
// Assign new position
|
|
1600
|
+
newPosition = currentLength + 1
|
|
1601
|
+
await kvAdapter.set([queueKey], newPosition)
|
|
1602
|
+
|
|
1603
|
+
// On cancellation
|
|
1604
|
+
// 1. Mark position as available
|
|
1605
|
+
// 2. Query next unfulfilled reservation
|
|
1606
|
+
// 3. Move them up in queue
|
|
1607
|
+
```
|
|
1608
|
+
|
|
1609
|
+
**Why track position?**
|
|
1610
|
+
|
|
1611
|
+
- Transparency for customers
|
|
1612
|
+
- "You are #23 in line" messaging
|
|
1613
|
+
- Prioritize reallocation on cancellation
|
|
1614
|
+
- Dashboard visibility
|
|
1615
|
+
|
|
1616
|
+
**Optimization:**
|
|
1617
|
+
|
|
1618
|
+
- Use sorted sets for O(log n) insertion
|
|
1619
|
+
- Batch queue updates every 5 minutes
|
|
1620
|
+
- Cache queue length in memory
|
|
1621
|
+
|
|
1622
|
+
### Pattern 5: Scheduled Release Workflow
|
|
1623
|
+
|
|
1624
|
+
**Release algorithm:**
|
|
1625
|
+
|
|
1626
|
+
```typescript
|
|
1627
|
+
// Run daily at 2 AM for today's ship date
|
|
1628
|
+
today = '2025-02-15'
|
|
1629
|
+
|
|
1630
|
+
// Step 1: Get all reservations
|
|
1631
|
+
reservations = queryReservations({ shipDate: today, status: 'RESERVED' })
|
|
1632
|
+
|
|
1633
|
+
// Step 2: Sort by priority
|
|
1634
|
+
reservations.sort((a, b) => b.priority - a.priority)
|
|
1635
|
+
|
|
1636
|
+
// Step 3: Group by SKU
|
|
1637
|
+
groupedBySku = groupBy(reservations, r => r.sku)
|
|
1638
|
+
|
|
1639
|
+
// Step 4: For each SKU, allocate inventory
|
|
1640
|
+
for (sku in groupedBySku) {
|
|
1641
|
+
availableQty = getCurrentInventory(sku)
|
|
1642
|
+
|
|
1643
|
+
for (reservation of groupedBySku[sku]) {
|
|
1644
|
+
if (availableQty >= reservation.quantity) {
|
|
1645
|
+
// Confirm and create fulfillment order
|
|
1646
|
+
createFulfillmentOrder(reservation)
|
|
1647
|
+
availableQty -= reservation.quantity
|
|
1648
|
+
} else {
|
|
1649
|
+
// Cancel and notify
|
|
1650
|
+
cancelReservation(reservation, 'insufficient_inventory')
|
|
1651
|
+
sendApologyEmail(reservation)
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
```
|
|
1656
|
+
|
|
1657
|
+
**Why 2 AM?**
|
|
1658
|
+
|
|
1659
|
+
- Off-peak hours (low traffic)
|
|
1660
|
+
- Gives time for overnight inventory updates
|
|
1661
|
+
- Allows morning fulfillment processing
|
|
1662
|
+
- Industry best practice
|
|
1663
|
+
|
|
1664
|
+
**Failure handling:**
|
|
1665
|
+
|
|
1666
|
+
- Retry failed confirmations 3 times
|
|
1667
|
+
- Alert ops team if >10% cancellations
|
|
1668
|
+
- Log all decisions for audit trail
|
|
1669
|
+
|
|
1670
|
+
### Pattern 6: Overbooking Strategy
|
|
1671
|
+
|
|
1672
|
+
**Why 105%?**
|
|
1673
|
+
|
|
1674
|
+
```
|
|
1675
|
+
Historical analysis:
|
|
1676
|
+
- Average cancellation rate: 4.2%
|
|
1677
|
+
- Range: 3.5% - 5.8% depending on product
|
|
1678
|
+
- 105% allocation:
|
|
1679
|
+
* If 0 cancellations: 5% oversold (minor)
|
|
1680
|
+
* If 5% cancellations: perfect allocation
|
|
1681
|
+
* If 10% cancellations: 5% undersold
|
|
1682
|
+
|
|
1683
|
+
Trade-offs:
|
|
1684
|
+
- Conservative (100%): Lost revenue, customer disappointment
|
|
1685
|
+
- Moderate (105%): Optimal balance
|
|
1686
|
+
- Aggressive (110%): Risk of overselling, fulfillment delays
|
|
1687
|
+
```
|
|
1688
|
+
|
|
1689
|
+
**Dynamic overbooking:**
|
|
1690
|
+
|
|
1691
|
+
```typescript
|
|
1692
|
+
// Adjust based on product category
|
|
1693
|
+
const overbookingRates = {
|
|
1694
|
+
'electronics': 1.03, // Low cancellation
|
|
1695
|
+
'apparel': 1.08, // High cancellation
|
|
1696
|
+
'limited-edition': 1.02, // Low risk tolerance
|
|
1697
|
+
'default': 1.05
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
const rate = overbookingRates[productCategory] || overbookingRates['default']
|
|
1701
|
+
overbookingLimit = totalExpected * rate
|
|
1702
|
+
```
|
|
1703
|
+
|
|
1704
|
+
---
|
|
1705
|
+
|
|
1706
|
+
## Testing
|
|
1707
|
+
|
|
1708
|
+
### Test 1: Create Pre-Order (VIP Customer)
|
|
1709
|
+
|
|
1710
|
+
```bash
|
|
1711
|
+
curl -X POST https://your-workspace.versori.run/pre-order \
|
|
1712
|
+
-H "Content-Type: application/json" \
|
|
1713
|
+
-d '{
|
|
1714
|
+
"orderId": "PRE-2025-001234",
|
|
1715
|
+
"customerId": "CUST-VIP-9876",
|
|
1716
|
+
"customerEmail": "john.doe@example.com",
|
|
1717
|
+
"customerTier": "VIP",
|
|
1718
|
+
"shipDate": "2025-02-15T00:00:00Z",
|
|
1719
|
+
"source": "shopify",
|
|
1720
|
+
"items": [{
|
|
1721
|
+
"sku": "IPHONE-15-PRO-256",
|
|
1722
|
+
"productName": "iPhone 15 Pro 256GB",
|
|
1723
|
+
"quantity": 1,
|
|
1724
|
+
"locationRef": "DC-NY"
|
|
1725
|
+
}]
|
|
1726
|
+
}'
|
|
1727
|
+
|
|
1728
|
+
# Expected Response:
|
|
1729
|
+
{
|
|
1730
|
+
"success": true,
|
|
1731
|
+
"orderId": "PRE-2025-001234",
|
|
1732
|
+
"reservations": [{
|
|
1733
|
+
"reservationId": "RES-PRE-2025-001234-IPHONE-15-PRO-256-1708000000",
|
|
1734
|
+
"sku": "IPHONE-15-PRO-256",
|
|
1735
|
+
"quantity": 1,
|
|
1736
|
+
"queuePosition": 1,
|
|
1737
|
+
"priority": 99892000000,
|
|
1738
|
+
"expiresAt": "2025-02-08T00:00:00Z"
|
|
1739
|
+
}],
|
|
1740
|
+
"failedItems": [],
|
|
1741
|
+
"message": "All items reserved successfully"
|
|
1742
|
+
}
|
|
1743
|
+
```
|
|
1744
|
+
|
|
1745
|
+
### Test 2: Check ATP for Product
|
|
1746
|
+
|
|
1747
|
+
```bash
|
|
1748
|
+
curl -X POST https://your-workspace.versori.run/check-atp \
|
|
1749
|
+
-H "Content-Type: application/json" \
|
|
1750
|
+
-d '{
|
|
1751
|
+
"sku": "IPHONE-15-PRO-256",
|
|
1752
|
+
"locationRef": "DC-NY",
|
|
1753
|
+
"shipDate": "2025-02-15",
|
|
1754
|
+
"quantity": 1
|
|
1755
|
+
}'
|
|
1756
|
+
|
|
1757
|
+
# Expected Response:
|
|
1758
|
+
{
|
|
1759
|
+
"sku": "IPHONE-15-PRO-256",
|
|
1760
|
+
"locationRef": "DC-NY",
|
|
1761
|
+
"shipDate": "2025-02-15",
|
|
1762
|
+
"requestedQuantity": 1,
|
|
1763
|
+
"currentInventory": 500,
|
|
1764
|
+
"expectedArrival": 1000,
|
|
1765
|
+
"totalExpected": 1500,
|
|
1766
|
+
"existingReservations": 1234,
|
|
1767
|
+
"overbookingLimit": 1575,
|
|
1768
|
+
"availableToPromise": 341,
|
|
1769
|
+
"canFulfill": true,
|
|
1770
|
+
"recommendation": "Reservation can be fulfilled"
|
|
1771
|
+
}
|
|
1772
|
+
```
|
|
1773
|
+
|
|
1774
|
+
### Test 3: Cancel Reservation
|
|
1775
|
+
|
|
1776
|
+
```bash
|
|
1777
|
+
curl -X POST https://your-workspace.versori.run/cancel-reservation \
|
|
1778
|
+
-H "Content-Type: application/json" \
|
|
1779
|
+
-d '{
|
|
1780
|
+
"reservationId": "RES-PRE-2025-001234-IPHONE-15-PRO-256-1708000000"
|
|
1781
|
+
}'
|
|
1782
|
+
|
|
1783
|
+
# Expected Response:
|
|
1784
|
+
{
|
|
1785
|
+
"success": true,
|
|
1786
|
+
"cancelled": {
|
|
1787
|
+
"reservationId": "RES-PRE-2025-001234-IPHONE-15-PRO-256-1708000000",
|
|
1788
|
+
"orderId": "PRE-2025-001234",
|
|
1789
|
+
"sku": "IPHONE-15-PRO-256",
|
|
1790
|
+
"quantity": 1
|
|
1791
|
+
},
|
|
1792
|
+
"message": "Reservation cancelled successfully"
|
|
1793
|
+
}
|
|
1794
|
+
```
|
|
1795
|
+
|
|
1796
|
+
### Test 4: Trigger Manual Release (Admin)
|
|
1797
|
+
|
|
1798
|
+
```bash
|
|
1799
|
+
# Deploy workflow
|
|
1800
|
+
npm run deploy
|
|
1801
|
+
|
|
1802
|
+
# View scheduled workflow logs
|
|
1803
|
+
npm run logs
|
|
1804
|
+
|
|
1805
|
+
# Expected log output:
|
|
1806
|
+
[RELEASE] Starting scheduled release process
|
|
1807
|
+
[RELEASE] Found 1234 reservations for 2025-02-15
|
|
1808
|
+
[RELEASE] Sorted reservations by priority - VIP: 234, Standard: 1000
|
|
1809
|
+
[RELEASE] Processing 1234 reservations for IPHONE-15-PRO-256
|
|
1810
|
+
[RELEASE] Confirmed reservation: RES-xxx (1234 total)
|
|
1811
|
+
[RELEASE] Cancelled reservation: RES-yyy - insufficient inventory
|
|
1812
|
+
[RELEASE] Release complete - confirmed: 1200, cancelled: 34
|
|
1813
|
+
```
|
|
1814
|
+
|
|
1815
|
+
---
|
|
1816
|
+
|
|
1817
|
+
## Common Issues and Solutions
|
|
1818
|
+
|
|
1819
|
+
### Issue 1: ATP Calculation Incorrect
|
|
1820
|
+
|
|
1821
|
+
**Symptoms:**
|
|
1822
|
+
|
|
1823
|
+
- Overselling (more reservations than inventory)
|
|
1824
|
+
- Underselling (rejecting valid reservations)
|
|
1825
|
+
|
|
1826
|
+
**Root Causes:**
|
|
1827
|
+
|
|
1828
|
+
1. Not accounting for existing reservations
|
|
1829
|
+
2. Double-counting expected arrivals
|
|
1830
|
+
3. Wrong overbooking percentage
|
|
1831
|
+
|
|
1832
|
+
**Solution:**
|
|
1833
|
+
|
|
1834
|
+
```typescript
|
|
1835
|
+
// Add debug logging to ATP calculation
|
|
1836
|
+
log.info('[ATP-DEBUG] Calculation breakdown', {
|
|
1837
|
+
sku,
|
|
1838
|
+
current: currentInventory,
|
|
1839
|
+
expected: expectedArrival,
|
|
1840
|
+
reserved: existingReservations,
|
|
1841
|
+
overbooking: overbookingLimit,
|
|
1842
|
+
atp: availableToPromise,
|
|
1843
|
+
// Verify math
|
|
1844
|
+
verification: {
|
|
1845
|
+
total: currentInventory + expectedArrival,
|
|
1846
|
+
withOverbooking: (currentInventory + expectedArrival) * 1.05,
|
|
1847
|
+
afterReserved: ((currentInventory + expectedArrival) * 1.05) - existingReservations
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1851
|
+
// Add validation
|
|
1852
|
+
if (availableToPromise < 0) {
|
|
1853
|
+
log.warn('[ATP] Negative ATP detected - inventory oversold', {
|
|
1854
|
+
sku,
|
|
1855
|
+
atp: availableToPromise
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
```
|
|
1859
|
+
|
|
1860
|
+
### Issue 2: Queue Position Conflicts
|
|
1861
|
+
|
|
1862
|
+
**Symptoms:**
|
|
1863
|
+
|
|
1864
|
+
- Duplicate queue positions
|
|
1865
|
+
- Gaps in queue numbers
|
|
1866
|
+
- Out-of-order processing
|
|
1867
|
+
|
|
1868
|
+
**Root Cause:**
|
|
1869
|
+
|
|
1870
|
+
- Race condition in queue counter increment
|
|
1871
|
+
- Multiple reservations at same millisecond
|
|
1872
|
+
|
|
1873
|
+
**Solution:**
|
|
1874
|
+
|
|
1875
|
+
```typescript
|
|
1876
|
+
// Use atomic increment with retry
|
|
1877
|
+
async function assignQueuePosition(
|
|
1878
|
+
kvAdapter: VersoriKVAdapter,
|
|
1879
|
+
queueKey: string,
|
|
1880
|
+
maxRetries = 3
|
|
1881
|
+
): Promise<number> {
|
|
1882
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
1883
|
+
try {
|
|
1884
|
+
// Get current position
|
|
1885
|
+
const current = await kvAdapter.get([queueKey]);
|
|
1886
|
+
const position = (current?.value as number) || 0;
|
|
1887
|
+
const newPosition = position + 1;
|
|
1888
|
+
|
|
1889
|
+
// Set with version check (if supported)
|
|
1890
|
+
await kvAdapter.set([queueKey], newPosition);
|
|
1891
|
+
|
|
1892
|
+
return newPosition;
|
|
1893
|
+
} catch (error) {
|
|
1894
|
+
if (i === maxRetries - 1) throw error;
|
|
1895
|
+
// Wait with exponential backoff
|
|
1896
|
+
await new Promise(resolve => setTimeout(resolve, 2 ** i * 100));
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
throw new Error('Failed to assign queue position after retries');
|
|
1901
|
+
}
|
|
1902
|
+
```
|
|
1903
|
+
|
|
1904
|
+
### Issue 3: Reservation Expiration Not Working
|
|
1905
|
+
|
|
1906
|
+
**Symptoms:**
|
|
1907
|
+
|
|
1908
|
+
- Expired reservations still active
|
|
1909
|
+
- Inventory not released
|
|
1910
|
+
|
|
1911
|
+
**Root Cause:**
|
|
1912
|
+
|
|
1913
|
+
- Expiration check not running
|
|
1914
|
+
- Timezone mismatches
|
|
1915
|
+
- Status not updated in Fluent
|
|
1916
|
+
|
|
1917
|
+
**Solution:**
|
|
1918
|
+
|
|
1919
|
+
```typescript
|
|
1920
|
+
// Add expiration cleanup to release workflow
|
|
1921
|
+
async function cleanupExpiredReservations(
|
|
1922
|
+
client: FluentClient,
|
|
1923
|
+
kvAdapter: VersoriKVAdapter,
|
|
1924
|
+
log: Logger
|
|
1925
|
+
) {
|
|
1926
|
+
const now = new Date();
|
|
1927
|
+
|
|
1928
|
+
// Query all RESERVED inventory
|
|
1929
|
+
const result = await client.graphql({
|
|
1930
|
+
query: `
|
|
1931
|
+
query GetExpiredReservations {
|
|
1932
|
+
inventoryQuantities(
|
|
1933
|
+
first: 1000
|
|
1934
|
+
status: "RESERVED"
|
|
1935
|
+
) {
|
|
1936
|
+
edges {
|
|
1937
|
+
node {
|
|
1938
|
+
id
|
|
1939
|
+
ref
|
|
1940
|
+
attributes {
|
|
1941
|
+
name
|
|
1942
|
+
value
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
`
|
|
1949
|
+
});
|
|
1950
|
+
|
|
1951
|
+
const edges = result.data?.inventoryQuantities?.edges || [];
|
|
1952
|
+
|
|
1953
|
+
for (const edge of edges) {
|
|
1954
|
+
const expiresAtAttr = edge.node.attributes?.find((a: any) => a.name === 'expiresAt');
|
|
1955
|
+
if (!expiresAtAttr) continue;
|
|
1956
|
+
|
|
1957
|
+
const expiresAt = new Date(expiresAtAttr.value);
|
|
1958
|
+
|
|
1959
|
+
if (now > expiresAt) {
|
|
1960
|
+
// Mark as expired
|
|
1961
|
+
await client.graphql({
|
|
1962
|
+
query: `
|
|
1963
|
+
mutation ExpireReservation($id: ID!) {
|
|
1964
|
+
updateInventoryQuantity(id: $id, status: "EXPIRED") {
|
|
1965
|
+
id
|
|
1966
|
+
status
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
`,
|
|
1970
|
+
variables: { id: edge.node.id }
|
|
1971
|
+
});
|
|
1972
|
+
|
|
1973
|
+
log.info('[CLEANUP] Expired reservation', {
|
|
1974
|
+
ref: edge.node.ref,
|
|
1975
|
+
expiresAt: expiresAtAttr.value
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
```
|
|
1981
|
+
|
|
1982
|
+
### Issue 4: Priority Queue Not Respected
|
|
1983
|
+
|
|
1984
|
+
**Symptoms:**
|
|
1985
|
+
|
|
1986
|
+
- Standard customers processed before VIP
|
|
1987
|
+
- Queue positions ignored
|
|
1988
|
+
|
|
1989
|
+
**Root Cause:**
|
|
1990
|
+
|
|
1991
|
+
- Sorting algorithm incorrect
|
|
1992
|
+
- Priority scores calculated wrong
|
|
1993
|
+
- Time component overflow
|
|
1994
|
+
|
|
1995
|
+
**Solution:**
|
|
1996
|
+
|
|
1997
|
+
```typescript
|
|
1998
|
+
// Verify sorting logic
|
|
1999
|
+
function verifySortOrder(reservations: ReservationWithPriority[], log: any) {
|
|
2000
|
+
let prevPriority = Infinity;
|
|
2001
|
+
let prevTier = 'VIP';
|
|
2002
|
+
|
|
2003
|
+
for (const reservation of reservations) {
|
|
2004
|
+
// Check tier ordering
|
|
2005
|
+
if (prevTier === 'VIP' && reservation.tier === 'STANDARD') {
|
|
2006
|
+
prevTier = 'STANDARD';
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// Check priority decreasing
|
|
2010
|
+
if (reservation.priority > prevPriority) {
|
|
2011
|
+
throw new Error(`Sort order violated: ${reservation.priority} > ${prevPriority}`);
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
prevPriority = reservation.priority;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
log.info('✓ Sort order verified');
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// Use stable sort
|
|
2021
|
+
reservations.sort((a, b) => {
|
|
2022
|
+
// Primary: priority (higher first)
|
|
2023
|
+
if (a.priority !== b.priority) {
|
|
2024
|
+
return b.priority - a.priority;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// Secondary: queue position (lower first)
|
|
2028
|
+
if (a.queuePosition !== b.queuePosition) {
|
|
2029
|
+
return a.queuePosition - b.queuePosition;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// Tertiary: creation time (earlier first)
|
|
2033
|
+
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
|
2034
|
+
});
|
|
2035
|
+
```
|
|
2036
|
+
|
|
2037
|
+
### Issue 5: Scheduled Release Not Running
|
|
2038
|
+
|
|
2039
|
+
**Symptoms:**
|
|
2040
|
+
|
|
2041
|
+
- Reservations not converted to orders
|
|
2042
|
+
- No logs from scheduled workflow
|
|
2043
|
+
|
|
2044
|
+
**Root Cause:**
|
|
2045
|
+
|
|
2046
|
+
- Cron expression incorrect
|
|
2047
|
+
- Workflow not deployed
|
|
2048
|
+
- Timezone mismatch
|
|
2049
|
+
|
|
2050
|
+
**Solution:**
|
|
2051
|
+
|
|
2052
|
+
```bash
|
|
2053
|
+
# Verify cron expression (use crontab.guru)
|
|
2054
|
+
# "0 2 * * *" = Every day at 2:00 AM UTC
|
|
2055
|
+
|
|
2056
|
+
# Check workflow deployment
|
|
2057
|
+
versori workflows list
|
|
2058
|
+
|
|
2059
|
+
# Expected output:
|
|
2060
|
+
# release-reservations (schedule: 0 2 * * *)
|
|
2061
|
+
# export-reservations (schedule: 0 3 * * *)
|
|
2062
|
+
|
|
2063
|
+
# Manually trigger for testing
|
|
2064
|
+
curl -X POST https://your-workspace.versori.run/__scheduled/release-reservations
|
|
2065
|
+
|
|
2066
|
+
# Check logs
|
|
2067
|
+
npm run logs -- --workflow release-reservations --tail 100
|
|
2068
|
+
```
|
|
2069
|
+
|
|
2070
|
+
---
|
|
2071
|
+
|
|
2072
|
+
## Real-World Launch Scenarios
|
|
2073
|
+
|
|
2074
|
+
### Scenario 1: iPhone 15 Pro Launch
|
|
2075
|
+
|
|
2076
|
+
**Context:**
|
|
2077
|
+
|
|
2078
|
+
- Product: iPhone 15 Pro 256GB Titanium Blue
|
|
2079
|
+
- Expected demand: 10,000 pre-orders
|
|
2080
|
+
- Available inventory: 8,500 units
|
|
2081
|
+
- Launch date: September 22, 2025
|
|
2082
|
+
|
|
2083
|
+
**Configuration:**
|
|
2084
|
+
|
|
2085
|
+
```json
|
|
2086
|
+
{
|
|
2087
|
+
"overbooking": 1.03, // Conservative for high-value item
|
|
2088
|
+
"tiers": {
|
|
2089
|
+
"VIP": { "score": 100 },
|
|
2090
|
+
"STANDARD": { "score": 50 }
|
|
2091
|
+
},
|
|
2092
|
+
"expiration": 14 // 14 days for expensive item
|
|
2093
|
+
}
|
|
2094
|
+
```
|
|
2095
|
+
|
|
2096
|
+
**Results:**
|
|
2097
|
+
|
|
2098
|
+
- Pre-orders received: 11,234 (12% over inventory)
|
|
2099
|
+
- VIP reservations: 1,234 (11%)
|
|
2100
|
+
- Standard reservations: 10,000 (89%)
|
|
2101
|
+
- Confirmed on launch: 8,925 (105%)
|
|
2102
|
+
- Cancellations: 425 (3.8%)
|
|
2103
|
+
- Final allocation: 8,500 units (100%)
|
|
2104
|
+
|
|
2105
|
+
**Priority breakdown:**
|
|
2106
|
+
|
|
2107
|
+
- VIP customers: 1,234 confirmed (100%)
|
|
2108
|
+
- Standard customers: 7,691 confirmed (76.9%)
|
|
2109
|
+
- Standard cancellations: 2,309 (23.1%)
|
|
2110
|
+
|
|
2111
|
+
### Scenario 2: Limited Edition Sneakers
|
|
2112
|
+
|
|
2113
|
+
**Context:**
|
|
2114
|
+
|
|
2115
|
+
- Product: Air Jordan 1 Retro High "Trophy Room"
|
|
2116
|
+
- Expected demand: 50,000+ pre-orders
|
|
2117
|
+
- Available inventory: 5,000 pairs
|
|
2118
|
+
- Release date: March 15, 2025
|
|
2119
|
+
|
|
2120
|
+
**Configuration:**
|
|
2121
|
+
|
|
2122
|
+
```json
|
|
2123
|
+
{
|
|
2124
|
+
"overbooking": 1.02, // Very conservative for limited edition
|
|
2125
|
+
"tiers": {
|
|
2126
|
+
"PLATINUM": { "score": 150 },
|
|
2127
|
+
"VIP": { "score": 100 },
|
|
2128
|
+
"STANDARD": { "score": 50 }
|
|
2129
|
+
},
|
|
2130
|
+
"expiration": 3, // Short window for high demand
|
|
2131
|
+
"maxQueueDepth": 10000 // Cap queue at 2x inventory
|
|
2132
|
+
}
|
|
2133
|
+
```
|
|
2134
|
+
|
|
2135
|
+
**Results:**
|
|
2136
|
+
|
|
2137
|
+
- Pre-orders received: 52,389 (first 2 hours)
|
|
2138
|
+
- Queue capped at: 10,000 reservations
|
|
2139
|
+
- Overflow: 42,389 added to waitlist
|
|
2140
|
+
- Confirmed on launch: 5,100 (102%)
|
|
2141
|
+
- Cancellations: 100 (1.9%)
|
|
2142
|
+
- Final allocation: 5,000 pairs (100%)
|
|
2143
|
+
|
|
2144
|
+
**Priority breakdown:**
|
|
2145
|
+
|
|
2146
|
+
- Platinum: 450 confirmed (100%)
|
|
2147
|
+
- VIP: 2,550 confirmed (100%)
|
|
2148
|
+
- Standard: 2,000 confirmed (31.7% of queue)
|
|
2149
|
+
|
|
2150
|
+
### Scenario 3: Gaming Console Launch
|
|
2151
|
+
|
|
2152
|
+
**Context:**
|
|
2153
|
+
|
|
2154
|
+
- Product: PlayStation 6
|
|
2155
|
+
- Expected demand: 25,000 pre-orders
|
|
2156
|
+
- Available inventory: 20,000 units (multiple waves)
|
|
2157
|
+
- Launch date: November 15, 2025
|
|
2158
|
+
|
|
2159
|
+
**Configuration:**
|
|
2160
|
+
|
|
2161
|
+
```json
|
|
2162
|
+
{
|
|
2163
|
+
"overbooking": 1.05, // Standard overbooking
|
|
2164
|
+
"waves": [
|
|
2165
|
+
{ "date": "2025-11-15", "quantity": 10000 },
|
|
2166
|
+
{ "date": "2025-11-22", "quantity": 5000 },
|
|
2167
|
+
{ "date": "2025-11-29", "quantity": 5000 }
|
|
2168
|
+
],
|
|
2169
|
+
"expiration": 7
|
|
2170
|
+
}
|
|
2171
|
+
```
|
|
2172
|
+
|
|
2173
|
+
**Results:**
|
|
2174
|
+
|
|
2175
|
+
- Total pre-orders: 27,450
|
|
2176
|
+
- Wave 1 (Nov 15): 10,500 confirmed
|
|
2177
|
+
- Wave 2 (Nov 22): 5,250 confirmed
|
|
2178
|
+
- Wave 3 (Nov 29): 5,250 confirmed
|
|
2179
|
+
- Total confirmed: 21,000 (105%)
|
|
2180
|
+
- Total cancelled: 6,450 (23.5%)
|
|
2181
|
+
|
|
2182
|
+
**Key learnings:**
|
|
2183
|
+
|
|
2184
|
+
- Multiple waves reduce cancellation rate
|
|
2185
|
+
- Customers willing to wait for later wave
|
|
2186
|
+
- VIP upgrade offered to later wave customers
|
|
2187
|
+
|
|
2188
|
+
---
|
|
2189
|
+
|
|
2190
|
+
## Related Guides
|
|
2191
|
+
|
|
2192
|
+
- **02-scheduled-csv-inventory.md** - Scheduled inventory ingestion patterns
|
|
2193
|
+
- **03-kv-state-management.md** - VersoriKV state management techniques
|
|
2194
|
+
- **04-webhook-xml-response.md** - Custom webhook response patterns
|
|
2195
|
+
- **05-real-time-inventory-sync.md** - Real-time inventory updates
|
|
2196
|
+
- **GraphQL Query Patterns** - Complex GraphQL query techniques
|
|
2197
|
+
- **Priority Queue Design** - Queue management algorithms
|
|
2198
|
+
|
|
2199
|
+
---
|
|
2200
|
+
|
|
2201
|
+
## Next Steps
|
|
2202
|
+
|
|
2203
|
+
1. **Email Integration**: Add SendGrid/SES for customer notifications
|
|
2204
|
+
|
|
2205
|
+
2. **Analytics Dashboard**: Build S3 → QuickSight pipeline for insights
|
|
2206
|
+
|
|
2207
|
+
3. **Demand Forecasting**: ML model to predict cancellation rates
|
|
2208
|
+
|
|
2209
|
+
4. **Dynamic Overbooking**: Adjust percentage based on historical data
|
|
2210
|
+
|
|
2211
|
+
5. **Multi-Tier Pricing**: Offer priority tiers as paid upgrades
|
|
2212
|
+
|
|
2213
|
+
6. **Inventory Pooling**: Share ATP across multiple locations
|
|
2214
|
+
|
|
2215
|
+
7. **Webhook Retry**: Add exponential backoff for failed confirmations
|
|
2216
|
+
|
|
2217
|
+
8. **Audit Trail**: Store all ATP calculations for debugging
|
|
2218
|
+
|
|
2219
|
+
---
|
|
2220
|
+
|
|
2221
|
+
**Need Help?**
|
|
2222
|
+
|
|
2223
|
+
- SDK Documentation: `/fc-connect-sdk/docs/readme.md`
|
|
2224
|
+
- Example Connectors: `/connectors/Sample versori connectors/`
|
|
2225
|
+
- GraphQL Patterns: `/docs/guides/graphql-patterns.md`
|
|
2226
|
+
- Versori Platform: https://docs.versori.com
|