@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +11 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/utils/pagination-helpers.js +38 -2
- package/dist/esm/versori/fluent-versori-client.js +11 -5
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
- package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
- package/docs/00-START-HERE/cli-documentation-index.md +202 -202
- package/docs/00-START-HERE/cli-quick-reference.md +252 -252
- package/docs/00-START-HERE/decision-tree.md +552 -552
- package/docs/00-START-HERE/getting-started.md +1070 -1070
- package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
- package/docs/00-START-HERE/readme.md +237 -237
- package/docs/00-START-HERE/retailerid-configuration.md +404 -404
- package/docs/00-START-HERE/sdk-philosophy.md +794 -794
- package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
- package/docs/01-TEMPLATES/faq.md +686 -686
- package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
- package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
- package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
- package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
- package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
- package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
- package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
- package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
- package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
- package/docs/01-TEMPLATES/readme.md +957 -957
- package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
- package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
- package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
- package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
- package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
- package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
- package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
- package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
- package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
- package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
- package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
- package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
- package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
- package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
- package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
- package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
- package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
- package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
- package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -520
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
- package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
- package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
- package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
- package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
- package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
- package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
- package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
- package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
- package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
- package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
- package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
- package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
- package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
- package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
- package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
- package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
- package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
- package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
- package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
- package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
- package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
- package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
- package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
- package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
- package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
- package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
- package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
- package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
- package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
- package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
- package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
- package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
- package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
- package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
- package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
- package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
- package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
- package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
- package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
- package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
- package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
- package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
- package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
- package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
- package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
- package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
- package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
- package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
- package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
- package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
- package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
- package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
- package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
- package/docs/02-CORE-GUIDES/readme.md +194 -194
- package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
- package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
- package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
- package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
- package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
- package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
- package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
- package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
- package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
- package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
- package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
- package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
- package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
- package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
- package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
- package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
- package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
- package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
- package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
- package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
- package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
- package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
- package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
- package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
- package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
- package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
- package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/readme.md +159 -159
- package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
- package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
- package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
- package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
- package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
- package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
- package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
- package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
- package/docs/04-REFERENCE/architecture/readme.md +279 -279
- package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
- package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
- package/docs/04-REFERENCE/platforms/readme.md +135 -135
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
- package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
- package/docs/04-REFERENCE/readme.md +148 -148
- package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
- package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
- package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
- package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
- package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
- package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
- package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
- package/docs/04-REFERENCE/schema/readme.md +141 -141
- package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
- package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
- package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
- package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
- package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
- package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
- package/docs/04-REFERENCE/testing/readme.md +86 -86
- package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
- package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
- package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
- package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
- package/docs/template-loading-matrix.md +242 -242
- package/package.json +5 -3
- package/docs/02-CORE-GUIDES/api-reference/cli-profile-integration.md +0 -377
|
@@ -1,1395 +1,1395 @@
|
|
|
1
|
-
---
|
|
2
|
-
template_id: tpl-webhook-flash-sale-reserve
|
|
3
|
-
canonical_filename: template-webhook-flash-sale-reserve.md
|
|
4
|
-
version: 2.0.0
|
|
5
|
-
sdk_version: ^0.1.39
|
|
6
|
-
runtime: versori
|
|
7
|
-
direction: ingestion
|
|
8
|
-
source: webhook-json-promotion
|
|
9
|
-
destination: fluent-graphql
|
|
10
|
-
entity: inventory-reservation
|
|
11
|
-
format: json
|
|
12
|
-
logging: versori
|
|
13
|
-
status: stable
|
|
14
|
-
features:
|
|
15
|
-
- webhook-signature-validation
|
|
16
|
-
- batched-events
|
|
17
|
-
- attribute-transformation
|
|
18
|
-
- memory-management
|
|
19
|
-
- enhanced-logging
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
# Template: Webhook - Flash Sale Inventory Reserve
|
|
23
|
-
|
|
24
|
-
**Template Version:** 2.0.0
|
|
25
|
-
**SDK Version:** @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
26
|
-
**Last Updated:** 2025-01-24
|
|
27
|
-
**Deployment Target:** Versori Platform
|
|
28
|
-
|
|
29
|
-
**🆕 Version 2.0.0 Enhancements:**
|
|
30
|
-
- ✅ **Webhook Signature Validation** - Secure webhook verification with HMAC-SHA256
|
|
31
|
-
- ✅ **Batched Events** - Process events in optimized batches to reduce API calls
|
|
32
|
-
- ✅ **Attribute Transformation** - Handle nested arrays and complex data structures
|
|
33
|
-
- ✅ **Memory Management** - Clear large arrays after processing batches
|
|
34
|
-
- ✅ **Enhanced Logging** - Track batch processing and event submission with emoji indicators
|
|
35
|
-
|
|
36
|
-
**FC Connect SDK Use Case Guide**
|
|
37
|
-
|
|
38
|
-
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
39
|
-
> **Version**: Use latest - `npm install @fluentcommerce/fc-connect-sdk@latest`
|
|
40
|
-
|
|
41
|
-
**Context**: Reserve inventory for time-limited flash sales, prevent overselling during high-concurrency events, auto-release unreserved inventory after timer expires, and reconcile at sale end.
|
|
42
|
-
|
|
43
|
-
**Complexity**: High
|
|
44
|
-
|
|
45
|
-
**Runtime**: Versori Platform
|
|
46
|
-
|
|
47
|
-
**Estimated Lines**: ~800 lines (modular structure)
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
|
|
51
|
-
## STEP 1: Understand This Template
|
|
52
|
-
|
|
53
|
-
**What This Template Does:**
|
|
54
|
-
|
|
55
|
-
- HTTP webhook triggered by promotion start event
|
|
56
|
-
- Inventory reservation with GraphQL mutations
|
|
57
|
-
- Time-bound reservations (auto-release after X minutes)
|
|
58
|
-
- Concurrency control to prevent race conditions
|
|
59
|
-
- Real-time order consumption of reservations
|
|
60
|
-
- Auto-release of unused reservations after expiry
|
|
61
|
-
- Reconciliation report after sale ends
|
|
62
|
-
- State tracking for reservation management
|
|
63
|
-
- **Sync + Fire-and-Forget Pattern**: Fast webhook response, background processing
|
|
64
|
-
|
|
65
|
-
**Key SDK Components:**
|
|
66
|
-
|
|
67
|
-
- `createClient()` - Universal client factory (auto-detects Versori context)
|
|
68
|
-
- `VersoriKVAdapter` - Reservation state storage (KV)
|
|
69
|
-
- `StateService` - Concurrency control
|
|
70
|
-
- GraphQL mutations - Reserve and consume inventory
|
|
71
|
-
- Native Versori `log` - Use `log` from context
|
|
72
|
-
|
|
73
|
-
**Entity Type:**
|
|
74
|
-
|
|
75
|
-
- **InventoryReservation** - Fluent entity for inventory reservations
|
|
76
|
-
- **GraphQL Mutations** - Reserve inventory, consume reservations
|
|
77
|
-
|
|
78
|
-
**Critical Patterns:**
|
|
79
|
-
|
|
80
|
-
- **Sync + Fire-and-Forget**: Webhook validates quickly, returns immediately, processes reservations in background
|
|
81
|
-
- **External JSON Config**: Reservation configuration in separate JSON file (`config/reservation-config.json`)
|
|
82
|
-
- **Modular Architecture**: Separate services, workflows, config, types folders
|
|
83
|
-
- **Background Processing**: Long-running operations (GraphQL mutations, state updates) happen asynchronously
|
|
84
|
-
- **State Management**: KV storage for reservation tracking across webhooks
|
|
85
|
-
|
|
86
|
-
**When to Use This Template:**
|
|
87
|
-
|
|
88
|
-
- ✅ Flash sale inventory reservations
|
|
89
|
-
- ✅ Time-bound inventory holds
|
|
90
|
-
- ✅ High-concurrency scenarios
|
|
91
|
-
- ✅ Need fast webhook response (don't wait for reservation creation)
|
|
92
|
-
- ✅ Auto-release expired reservations
|
|
93
|
-
|
|
94
|
-
**When NOT to Use:**
|
|
95
|
-
|
|
96
|
-
- ❌ Permanent inventory allocation (use direct inventory updates)
|
|
97
|
-
- ❌ Bulk reservation processing (use Batch API or scheduled workflows)
|
|
98
|
-
- ❌ Need synchronous reservation (wait for result before responding)
|
|
99
|
-
|
|
100
|
-
---
|
|
101
|
-
|
|
102
|
-
## STEP 2: Implementation Prompt for Claude Code
|
|
103
|
-
|
|
104
|
-
**Copy this prompt and send to Claude Code to generate the complete implementation:**
|
|
105
|
-
|
|
106
|
-
```
|
|
107
|
-
Create a Versori webhook workflow for flash sale inventory reservation to Fluent Commerce.
|
|
108
|
-
|
|
109
|
-
REQUIREMENTS:
|
|
110
|
-
1. Runtime: Versori Platform (HTTP webhook)
|
|
111
|
-
2. Source: Promotion data via HTTP POST webhook (JSON)
|
|
112
|
-
3. Destination: Fluent Commerce GraphQL API (reserve inventory mutations)
|
|
113
|
-
4. Format: JSON promotion data
|
|
114
|
-
5. Entity: InventoryReservation (GraphQL mutations)
|
|
115
|
-
|
|
116
|
-
KEY FEATURES:
|
|
117
|
-
- Sync + fire-and-forget pattern (fast webhook response, background processing)
|
|
118
|
-
- External JSON configuration (config/reservation-config.json)
|
|
119
|
-
- Modular architecture (workflows/, services/, config/, types/)
|
|
120
|
-
- Inventory reservation with GraphQL mutations
|
|
121
|
-
- Time-bound reservations (auto-release after expiry)
|
|
122
|
-
- Concurrency control (StateService)
|
|
123
|
-
- KV storage for reservation tracking
|
|
124
|
-
- Comprehensive error handling
|
|
125
|
-
|
|
126
|
-
CRITICAL REQUIREMENTS:
|
|
127
|
-
1. Webhook Mode: response: { mode: 'sync' } (fast response)
|
|
128
|
-
2. Background Processing: Fire-and-forget pattern (no await on long operations)
|
|
129
|
-
3. Reservation Config: External JSON file (config/reservation-config.json)
|
|
130
|
-
4. Modular Structure: Separate services/, config/, types/ folders
|
|
131
|
-
5. Native Logging: Use log from context (no LoggingService)
|
|
132
|
-
6. State Management: VersoriKVAdapter for reservation tracking
|
|
133
|
-
|
|
134
|
-
SDK METHODS TO USE:
|
|
135
|
-
- createClient({ ...ctx, log }) - Pass full Versori context
|
|
136
|
-
- new VersoriKVAdapter(openKv(':project:')) - Reservation state storage
|
|
137
|
-
- new StateService(log) - Concurrency control
|
|
138
|
-
- client.graphql({ query, variables }) - Execute GraphQL mutations
|
|
139
|
-
|
|
140
|
-
FORBIDDEN PATTERNS:
|
|
141
|
-
- ❌ Inline config (use external JSON)
|
|
142
|
-
- ❌ await on background processing (use fire-and-forget)
|
|
143
|
-
- ❌ LoggingService (use native log from context)
|
|
144
|
-
- ❌ All code in one file (use modular structure)
|
|
145
|
-
- ❌ async mode webhook (use sync + fire-and-forget)
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
---
|
|
149
|
-
|
|
150
|
-
## STEP 3: Detailed Flow Documentation
|
|
151
|
-
|
|
152
|
-
### Complete Processing Flow
|
|
153
|
-
|
|
154
|
-
```
|
|
155
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
156
|
-
│ 1. WEBHOOK RECEIVED │
|
|
157
|
-
│ POST https://{workspace}.versori.run/start-flash-sale │
|
|
158
|
-
│ Content-Type: application/json │
|
|
159
|
-
│ Body: { promotion: { id: "...", skus: [...] } } │
|
|
160
|
-
└────────────────────┬────────────────────────────────────────┘
|
|
161
|
-
│
|
|
162
|
-
▼
|
|
163
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
164
|
-
│ 2. QUICK VALIDATION (Synchronous, ~10-50ms) │
|
|
165
|
-
│ - Check fluent_commerce connection exists │
|
|
166
|
-
│ - Validate promotion payload present │
|
|
167
|
-
│ - Return HTTP 200 OK immediately │
|
|
168
|
-
└────────────────────┬────────────────────────────────────────┘
|
|
169
|
-
│
|
|
170
|
-
▼
|
|
171
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
172
|
-
│ 3. BACKGROUND PROCESSING (Fire-and-Forget) │
|
|
173
|
-
│ ┌─────────────────────────────────────────────────────┐ │
|
|
174
|
-
│ │ 3a. Initialize Fluent Client │ │
|
|
175
|
-
│ │ - createClient({ ...ctx, log }) │ │
|
|
176
|
-
│ └─────────────────────────────────────────────────────┘ │
|
|
177
|
-
│ ┌─────────────────────────────────────────────────────┐ │
|
|
178
|
-
│ │ 3b. Initialize State Services │ │
|
|
179
|
-
│ │ - VersoriKVAdapter for state storage │ │
|
|
180
|
-
│ │ - StateService for concurrency control │ │
|
|
181
|
-
│ └─────────────────────────────────────────────────────┘ │
|
|
182
|
-
│ ┌─────────────────────────────────────────────────────┐ │
|
|
183
|
-
│ │ 3c. Reserve Inventory for Each SKU │ │
|
|
184
|
-
│ │ - GraphQL mutation to reserve │ │
|
|
185
|
-
│ │ - Store reservation in KV │ │
|
|
186
|
-
│ └─────────────────────────────────────────────────────┘ │
|
|
187
|
-
│ ┌─────────────────────────────────────────────────────┐ │
|
|
188
|
-
│ │ 3d. Track Promotion State │ │
|
|
189
|
-
│ │ - Store promotion metadata in KV │ │
|
|
190
|
-
│ │ - Set expiry timer │ │
|
|
191
|
-
│ └─────────────────────────────────────────────────────┘ │
|
|
192
|
-
└─────────────────────────────────────────────────────────────┘
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
### Response Timing
|
|
196
|
-
|
|
197
|
-
| Stage | Timing | Blocking |
|
|
198
|
-
|-------|--------|----------|
|
|
199
|
-
| **Webhook Validation** | ~10-50ms | ✅ Yes (blocks response) |
|
|
200
|
-
| **Background Processing** | ~2000-5000ms | ❌ No (fire-and-forget) |
|
|
201
|
-
| **Total Response Time** | ~10-50ms | ✅ Fast response |
|
|
202
|
-
|
|
203
|
-
**Key Benefit**: Webhook caller receives immediate acknowledgment (~50ms) while reservations happen in background (~2-5s).
|
|
204
|
-
|
|
205
|
-
---
|
|
206
|
-
|
|
207
|
-
## STEP 4: Production Modular Structure
|
|
208
|
-
|
|
209
|
-
> **✅ This section shows the COMPLETE production-ready modular structure.**
|
|
210
|
-
> All files are shown with proper imports/exports and folder organization.
|
|
211
|
-
|
|
212
|
-
### Complete Project Structure
|
|
213
|
-
|
|
214
|
-
```
|
|
215
|
-
flash-sale-reserve/
|
|
216
|
-
├── package.json # Dependencies and Versori config
|
|
217
|
-
├── index.ts # Entry point - exports all workflows
|
|
218
|
-
└── src/
|
|
219
|
-
├── workflows/
|
|
220
|
-
│ ├── webhook/
|
|
221
|
-
│ │ ├── start-flash-sale.ts # Webhook: Reserve inventory
|
|
222
|
-
│ │ └── consume-reservation.ts # Webhook: Consume reservations
|
|
223
|
-
│ │
|
|
224
|
-
│ └── scheduled/
|
|
225
|
-
│ └── auto-release.ts # Scheduled: Release expired reservations
|
|
226
|
-
│
|
|
227
|
-
├── services/
|
|
228
|
-
│ └── reservation-service.ts # Shared orchestration logic (reusable)
|
|
229
|
-
│
|
|
230
|
-
├── config/
|
|
231
|
-
│ └── reservation-config.json # Configuration (external JSON)
|
|
232
|
-
│
|
|
233
|
-
└── types/
|
|
234
|
-
└── reservation.types.ts # TypeScript interfaces
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
**Why This Structure?**
|
|
238
|
-
|
|
239
|
-
- ✅ **Clear separation**: Webhook handlers vs scheduled tasks vs business logic
|
|
240
|
-
- ✅ **Reusable services**: Reservation logic can be reused
|
|
241
|
-
- ✅ **External config**: Configuration changes don't require code changes
|
|
242
|
-
- ✅ **Type safety**: TypeScript interfaces for better IDE support
|
|
243
|
-
- ✅ **Scalable**: Easy to add new reservation types or workflows
|
|
244
|
-
|
|
245
|
-
---
|
|
246
|
-
|
|
247
|
-
## SDK Imports
|
|
248
|
-
|
|
249
|
-
**All imports validated against SDK v0.1.27 exports** - this code compiles and runs as-is.
|
|
250
|
-
|
|
251
|
-
**Logging (Versori Platform):**
|
|
252
|
-
- Use native `log` from context: `const { log } = ctx;`
|
|
253
|
-
- Do NOT import or use SDK logging utilities
|
|
254
|
-
- Native Versori logs are automatically integrated with platform monitoring
|
|
255
|
-
|
|
256
|
-
```typescript
|
|
257
|
-
import {
|
|
258
|
-
createClient,
|
|
259
|
-
VersoriKVAdapter,
|
|
260
|
-
StateService
|
|
261
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
262
|
-
|
|
263
|
-
// Key SDK methods used:
|
|
264
|
-
await createClient(ctx); // Versori-aware client factory
|
|
265
|
-
await client.graphql({ query, variables }); // GraphQL mutations
|
|
266
|
-
new VersoriKVAdapter(ctx.openKv(scope)); // Reservation state storage (KV)
|
|
267
|
-
new StateService(log); // Concurrency control (logger only)
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
---
|
|
271
|
-
|
|
272
|
-
## Versori Workflows Structure
|
|
273
|
-
|
|
274
|
-
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
275
|
-
|
|
276
|
-
**Trigger Types:**
|
|
277
|
-
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
278
|
-
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
279
|
-
- **`http()`** → External API calls (chained from webhook/schedule)
|
|
280
|
-
- **`fn()`** → Internal processing (chained from webhook/schedule)
|
|
281
|
-
|
|
282
|
-
### Recommended Project Structure
|
|
283
|
-
|
|
284
|
-
```
|
|
285
|
-
flash-sale-reserve/
|
|
286
|
-
├── index.ts # Entry point - MemoryInterpreter registration
|
|
287
|
-
└── src/
|
|
288
|
-
├── workflows/
|
|
289
|
-
│ ├── webhook/
|
|
290
|
-
│ │ ├── start-flash-sale.ts # Webhook: Reserve inventory
|
|
291
|
-
│ │ └── consume-reservation.ts # Webhook: Consume reservations
|
|
292
|
-
│ │
|
|
293
|
-
│ └── scheduled/
|
|
294
|
-
│ └── auto-release.ts # Scheduled: Release expired reservations
|
|
295
|
-
│
|
|
296
|
-
├── services/
|
|
297
|
-
│ └── reservation-service.ts # Shared orchestration logic (reusable)
|
|
298
|
-
│
|
|
299
|
-
└── config/
|
|
300
|
-
└── reservation-config.json # Configuration
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
**Benefits:**
|
|
304
|
-
- ✅ Clear trigger separation (`webhook/` vs `scheduled/`)
|
|
305
|
-
- ✅ Descriptive file names (easy to browse and understand)
|
|
306
|
-
- ✅ Scalable (add new workflows without cluttering)
|
|
307
|
-
- ✅ Reusable code in `services/` (DRY principle)
|
|
308
|
-
- ✅ Easy to modify individual workflows without affecting others
|
|
309
|
-
- ✅ MemoryInterpreter pattern prevents "unexpected export" errors
|
|
310
|
-
|
|
311
|
-
---
|
|
312
|
-
|
|
313
|
-
## Project Setup
|
|
314
|
-
|
|
315
|
-
```bash
|
|
316
|
-
mkdir versori-flash-sale-reserve && cd $_
|
|
317
|
-
npm init -y
|
|
318
|
-
npm install @fluentcommerce/fc-connect-sdk@^0.1.27 @versori/run
|
|
319
|
-
mkdir -p src/services
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
### Package Configuration (package.json)
|
|
323
|
-
|
|
324
|
-
```json
|
|
325
|
-
{
|
|
326
|
-
"name": "versori-flash-sale-reserve",
|
|
327
|
-
"version": "1.0.0",
|
|
328
|
-
"versori": {
|
|
329
|
-
"workflows": "./src/index.ts"
|
|
330
|
-
},
|
|
331
|
-
"type": "module",
|
|
332
|
-
"scripts": {
|
|
333
|
-
"deploy": "versori deploy",
|
|
334
|
-
"logs": "versori logs"
|
|
335
|
-
},
|
|
336
|
-
"dependencies": {
|
|
337
|
-
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
338
|
-
"@versori/run": "latest"
|
|
339
|
-
},
|
|
340
|
-
"devDependencies": {
|
|
341
|
-
"typescript": "^5.0.0",
|
|
342
|
-
"@types/node": "^20.0.0"
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
### Activation Variables
|
|
348
|
-
|
|
349
|
-
```bash
|
|
350
|
-
# Retailer
|
|
351
|
-
retailerId=your-retailer-id
|
|
352
|
-
|
|
353
|
-
# Reservation configuration
|
|
354
|
-
reservationDurationMinutes=120 # 2 hours
|
|
355
|
-
autoReleaseEnabled=true
|
|
356
|
-
|
|
357
|
-
# Concurrency control
|
|
358
|
-
lockTimeoutMs=5000
|
|
359
|
-
maxRetries=3
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
---
|
|
363
|
-
|
|
364
|
-
## Flash Sale Flow
|
|
365
|
-
|
|
366
|
-
```
|
|
367
|
-
Promotion Start Event →
|
|
368
|
-
Parse promotion (SKUs, quantity, duration)
|
|
369
|
-
For each SKU:
|
|
370
|
-
- Query available inventory
|
|
371
|
-
- Create reservation mutation (status: RESERVED, promotion link)
|
|
372
|
-
- Set expiration timestamp in KV
|
|
373
|
-
|
|
374
|
-
During sale:
|
|
375
|
-
- Orders consume reservations (real-time)
|
|
376
|
-
- Track remaining reserved qty in KV
|
|
377
|
-
|
|
378
|
-
After timer expires:
|
|
379
|
-
- Query unreserved inventory
|
|
380
|
-
- Release back to AVAILABLE
|
|
381
|
-
- Generate reconciliation report
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
---
|
|
385
|
-
|
|
386
|
-
## Reservation Service Implementation
|
|
387
|
-
|
|
388
|
-
**File:** `src/services/reservation-service.ts`
|
|
389
|
-
|
|
390
|
-
**Note on Connection Validation:**
|
|
391
|
-
This template uses direct GraphQL queries via FluentClient and does not require explicit connection validation. If you're using SFTP or S3 data sources in your implementation, add `await dataSource.validateConnection()` after initialization.
|
|
392
|
-
|
|
393
|
-
```typescript
|
|
394
|
-
// src/services/reservation-service.ts
|
|
395
|
-
import { FluentClient } from '@fluentcommerce/fc-connect-sdk';
|
|
396
|
-
|
|
397
|
-
export class ReservationService {
|
|
398
|
-
constructor(
|
|
399
|
-
private client: any,
|
|
400
|
-
private stateService: any,
|
|
401
|
-
private retailerId: string,
|
|
402
|
-
private log: any
|
|
403
|
-
) {}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Reserve inventory for flash sale
|
|
407
|
-
*/
|
|
408
|
-
async reserveInventory(
|
|
409
|
-
promotionId: string,
|
|
410
|
-
skus: Array<{ sku: string; quantity: number }>,
|
|
411
|
-
durationMinutes: number
|
|
412
|
-
): Promise<any[]> {
|
|
413
|
-
const results: any[] = [];
|
|
414
|
-
const expiresAt = new Date(Date.now() + durationMinutes * 60000);
|
|
415
|
-
|
|
416
|
-
for (const item of skus) {
|
|
417
|
-
try {
|
|
418
|
-
// Step 1: Query available inventory
|
|
419
|
-
const available = await this.getAvailableInventory(item.sku);
|
|
420
|
-
if (available < item.quantity) {
|
|
421
|
-
this.log.warn('Insufficient inventory for reservation', {
|
|
422
|
-
sku: item.sku,
|
|
423
|
-
requested: item.quantity,
|
|
424
|
-
available
|
|
425
|
-
});
|
|
426
|
-
results.push({
|
|
427
|
-
sku: item.sku,
|
|
428
|
-
success: false,
|
|
429
|
-
error: 'Insufficient inventory'
|
|
430
|
-
});
|
|
431
|
-
continue;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Step 2: Create reservation (with distributed lock)
|
|
435
|
-
const lockKey = `lock:inventory:${item.sku}`;
|
|
436
|
-
const acquired = await this.acquireLock(lockKey);
|
|
437
|
-
if (!acquired) {
|
|
438
|
-
results.push({
|
|
439
|
-
sku: item.sku,
|
|
440
|
-
success: false,
|
|
441
|
-
error: 'Failed to acquire lock (concurrent reservation)'
|
|
442
|
-
});
|
|
443
|
-
continue;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
try {
|
|
447
|
-
// Create reservation via GraphQL mutation
|
|
448
|
-
const reservationRef = `${promotionId}-${item.sku}-${Date.now()}`;
|
|
449
|
-
const reserveMutation = `
|
|
450
|
-
mutation ReserveInventory($input: CreateInventoryInput!) {
|
|
451
|
-
createInventory(input: $input) {
|
|
452
|
-
id
|
|
453
|
-
ref
|
|
454
|
-
productRef
|
|
455
|
-
quantity
|
|
456
|
-
status
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
`;
|
|
460
|
-
|
|
461
|
-
const input = {
|
|
462
|
-
ref: reservationRef,
|
|
463
|
-
retailerId: this.retailerId,
|
|
464
|
-
productRef: item.sku,
|
|
465
|
-
quantity: item.quantity,
|
|
466
|
-
status: 'RESERVED',
|
|
467
|
-
attributes: [
|
|
468
|
-
{ name: 'promotion_id', value: promotionId },
|
|
469
|
-
{ name: 'expires_at', value: expiresAt.toISOString() },
|
|
470
|
-
{ name: 'reservation_type', value: 'FLASH_SALE' }
|
|
471
|
-
]
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
const response = await this.client.graphql({
|
|
475
|
-
query: reserveMutation,
|
|
476
|
-
variables: { input }
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
const reservation = response?.data?.createInventory;
|
|
480
|
-
|
|
481
|
-
// Store reservation metadata in KV
|
|
482
|
-
const stateKey = `reservation:${reservationRef}`;
|
|
483
|
-
await this.stateService.setState(stateKey, {
|
|
484
|
-
promotionId,
|
|
485
|
-
sku: item.sku,
|
|
486
|
-
quantity: item.quantity,
|
|
487
|
-
reserved: item.quantity,
|
|
488
|
-
consumed: 0,
|
|
489
|
-
expiresAt: expiresAt.toISOString(),
|
|
490
|
-
createdAt: new Date().toISOString()
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
this.log.info('Inventory reserved', {
|
|
494
|
-
sku: item.sku,
|
|
495
|
-
quantity: item.quantity,
|
|
496
|
-
reservationRef,
|
|
497
|
-
expiresAt
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
results.push({
|
|
501
|
-
sku: item.sku,
|
|
502
|
-
success: true,
|
|
503
|
-
reservationRef,
|
|
504
|
-
quantity: item.quantity,
|
|
505
|
-
expiresAt
|
|
506
|
-
});
|
|
507
|
-
} finally {
|
|
508
|
-
await this.releaseLock(lockKey);
|
|
509
|
-
}
|
|
510
|
-
} catch (error: any) {
|
|
511
|
-
// ? Enhanced: Error logging with recommendations
|
|
512
|
-
this.log.error('[FlashSaleReserve] Reservation failed', {
|
|
513
|
-
sku: item.sku,
|
|
514
|
-
error: error instanceof Error ? error.message : String(error),
|
|
515
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
516
|
-
recommendation: error.message?.includes('lock') || error.message?.includes('concurrent')
|
|
517
|
-
? 'Concurrent reservation attempt detected - retry after lock release'
|
|
518
|
-
: error.message?.includes('inventory') || error.message?.includes('quantity')
|
|
519
|
-
? 'Check available inventory quantity and reservation limits'
|
|
520
|
-
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
521
|
-
? 'Check network connectivity and Fluent Commerce API availability'
|
|
522
|
-
: 'Review error details and check SKU/product availability'
|
|
523
|
-
});
|
|
524
|
-
results.push({
|
|
525
|
-
sku: item.sku,
|
|
526
|
-
success: false,
|
|
527
|
-
error: error.message
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
return results;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Consume reservation when order is placed
|
|
537
|
-
*/
|
|
538
|
-
async consumeReservation(
|
|
539
|
-
promotionId: string,
|
|
540
|
-
sku: string,
|
|
541
|
-
quantity: number
|
|
542
|
-
): Promise<boolean> {
|
|
543
|
-
const stateKey = `reservation:${promotionId}-${sku}`;
|
|
544
|
-
try {
|
|
545
|
-
const reservation = await this.stateService.getState(stateKey);
|
|
546
|
-
if (!reservation) {
|
|
547
|
-
this.log.warn('Reservation not found', { promotionId, sku });
|
|
548
|
-
return false;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
const remaining = reservation.reserved - reservation.consumed;
|
|
552
|
-
if (remaining < quantity) {
|
|
553
|
-
this.log.warn('Insufficient reserved quantity', {
|
|
554
|
-
promotionId,
|
|
555
|
-
sku,
|
|
556
|
-
requested: quantity,
|
|
557
|
-
remaining
|
|
558
|
-
});
|
|
559
|
-
return false;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Update consumed count
|
|
563
|
-
reservation.consumed += quantity;
|
|
564
|
-
await this.stateService.setState(stateKey, reservation);
|
|
565
|
-
|
|
566
|
-
this.log.info('Reservation consumed', {
|
|
567
|
-
promotionId,
|
|
568
|
-
sku,
|
|
569
|
-
quantity,
|
|
570
|
-
remaining: reservation.reserved - reservation.consumed
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
return true;
|
|
574
|
-
} catch (error: any) {
|
|
575
|
-
// ? Enhanced: Error logging with recommendations
|
|
576
|
-
this.log.error('[FlashSaleReserve] Failed to consume reservation', {
|
|
577
|
-
promotionId,
|
|
578
|
-
sku,
|
|
579
|
-
error: error instanceof Error ? error.message : String(error),
|
|
580
|
-
recommendation: error.message?.includes('not found') || error.message?.includes('missing')
|
|
581
|
-
? 'Reservation may have expired or was not created - verify promotion and reservation state'
|
|
582
|
-
: error.message?.includes('insufficient') || error.message?.includes('quantity')
|
|
583
|
-
? 'Insufficient reserved quantity - check reservation consumption limits'
|
|
584
|
-
: 'Review error details and check reservation state in KV store'
|
|
585
|
-
});
|
|
586
|
-
return false;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Release expired reservations back to AVAILABLE
|
|
592
|
-
*/
|
|
593
|
-
async releaseExpiredReservations(promotionId: string): Promise<any> {
|
|
594
|
-
try {
|
|
595
|
-
// Query all reservations for promotion
|
|
596
|
-
const query = `
|
|
597
|
-
query GetReservations($retailerId: ID!, $promotionId: String!) {
|
|
598
|
-
inventories(
|
|
599
|
-
retailerId: $retailerId
|
|
600
|
-
status: "RESERVED"
|
|
601
|
-
attributes: [{ name: "promotion_id", value: $promotionId }]
|
|
602
|
-
) {
|
|
603
|
-
edges {
|
|
604
|
-
node {
|
|
605
|
-
id
|
|
606
|
-
ref
|
|
607
|
-
productRef
|
|
608
|
-
quantity
|
|
609
|
-
attributes {
|
|
610
|
-
name
|
|
611
|
-
value
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
`;
|
|
618
|
-
|
|
619
|
-
const response = await this.client.graphql({
|
|
620
|
-
query,
|
|
621
|
-
variables: { retailerId: this.retailerId, promotionId }
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
const reservations = response?.data?.inventories?.edges || [];
|
|
625
|
-
const released: any[] = [];
|
|
626
|
-
const now = new Date();
|
|
627
|
-
|
|
628
|
-
for (const edge of reservations) {
|
|
629
|
-
const reservation = edge.node;
|
|
630
|
-
const expiresAtAttr = reservation.attributes?.find((a: any) => a.name === 'expires_at');
|
|
631
|
-
const expiresAt = expiresAtAttr ? new Date(expiresAtAttr.value) : null;
|
|
632
|
-
|
|
633
|
-
if (!expiresAt || expiresAt > now) {
|
|
634
|
-
continue; // Not yet expired
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// Check state to see if fully consumed
|
|
638
|
-
const stateKey = `reservation:${reservation.ref}`;
|
|
639
|
-
const state = await this.stateService.getState(stateKey);
|
|
640
|
-
if (state && state.consumed >= state.reserved) {
|
|
641
|
-
this.log.debug('Reservation fully consumed, skipping release', { ref: reservation.ref });
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
const unreservedQty = state ? state.reserved - state.consumed : reservation.quantity;
|
|
646
|
-
|
|
647
|
-
// Release unreserved quantity back to AVAILABLE
|
|
648
|
-
const releaseMutation = `
|
|
649
|
-
mutation UpdateInventory($input: UpdateInventoryInput!) {
|
|
650
|
-
updateInventory(input: $input) {
|
|
651
|
-
id
|
|
652
|
-
ref
|
|
653
|
-
status
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
`;
|
|
657
|
-
|
|
658
|
-
const input = {
|
|
659
|
-
ref: reservation.ref,
|
|
660
|
-
status: 'AVAILABLE',
|
|
661
|
-
quantity: unreservedQty
|
|
662
|
-
};
|
|
663
|
-
|
|
664
|
-
await this.client.graphql({
|
|
665
|
-
query: releaseMutation,
|
|
666
|
-
variables: { input }
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
released.push({
|
|
670
|
-
ref: reservation.ref,
|
|
671
|
-
sku: reservation.productRef,
|
|
672
|
-
quantity: unreservedQty
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
this.log.info('Reservation released', {
|
|
676
|
-
ref: reservation.ref,
|
|
677
|
-
sku: reservation.productRef,
|
|
678
|
-
quantity: unreservedQty
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
return {
|
|
683
|
-
promotionId,
|
|
684
|
-
released: released.length,
|
|
685
|
-
details: released
|
|
686
|
-
};
|
|
687
|
-
} catch (error: any) {
|
|
688
|
-
this.log.error('Failed to release reservations', { promotionId, error: error.message });
|
|
689
|
-
throw error;
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
/**
|
|
694
|
-
* Query available inventory (non-reserved)
|
|
695
|
-
*/
|
|
696
|
-
private async getAvailableInventory(sku: string): Promise<number> {
|
|
697
|
-
const query = `
|
|
698
|
-
query GetInventory($sku: String!, $retailerId: ID!) {
|
|
699
|
-
inventories(
|
|
700
|
-
first: 10
|
|
701
|
-
productRef: [$sku]
|
|
702
|
-
retailerId: $retailerId
|
|
703
|
-
status: "AVAILABLE"
|
|
704
|
-
) {
|
|
705
|
-
edges {
|
|
706
|
-
node {
|
|
707
|
-
quantity
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
`;
|
|
713
|
-
|
|
714
|
-
const response = await this.client.graphql({
|
|
715
|
-
query,
|
|
716
|
-
variables: { sku, retailerId: this.retailerId }
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
const inventories = response?.data?.inventories?.edges || [];
|
|
720
|
-
return inventories.reduce((sum: number, edge: any) => sum + (edge.node.quantity || 0), 0);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
/**
|
|
724
|
-
* Distributed lock for concurrency control
|
|
725
|
-
*/
|
|
726
|
-
private async acquireLock(lockKey: string): Promise<boolean> {
|
|
727
|
-
try {
|
|
728
|
-
const existing = await this.stateService.getState(lockKey);
|
|
729
|
-
if (existing && existing.lockedUntil > Date.now()) {
|
|
730
|
-
return false; // Lock already held
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// Set lock with timeout
|
|
734
|
-
const lockTimeoutMs = 5000; // 5 seconds
|
|
735
|
-
await this.stateService.setState(lockKey, {
|
|
736
|
-
locked: true,
|
|
737
|
-
lockedAt: Date.now(),
|
|
738
|
-
lockedUntil: Date.now() + lockTimeoutMs
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
return true;
|
|
742
|
-
} catch (error: any) {
|
|
743
|
-
// ? Enhanced: Error logging with recommendations
|
|
744
|
-
this.log.warn('[FlashSaleReserve] Failed to acquire lock', {
|
|
745
|
-
lockKey,
|
|
746
|
-
error: error instanceof Error ? error.message : String(error),
|
|
747
|
-
recommendation: 'Lock may be held by another process - retry after lock timeout or check KV store connectivity'
|
|
748
|
-
});
|
|
749
|
-
return false;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
/**
|
|
754
|
-
* Release distributed lock
|
|
755
|
-
*/
|
|
756
|
-
private async releaseLock(lockKey: string): Promise<void> {
|
|
757
|
-
try {
|
|
758
|
-
await this.stateService.setState(lockKey, {
|
|
759
|
-
locked: false,
|
|
760
|
-
releasedAt: Date.now()
|
|
761
|
-
});
|
|
762
|
-
} catch (error: any) {
|
|
763
|
-
this.log.warn('Failed to release lock', { lockKey, error: error.message });
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
```
|
|
768
|
-
|
|
769
|
-
---
|
|
770
|
-
|
|
771
|
-
## Complete Workflow Implementation
|
|
772
|
-
|
|
773
|
-
### Entry Point with MemoryInterpreter Pattern
|
|
774
|
-
|
|
775
|
-
**File:** `index.ts` (project root)
|
|
776
|
-
|
|
777
|
-
```typescript
|
|
778
|
-
/**
|
|
779
|
-
* Entry Point - Registers all workflows with Versori platform
|
|
780
|
-
*
|
|
781
|
-
* ⚠️ CRITICAL PATTERN: MemoryInterpreter Setup
|
|
782
|
-
* This pattern ensures workflows execute in the correct Versori runtime context
|
|
783
|
-
* and prevents "unexpected export" errors during deployment.
|
|
784
|
-
*
|
|
785
|
-
* COMPONENTS:
|
|
786
|
-
* 1. Import ALL workflows (scheduled, webhook, etc.)
|
|
787
|
-
* 2. Create MemoryInterpreter with workflow array
|
|
788
|
-
* 3. NO direct exports - interpreter handles workflow registration
|
|
789
|
-
*/
|
|
790
|
-
|
|
791
|
-
import { MemoryInterpreter } from '@versori/run';
|
|
792
|
-
|
|
793
|
-
// Import workflows
|
|
794
|
-
import { startFlashSale } from './src/workflows/webhook/start-flash-sale';
|
|
795
|
-
import { consumeReservation } from './src/workflows/webhook/consume-reservation';
|
|
796
|
-
import { autoReleaseReservations } from './src/workflows/scheduled/auto-release';
|
|
797
|
-
|
|
798
|
-
// ✅ CORRECT: Register via MemoryInterpreter (Versori standard)
|
|
799
|
-
new MemoryInterpreter([
|
|
800
|
-
// Webhooks (HTTP-based triggers)
|
|
801
|
-
startFlashSale,
|
|
802
|
-
consumeReservation,
|
|
803
|
-
|
|
804
|
-
// Scheduled (time-based triggers)
|
|
805
|
-
autoReleaseReservations,
|
|
806
|
-
]);
|
|
807
|
-
|
|
808
|
-
// ❌ WRONG: Direct exports cause "unexpected export" errors
|
|
809
|
-
// export { startFlashSale, consumeReservation, autoReleaseReservations };
|
|
810
|
-
```
|
|
811
|
-
|
|
812
|
-
**What Gets Exposed:**
|
|
813
|
-
- ✅ `startFlashSale` → `https://{workspace}.versori.run/start-flash-sale`
|
|
814
|
-
- ✅ `consumeReservation` → `https://{workspace}.versori.run/consume-reservation`
|
|
815
|
-
- ❌ `autoReleaseReservations` → NOT exposed (runs automatically on cron)
|
|
816
|
-
|
|
817
|
-
---
|
|
818
|
-
|
|
819
|
-
### Workflow Implementations
|
|
820
|
-
|
|
821
|
-
**File:** `src/workflows/webhook/start-flash-sale.ts`
|
|
822
|
-
|
|
823
|
-
```typescript
|
|
824
|
-
import { webhook, http } from '@versori/run';
|
|
825
|
-
import {
|
|
826
|
-
createClient,
|
|
827
|
-
VersoriKVAdapter,
|
|
828
|
-
StateService
|
|
829
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
830
|
-
import { ReservationService } from '../../services/reservation-service';
|
|
831
|
-
|
|
832
|
-
/**
|
|
833
|
-
* Webhook: Start flash sale and reserve inventory
|
|
834
|
-
*
|
|
835
|
-
* Endpoint: POST https://{workspace}.versori.run/start-flash-sale
|
|
836
|
-
* Request body: { promotion: { id: "FLASH-001", skus: [{ sku: "SKU-001", quantity: 100 }] } }
|
|
837
|
-
*/
|
|
838
|
-
export const startFlashSale = webhook('start-flash-sale', {
|
|
839
|
-
response: { mode: 'sync' }
|
|
840
|
-
}).then(http('reserve-inventory', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
841
|
-
const { log, openKv, activation, data } = ctx;
|
|
842
|
-
const startTime = Date.now();
|
|
843
|
-
|
|
844
|
-
log.info('🚀 Starting flash sale inventory reservation');
|
|
845
|
-
|
|
846
|
-
try {
|
|
847
|
-
// ========================================
|
|
848
|
-
// STEP 1: VALIDATION & CLIENT INITIALIZATION
|
|
849
|
-
// ========================================
|
|
850
|
-
|
|
851
|
-
// ? Enhanced: Configuration validation with error recommendations
|
|
852
|
-
const retailerId = activation?.getVariable('retailerId');
|
|
853
|
-
if (!retailerId) {
|
|
854
|
-
log.error('❌ Configuration error: Missing retailerId', {
|
|
855
|
-
recommendation: 'Configure retailerId in Activation Variables section'
|
|
856
|
-
});
|
|
857
|
-
return {
|
|
858
|
-
status: 500,
|
|
859
|
-
error: 'Missing retailerId configuration',
|
|
860
|
-
recommendation: 'Add retailerId to Activation Variables'
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
const client = await createClient(ctx);
|
|
865
|
-
client.setRetailerId(retailerId);
|
|
866
|
-
log.info('✅ Client initialized', { retailerId });
|
|
867
|
-
|
|
868
|
-
// ========================================
|
|
869
|
-
// STEP 2: SERVICE INITIALIZATION
|
|
870
|
-
// ========================================
|
|
871
|
-
|
|
872
|
-
const kv = new VersoriKVAdapter(openKv(':project:'));
|
|
873
|
-
const stateService = new StateService(log);
|
|
874
|
-
const durationMinutes = Number(activation?.getVariable('reservationDurationMinutes') || 120);
|
|
875
|
-
|
|
876
|
-
const reservationService = new ReservationService(client, stateService, retailerId, log);
|
|
877
|
-
|
|
878
|
-
log.info('🔍 Configuration loaded', { durationMinutes });
|
|
879
|
-
|
|
880
|
-
// ========================================
|
|
881
|
-
// STEP 3: VALIDATE REQUEST PAYLOAD
|
|
882
|
-
// ========================================
|
|
883
|
-
|
|
884
|
-
const promotion = data?.promotion || {};
|
|
885
|
-
const promotionId = promotion.id || promotion.ref;
|
|
886
|
-
const skus = promotion.skus || [];
|
|
887
|
-
|
|
888
|
-
if (!promotionId || skus.length === 0) {
|
|
889
|
-
log.error('❌ Invalid request payload', {
|
|
890
|
-
hasPromotionId: !!promotionId,
|
|
891
|
-
skuCount: skus.length,
|
|
892
|
-
recommendation: 'Ensure request includes: { promotion: { id: "...", skus: [...] } }'
|
|
893
|
-
});
|
|
894
|
-
return {
|
|
895
|
-
status: 400,
|
|
896
|
-
error: 'Invalid promotion data',
|
|
897
|
-
recommendation: 'Request must include promotion.id and promotion.skus array'
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
log.info('🔍 Processing promotion', { promotionId, skuCount: skus.length });
|
|
902
|
-
|
|
903
|
-
// ========================================
|
|
904
|
-
// STEP 4: RESERVE INVENTORY
|
|
905
|
-
// ========================================
|
|
906
|
-
|
|
907
|
-
const results = await reservationService.reserveInventory(promotionId, skus, durationMinutes);
|
|
908
|
-
|
|
909
|
-
const successCount = results.filter(r => r.success).length;
|
|
910
|
-
const errorCount = results.filter(r => !r.success).length;
|
|
911
|
-
|
|
912
|
-
// Store promotion metadata in KV
|
|
913
|
-
const promotionStateKey = `promotion:${promotionId}`;
|
|
914
|
-
await kv.set(promotionStateKey, {
|
|
915
|
-
promotionId,
|
|
916
|
-
startedAt: new Date().toISOString(),
|
|
917
|
-
durationMinutes,
|
|
918
|
-
skus,
|
|
919
|
-
reservationResults: results
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
const duration = Date.now() - startTime;
|
|
923
|
-
|
|
924
|
-
log.info('✅ Flash sale inventory reserved', {
|
|
925
|
-
promotionId,
|
|
926
|
-
success: successCount,
|
|
927
|
-
errors: errorCount,
|
|
928
|
-
duration: `${duration}ms`
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
return {
|
|
932
|
-
status: 200,
|
|
933
|
-
promotionId,
|
|
934
|
-
reservations: results,
|
|
935
|
-
summary: {
|
|
936
|
-
success: successCount,
|
|
937
|
-
errors: errorCount
|
|
938
|
-
},
|
|
939
|
-
duration
|
|
940
|
-
};
|
|
941
|
-
|
|
942
|
-
} catch (error: any) {
|
|
943
|
-
const duration = Date.now() - startTime;
|
|
944
|
-
|
|
945
|
-
log.error('❌ Flash sale reservation failed', {
|
|
946
|
-
error: error instanceof Error ? error.message : String(error),
|
|
947
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
948
|
-
duration: `${duration}ms`,
|
|
949
|
-
recommendation: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
950
|
-
? 'Check Fluent Commerce API connectivity and connection configuration'
|
|
951
|
-
: error.message?.includes('authentication') || error.message?.includes('401')
|
|
952
|
-
? 'Verify Fluent Commerce OAuth2 credentials in connection settings'
|
|
953
|
-
: error.message?.includes('retailerId')
|
|
954
|
-
? 'Ensure retailerId is configured in Activation Variables'
|
|
955
|
-
: 'Review error details and check promotion payload format'
|
|
956
|
-
});
|
|
957
|
-
|
|
958
|
-
return {
|
|
959
|
-
status: 500,
|
|
960
|
-
error: error.message,
|
|
961
|
-
duration
|
|
962
|
-
};
|
|
963
|
-
}
|
|
964
|
-
}));
|
|
965
|
-
```
|
|
966
|
-
|
|
967
|
-
**File:** `src/workflows/webhook/consume-reservation.ts`
|
|
968
|
-
|
|
969
|
-
```typescript
|
|
970
|
-
import { webhook, http } from '@versori/run';
|
|
971
|
-
import {
|
|
972
|
-
createClient,
|
|
973
|
-
VersoriKVAdapter,
|
|
974
|
-
StateService
|
|
975
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
976
|
-
import { ReservationService } from '../../services/reservation-service';
|
|
977
|
-
|
|
978
|
-
/**
|
|
979
|
-
* Webhook: Consume reservation when order is placed
|
|
980
|
-
*
|
|
981
|
-
* Endpoint: POST https://{workspace}.versori.run/consume-reservation
|
|
982
|
-
* Request body: { order: { ref: "ORD-001", promotionId: "FLASH-001", items: [{ sku: "SKU-001", quantity: 2 }] } }
|
|
983
|
-
*/
|
|
984
|
-
export const consumeReservation = webhook('consume-reservation', {
|
|
985
|
-
response: { mode: 'sync' }
|
|
986
|
-
}).then(http('consume-inventory', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
987
|
-
const { log, openKv, activation, data } = ctx;
|
|
988
|
-
const startTime = Date.now();
|
|
989
|
-
|
|
990
|
-
log.info('🚀 Starting reservation consumption');
|
|
991
|
-
|
|
992
|
-
try {
|
|
993
|
-
// ========================================
|
|
994
|
-
// STEP 1: CLIENT INITIALIZATION
|
|
995
|
-
// ========================================
|
|
996
|
-
|
|
997
|
-
const retailerId = activation?.getVariable('retailerId');
|
|
998
|
-
if (!retailerId) {
|
|
999
|
-
log.error('❌ Configuration error: Missing retailerId', {
|
|
1000
|
-
recommendation: 'Configure retailerId in Activation Variables'
|
|
1001
|
-
});
|
|
1002
|
-
return {
|
|
1003
|
-
status: 500,
|
|
1004
|
-
error: 'Missing retailerId configuration',
|
|
1005
|
-
recommendation: 'Add retailerId to Activation Variables'
|
|
1006
|
-
};
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
const client = await createClient(ctx);
|
|
1010
|
-
client.setRetailerId(retailerId);
|
|
1011
|
-
log.info('✅ Client initialized', { retailerId });
|
|
1012
|
-
|
|
1013
|
-
// ========================================
|
|
1014
|
-
// STEP 2: SERVICE INITIALIZATION
|
|
1015
|
-
// ========================================
|
|
1016
|
-
|
|
1017
|
-
const kv = new VersoriKVAdapter(openKv(':project:'));
|
|
1018
|
-
const stateService = new StateService(log);
|
|
1019
|
-
const reservationService = new ReservationService(client, stateService, retailerId, log);
|
|
1020
|
-
|
|
1021
|
-
// ========================================
|
|
1022
|
-
// STEP 3: VALIDATE REQUEST PAYLOAD
|
|
1023
|
-
// ========================================
|
|
1024
|
-
|
|
1025
|
-
const order = data?.order || {};
|
|
1026
|
-
const promotionId = order.promotionId;
|
|
1027
|
-
const items = order.items || [];
|
|
1028
|
-
|
|
1029
|
-
if (!promotionId) {
|
|
1030
|
-
log.error('❌ Missing promotion ID in request', {
|
|
1031
|
-
recommendation: 'Request must include order.promotionId'
|
|
1032
|
-
});
|
|
1033
|
-
return {
|
|
1034
|
-
status: 400,
|
|
1035
|
-
error: 'Missing promotion ID',
|
|
1036
|
-
recommendation: 'Include order.promotionId in request payload'
|
|
1037
|
-
};
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
log.info('🔍 Consuming reservations', {
|
|
1041
|
-
orderRef: order.ref,
|
|
1042
|
-
promotionId,
|
|
1043
|
-
itemCount: items.length
|
|
1044
|
-
});
|
|
1045
|
-
|
|
1046
|
-
// ========================================
|
|
1047
|
-
// STEP 4: CONSUME RESERVATIONS
|
|
1048
|
-
// ========================================
|
|
1049
|
-
|
|
1050
|
-
const consumeResults: any[] = [];
|
|
1051
|
-
for (const item of items) {
|
|
1052
|
-
const consumed = await reservationService.consumeReservation(
|
|
1053
|
-
promotionId,
|
|
1054
|
-
item.sku,
|
|
1055
|
-
item.quantity
|
|
1056
|
-
);
|
|
1057
|
-
consumeResults.push({
|
|
1058
|
-
sku: item.sku,
|
|
1059
|
-
quantity: item.quantity,
|
|
1060
|
-
consumed
|
|
1061
|
-
});
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
const successCount = consumeResults.filter(r => r.consumed).length;
|
|
1065
|
-
const duration = Date.now() - startTime;
|
|
1066
|
-
|
|
1067
|
-
log.info('✅ Reservation consumption complete', {
|
|
1068
|
-
orderRef: order.ref,
|
|
1069
|
-
success: successCount,
|
|
1070
|
-
failed: consumeResults.length - successCount,
|
|
1071
|
-
duration: `${duration}ms`
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
return {
|
|
1075
|
-
status: 200,
|
|
1076
|
-
orderRef: order.ref,
|
|
1077
|
-
consumeResults,
|
|
1078
|
-
duration
|
|
1079
|
-
};
|
|
1080
|
-
|
|
1081
|
-
} catch (error: any) {
|
|
1082
|
-
const duration = Date.now() - startTime;
|
|
1083
|
-
|
|
1084
|
-
log.error('❌ Reservation consumption failed', {
|
|
1085
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1086
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1087
|
-
duration: `${duration}ms`,
|
|
1088
|
-
recommendation: error.message?.includes('not found') || error.message?.includes('missing')
|
|
1089
|
-
? 'Verify promotion exists and reservations were created successfully'
|
|
1090
|
-
: error.message?.includes('insufficient') || error.message?.includes('quantity')
|
|
1091
|
-
? 'Check reservation quantities and consumption limits'
|
|
1092
|
-
: 'Review error details and check order payload format'
|
|
1093
|
-
});
|
|
1094
|
-
|
|
1095
|
-
return {
|
|
1096
|
-
status: 500,
|
|
1097
|
-
error: error.message,
|
|
1098
|
-
duration
|
|
1099
|
-
};
|
|
1100
|
-
}
|
|
1101
|
-
}));
|
|
1102
|
-
```
|
|
1103
|
-
|
|
1104
|
-
**File:** `src/workflows/scheduled/auto-release.ts`
|
|
1105
|
-
|
|
1106
|
-
```typescript
|
|
1107
|
-
import { schedule, http } from '@versori/run';
|
|
1108
|
-
import {
|
|
1109
|
-
createClient,
|
|
1110
|
-
VersoriKVAdapter,
|
|
1111
|
-
StateService
|
|
1112
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
1113
|
-
import { ReservationService } from '../../services/reservation-service';
|
|
1114
|
-
|
|
1115
|
-
/**
|
|
1116
|
-
* Scheduled: Auto-release expired reservations
|
|
1117
|
-
*
|
|
1118
|
-
* Schedule: Every 10 minutes
|
|
1119
|
-
* Purpose: Release expired reservations back to AVAILABLE inventory
|
|
1120
|
-
*/
|
|
1121
|
-
export const autoReleaseReservations = schedule('auto-release', '*/10 * * * *').then(
|
|
1122
|
-
http('release-expired', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
1123
|
-
const { log, openKv, activation } = ctx;
|
|
1124
|
-
const startTime = Date.now();
|
|
1125
|
-
|
|
1126
|
-
log.info('🚀 Starting auto-release of expired reservations');
|
|
1127
|
-
|
|
1128
|
-
try {
|
|
1129
|
-
// ========================================
|
|
1130
|
-
// STEP 1: CHECK IF AUTO-RELEASE ENABLED
|
|
1131
|
-
// ========================================
|
|
1132
|
-
|
|
1133
|
-
const autoReleaseEnabled = activation?.getVariable('autoReleaseEnabled') === 'true';
|
|
1134
|
-
if (!autoReleaseEnabled) {
|
|
1135
|
-
log.info('⚠️ Auto-release disabled');
|
|
1136
|
-
return { status: 200, message: 'Auto-release disabled' };
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
// ========================================
|
|
1140
|
-
// STEP 2: CLIENT INITIALIZATION
|
|
1141
|
-
// ========================================
|
|
1142
|
-
|
|
1143
|
-
const retailerId = activation?.getVariable('retailerId');
|
|
1144
|
-
if (!retailerId) {
|
|
1145
|
-
log.error('❌ Configuration error: Missing retailerId', {
|
|
1146
|
-
recommendation: 'Configure retailerId in Activation Variables'
|
|
1147
|
-
});
|
|
1148
|
-
return {
|
|
1149
|
-
status: 500,
|
|
1150
|
-
error: 'Missing retailerId configuration'
|
|
1151
|
-
};
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
const client = await createClient(ctx);
|
|
1155
|
-
client.setRetailerId(retailerId);
|
|
1156
|
-
log.info('✅ Client initialized', { retailerId });
|
|
1157
|
-
|
|
1158
|
-
// ========================================
|
|
1159
|
-
// STEP 3: SERVICE INITIALIZATION
|
|
1160
|
-
// ========================================
|
|
1161
|
-
|
|
1162
|
-
const kv = new VersoriKVAdapter(openKv(':project:'));
|
|
1163
|
-
const stateService = new StateService(log);
|
|
1164
|
-
const reservationService = new ReservationService(client, stateService, retailerId, log);
|
|
1165
|
-
|
|
1166
|
-
// ========================================
|
|
1167
|
-
// STEP 4: QUERY ACTIVE PROMOTIONS
|
|
1168
|
-
// ========================================
|
|
1169
|
-
|
|
1170
|
-
// In production, maintain a list of active promotions in KV
|
|
1171
|
-
const activePromotions = ['PROMO-001']; // Replace with actual query from KV
|
|
1172
|
-
|
|
1173
|
-
log.info('🔍 Processing active promotions', { count: activePromotions.length });
|
|
1174
|
-
|
|
1175
|
-
// ========================================
|
|
1176
|
-
// STEP 5: RELEASE EXPIRED RESERVATIONS
|
|
1177
|
-
// ========================================
|
|
1178
|
-
|
|
1179
|
-
const releaseResults: any[] = [];
|
|
1180
|
-
for (const promotionId of activePromotions) {
|
|
1181
|
-
try {
|
|
1182
|
-
const result = await reservationService.releaseExpiredReservations(promotionId);
|
|
1183
|
-
releaseResults.push(result);
|
|
1184
|
-
|
|
1185
|
-
if (result.released > 0) {
|
|
1186
|
-
log.info('✅ Expired reservations released', {
|
|
1187
|
-
promotionId,
|
|
1188
|
-
released: result.released
|
|
1189
|
-
});
|
|
1190
|
-
}
|
|
1191
|
-
} catch (error: any) {
|
|
1192
|
-
log.error('❌ Failed to release reservations', {
|
|
1193
|
-
promotionId,
|
|
1194
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1195
|
-
recommendation: 'Check promotion exists and has expired reservations'
|
|
1196
|
-
});
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
const totalReleased = releaseResults.reduce((sum, r) => sum + r.released, 0);
|
|
1201
|
-
const duration = Date.now() - startTime;
|
|
1202
|
-
|
|
1203
|
-
log.info('✅ Auto-release complete', {
|
|
1204
|
-
totalReleased,
|
|
1205
|
-
promotionsProcessed: activePromotions.length,
|
|
1206
|
-
duration: `${duration}ms`
|
|
1207
|
-
});
|
|
1208
|
-
|
|
1209
|
-
return {
|
|
1210
|
-
status: 200,
|
|
1211
|
-
releaseResults,
|
|
1212
|
-
totalReleased,
|
|
1213
|
-
duration
|
|
1214
|
-
};
|
|
1215
|
-
|
|
1216
|
-
} catch (error: any) {
|
|
1217
|
-
const duration = Date.now() - startTime;
|
|
1218
|
-
|
|
1219
|
-
log.error('❌ Auto-release failed', {
|
|
1220
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1221
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1222
|
-
duration: `${duration}ms`,
|
|
1223
|
-
recommendation: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
1224
|
-
? 'Check Fluent Commerce API connectivity'
|
|
1225
|
-
: error.message?.includes('KV') || error.message?.includes('storage')
|
|
1226
|
-
? 'Check KV storage connectivity and promotion data'
|
|
1227
|
-
: 'Review error details and check active promotions list'
|
|
1228
|
-
});
|
|
1229
|
-
|
|
1230
|
-
return {
|
|
1231
|
-
status: 500,
|
|
1232
|
-
error: error.message,
|
|
1233
|
-
duration
|
|
1234
|
-
};
|
|
1235
|
-
}
|
|
1236
|
-
})
|
|
1237
|
-
);
|
|
1238
|
-
```
|
|
1239
|
-
|
|
1240
|
-
---
|
|
1241
|
-
|
|
1242
|
-
## Testing
|
|
1243
|
-
|
|
1244
|
-
### Start Flash Sale
|
|
1245
|
-
|
|
1246
|
-
```bash
|
|
1247
|
-
curl -X POST https://{workspace}.versori.run/start-flash-sale \
|
|
1248
|
-
-H "Content-Type: application/json" \
|
|
1249
|
-
-d '{
|
|
1250
|
-
"promotion": {
|
|
1251
|
-
"id": "FLASH-2025-01-17",
|
|
1252
|
-
"skus": [
|
|
1253
|
-
{ "sku": "SKU-001", "quantity": 100 },
|
|
1254
|
-
{ "sku": "SKU-002", "quantity": 50 }
|
|
1255
|
-
]
|
|
1256
|
-
}
|
|
1257
|
-
}'
|
|
1258
|
-
```
|
|
1259
|
-
|
|
1260
|
-
Expected response:
|
|
1261
|
-
|
|
1262
|
-
```json
|
|
1263
|
-
{
|
|
1264
|
-
"status": 200,
|
|
1265
|
-
"promotionId": "FLASH-2025-01-17",
|
|
1266
|
-
"reservations": [
|
|
1267
|
-
{
|
|
1268
|
-
"sku": "SKU-001",
|
|
1269
|
-
"success": true,
|
|
1270
|
-
"reservationRef": "FLASH-2025-01-17-SKU-001-1737110400000",
|
|
1271
|
-
"quantity": 100,
|
|
1272
|
-
"expiresAt": "2025-01-17T12:00:00.000Z"
|
|
1273
|
-
},
|
|
1274
|
-
{
|
|
1275
|
-
"sku": "SKU-002",
|
|
1276
|
-
"success": true,
|
|
1277
|
-
"reservationRef": "FLASH-2025-01-17-SKU-002-1737110400000",
|
|
1278
|
-
"quantity": 50,
|
|
1279
|
-
"expiresAt": "2025-01-17T12:00:00.000Z"
|
|
1280
|
-
}
|
|
1281
|
-
],
|
|
1282
|
-
"summary": {
|
|
1283
|
-
"success": 2,
|
|
1284
|
-
"errors": 0
|
|
1285
|
-
},
|
|
1286
|
-
"duration": 1234
|
|
1287
|
-
}
|
|
1288
|
-
```
|
|
1289
|
-
|
|
1290
|
-
### Consume Reservation
|
|
1291
|
-
|
|
1292
|
-
```bash
|
|
1293
|
-
curl -X POST https://{workspace}.versori.run/consume-reservation \
|
|
1294
|
-
-H "Content-Type: application/json" \
|
|
1295
|
-
-d '{
|
|
1296
|
-
"order": {
|
|
1297
|
-
"ref": "ORD-5001",
|
|
1298
|
-
"promotionId": "FLASH-2025-01-17",
|
|
1299
|
-
"items": [
|
|
1300
|
-
{ "sku": "SKU-001", "quantity": 2 }
|
|
1301
|
-
]
|
|
1302
|
-
}
|
|
1303
|
-
}'
|
|
1304
|
-
```
|
|
1305
|
-
|
|
1306
|
-
---
|
|
1307
|
-
|
|
1308
|
-
## Common Issues & Solutions
|
|
1309
|
-
|
|
1310
|
-
1. **Concurrent reservation failures**
|
|
1311
|
-
- Distributed lock timeout too short
|
|
1312
|
-
- Increase `lockTimeoutMs` or add retry logic
|
|
1313
|
-
2. **Reservations not releasing**
|
|
1314
|
-
- Verify auto-release schedule is running
|
|
1315
|
-
- Check expiration timestamp calculation
|
|
1316
|
-
3. **Overselling during flash sale**
|
|
1317
|
-
- Ensure consume webhook is called before order creation
|
|
1318
|
-
- Verify lock acquisition working correctly
|
|
1319
|
-
4. **Memory issues with large promotions**
|
|
1320
|
-
- Process SKUs in batches
|
|
1321
|
-
- Use streaming for reservation creation
|
|
1322
|
-
|
|
1323
|
-
---
|
|
1324
|
-
|
|
1325
|
-
## Production Checklist
|
|
1326
|
-
|
|
1327
|
-
- [ ] Reservation duration appropriate for sale type (30-120 min)
|
|
1328
|
-
- [ ] Auto-release schedule configured (every 10-15 min)
|
|
1329
|
-
- [ ] Distributed locking working correctly
|
|
1330
|
-
- [ ] Concurrent reservation handling tested
|
|
1331
|
-
- [ ] Consume webhook integrated with order flow
|
|
1332
|
-
- [ ] Reconciliation report generated after sale
|
|
1333
|
-
- [ ] Monitoring for reservation failures
|
|
1334
|
-
- [ ] Rollback plan for incorrect reservations
|
|
1335
|
-
|
|
1336
|
-
---
|
|
1337
|
-
|
|
1338
|
-
## Key Versori Syntax Patterns
|
|
1339
|
-
|
|
1340
|
-
### Webhook Definition
|
|
1341
|
-
```typescript
|
|
1342
|
-
// ✅ CORRECT - v0.1.27
|
|
1343
|
-
export const myWebhook = webhook('webhook-name', {
|
|
1344
|
-
response: {
|
|
1345
|
-
mode: 'sync' | 'async'
|
|
1346
|
-
}
|
|
1347
|
-
});
|
|
1348
|
-
```
|
|
1349
|
-
|
|
1350
|
-
### Schedule Definition
|
|
1351
|
-
```typescript
|
|
1352
|
-
// ✅ CORRECT - v0.1.27
|
|
1353
|
-
export const mySchedule = schedule('schedule-name', {
|
|
1354
|
-
cron: '*/10 * * * *' // Cron expression
|
|
1355
|
-
});
|
|
1356
|
-
```
|
|
1357
|
-
|
|
1358
|
-
### HTTP Step with Connection
|
|
1359
|
-
```typescript
|
|
1360
|
-
// ✅ CORRECT - v0.1.27
|
|
1361
|
-
.then(http('step-name', { connection: 'connection_name' }, async (ctx) => {
|
|
1362
|
-
const { log, openKv, activation, connections } = ctx;
|
|
1363
|
-
// ...
|
|
1364
|
-
}));
|
|
1365
|
-
```
|
|
1366
|
-
|
|
1367
|
-
### KV Storage Scopes
|
|
1368
|
-
```typescript
|
|
1369
|
-
// ✅ CORRECT - Three scope options
|
|
1370
|
-
ctx.openKv(':project:') // Project-wide scope (shared across all workflows)
|
|
1371
|
-
ctx.openKv(':workflow:') // Workflow-specific scope
|
|
1372
|
-
ctx.openKv(':activation:') // Activation-specific scope (single execution)
|
|
1373
|
-
|
|
1374
|
-
// Most common for reservation tracking: :project:
|
|
1375
|
-
const kv = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
1376
|
-
```
|
|
1377
|
-
|
|
1378
|
-
### StateService Usage
|
|
1379
|
-
```typescript
|
|
1380
|
-
// ✅ CORRECT - StateService doesn't need KV adapter
|
|
1381
|
-
const stateService = new StateService(log);
|
|
1382
|
-
|
|
1383
|
-
// For persistent state, use KV directly:
|
|
1384
|
-
await kv.set(key, value);
|
|
1385
|
-
const value = await kv.get(key);
|
|
1386
|
-
```
|
|
1387
|
-
|
|
1388
|
-
---
|
|
1389
|
-
|
|
1390
|
-
## Related Guides
|
|
1391
|
-
|
|
1392
|
-
- **Versori Platform**: `fc-connect-sdk/docs/04-REFERENCE/platforms/versori/`
|
|
1393
|
-
- **State Management**: `fc-connect-sdk/docs/03-PATTERN-GUIDES/file-operations/`
|
|
1394
|
-
- **Error Handling**: `fc-connect-sdk/docs/03-PATTERN-GUIDES/error-handling/`
|
|
1395
|
-
- **GraphQL Mutations**: `fc-connect-sdk/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/`
|
|
1
|
+
---
|
|
2
|
+
template_id: tpl-webhook-flash-sale-reserve
|
|
3
|
+
canonical_filename: template-webhook-flash-sale-reserve.md
|
|
4
|
+
version: 2.0.0
|
|
5
|
+
sdk_version: ^0.1.39
|
|
6
|
+
runtime: versori
|
|
7
|
+
direction: ingestion
|
|
8
|
+
source: webhook-json-promotion
|
|
9
|
+
destination: fluent-graphql
|
|
10
|
+
entity: inventory-reservation
|
|
11
|
+
format: json
|
|
12
|
+
logging: versori
|
|
13
|
+
status: stable
|
|
14
|
+
features:
|
|
15
|
+
- webhook-signature-validation
|
|
16
|
+
- batched-events
|
|
17
|
+
- attribute-transformation
|
|
18
|
+
- memory-management
|
|
19
|
+
- enhanced-logging
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Template: Webhook - Flash Sale Inventory Reserve
|
|
23
|
+
|
|
24
|
+
**Template Version:** 2.0.0
|
|
25
|
+
**SDK Version:** @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
26
|
+
**Last Updated:** 2025-01-24
|
|
27
|
+
**Deployment Target:** Versori Platform
|
|
28
|
+
|
|
29
|
+
**🆕 Version 2.0.0 Enhancements:**
|
|
30
|
+
- ✅ **Webhook Signature Validation** - Secure webhook verification with HMAC-SHA256
|
|
31
|
+
- ✅ **Batched Events** - Process events in optimized batches to reduce API calls
|
|
32
|
+
- ✅ **Attribute Transformation** - Handle nested arrays and complex data structures
|
|
33
|
+
- ✅ **Memory Management** - Clear large arrays after processing batches
|
|
34
|
+
- ✅ **Enhanced Logging** - Track batch processing and event submission with emoji indicators
|
|
35
|
+
|
|
36
|
+
**FC Connect SDK Use Case Guide**
|
|
37
|
+
|
|
38
|
+
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
39
|
+
> **Version**: Use latest - `npm install @fluentcommerce/fc-connect-sdk@latest`
|
|
40
|
+
|
|
41
|
+
**Context**: Reserve inventory for time-limited flash sales, prevent overselling during high-concurrency events, auto-release unreserved inventory after timer expires, and reconcile at sale end.
|
|
42
|
+
|
|
43
|
+
**Complexity**: High
|
|
44
|
+
|
|
45
|
+
**Runtime**: Versori Platform
|
|
46
|
+
|
|
47
|
+
**Estimated Lines**: ~800 lines (modular structure)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## STEP 1: Understand This Template
|
|
52
|
+
|
|
53
|
+
**What This Template Does:**
|
|
54
|
+
|
|
55
|
+
- HTTP webhook triggered by promotion start event
|
|
56
|
+
- Inventory reservation with GraphQL mutations
|
|
57
|
+
- Time-bound reservations (auto-release after X minutes)
|
|
58
|
+
- Concurrency control to prevent race conditions
|
|
59
|
+
- Real-time order consumption of reservations
|
|
60
|
+
- Auto-release of unused reservations after expiry
|
|
61
|
+
- Reconciliation report after sale ends
|
|
62
|
+
- State tracking for reservation management
|
|
63
|
+
- **Sync + Fire-and-Forget Pattern**: Fast webhook response, background processing
|
|
64
|
+
|
|
65
|
+
**Key SDK Components:**
|
|
66
|
+
|
|
67
|
+
- `createClient()` - Universal client factory (auto-detects Versori context)
|
|
68
|
+
- `VersoriKVAdapter` - Reservation state storage (KV)
|
|
69
|
+
- `StateService` - Concurrency control
|
|
70
|
+
- GraphQL mutations - Reserve and consume inventory
|
|
71
|
+
- Native Versori `log` - Use `log` from context
|
|
72
|
+
|
|
73
|
+
**Entity Type:**
|
|
74
|
+
|
|
75
|
+
- **InventoryReservation** - Fluent entity for inventory reservations
|
|
76
|
+
- **GraphQL Mutations** - Reserve inventory, consume reservations
|
|
77
|
+
|
|
78
|
+
**Critical Patterns:**
|
|
79
|
+
|
|
80
|
+
- **Sync + Fire-and-Forget**: Webhook validates quickly, returns immediately, processes reservations in background
|
|
81
|
+
- **External JSON Config**: Reservation configuration in separate JSON file (`config/reservation-config.json`)
|
|
82
|
+
- **Modular Architecture**: Separate services, workflows, config, types folders
|
|
83
|
+
- **Background Processing**: Long-running operations (GraphQL mutations, state updates) happen asynchronously
|
|
84
|
+
- **State Management**: KV storage for reservation tracking across webhooks
|
|
85
|
+
|
|
86
|
+
**When to Use This Template:**
|
|
87
|
+
|
|
88
|
+
- ✅ Flash sale inventory reservations
|
|
89
|
+
- ✅ Time-bound inventory holds
|
|
90
|
+
- ✅ High-concurrency scenarios
|
|
91
|
+
- ✅ Need fast webhook response (don't wait for reservation creation)
|
|
92
|
+
- ✅ Auto-release expired reservations
|
|
93
|
+
|
|
94
|
+
**When NOT to Use:**
|
|
95
|
+
|
|
96
|
+
- ❌ Permanent inventory allocation (use direct inventory updates)
|
|
97
|
+
- ❌ Bulk reservation processing (use Batch API or scheduled workflows)
|
|
98
|
+
- ❌ Need synchronous reservation (wait for result before responding)
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## STEP 2: Implementation Prompt for Claude Code
|
|
103
|
+
|
|
104
|
+
**Copy this prompt and send to Claude Code to generate the complete implementation:**
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
Create a Versori webhook workflow for flash sale inventory reservation to Fluent Commerce.
|
|
108
|
+
|
|
109
|
+
REQUIREMENTS:
|
|
110
|
+
1. Runtime: Versori Platform (HTTP webhook)
|
|
111
|
+
2. Source: Promotion data via HTTP POST webhook (JSON)
|
|
112
|
+
3. Destination: Fluent Commerce GraphQL API (reserve inventory mutations)
|
|
113
|
+
4. Format: JSON promotion data
|
|
114
|
+
5. Entity: InventoryReservation (GraphQL mutations)
|
|
115
|
+
|
|
116
|
+
KEY FEATURES:
|
|
117
|
+
- Sync + fire-and-forget pattern (fast webhook response, background processing)
|
|
118
|
+
- External JSON configuration (config/reservation-config.json)
|
|
119
|
+
- Modular architecture (workflows/, services/, config/, types/)
|
|
120
|
+
- Inventory reservation with GraphQL mutations
|
|
121
|
+
- Time-bound reservations (auto-release after expiry)
|
|
122
|
+
- Concurrency control (StateService)
|
|
123
|
+
- KV storage for reservation tracking
|
|
124
|
+
- Comprehensive error handling
|
|
125
|
+
|
|
126
|
+
CRITICAL REQUIREMENTS:
|
|
127
|
+
1. Webhook Mode: response: { mode: 'sync' } (fast response)
|
|
128
|
+
2. Background Processing: Fire-and-forget pattern (no await on long operations)
|
|
129
|
+
3. Reservation Config: External JSON file (config/reservation-config.json)
|
|
130
|
+
4. Modular Structure: Separate services/, config/, types/ folders
|
|
131
|
+
5. Native Logging: Use log from context (no LoggingService)
|
|
132
|
+
6. State Management: VersoriKVAdapter for reservation tracking
|
|
133
|
+
|
|
134
|
+
SDK METHODS TO USE:
|
|
135
|
+
- createClient({ ...ctx, log }) - Pass full Versori context
|
|
136
|
+
- new VersoriKVAdapter(openKv(':project:')) - Reservation state storage
|
|
137
|
+
- new StateService(log) - Concurrency control
|
|
138
|
+
- client.graphql({ query, variables }) - Execute GraphQL mutations
|
|
139
|
+
|
|
140
|
+
FORBIDDEN PATTERNS:
|
|
141
|
+
- ❌ Inline config (use external JSON)
|
|
142
|
+
- ❌ await on background processing (use fire-and-forget)
|
|
143
|
+
- ❌ LoggingService (use native log from context)
|
|
144
|
+
- ❌ All code in one file (use modular structure)
|
|
145
|
+
- ❌ async mode webhook (use sync + fire-and-forget)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## STEP 3: Detailed Flow Documentation
|
|
151
|
+
|
|
152
|
+
### Complete Processing Flow
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
156
|
+
│ 1. WEBHOOK RECEIVED │
|
|
157
|
+
│ POST https://{workspace}.versori.run/start-flash-sale │
|
|
158
|
+
│ Content-Type: application/json │
|
|
159
|
+
│ Body: { promotion: { id: "...", skus: [...] } } │
|
|
160
|
+
└────────────────────┬────────────────────────────────────────┘
|
|
161
|
+
│
|
|
162
|
+
▼
|
|
163
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
164
|
+
│ 2. QUICK VALIDATION (Synchronous, ~10-50ms) │
|
|
165
|
+
│ - Check fluent_commerce connection exists │
|
|
166
|
+
│ - Validate promotion payload present │
|
|
167
|
+
│ - Return HTTP 200 OK immediately │
|
|
168
|
+
└────────────────────┬────────────────────────────────────────┘
|
|
169
|
+
│
|
|
170
|
+
▼
|
|
171
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
172
|
+
│ 3. BACKGROUND PROCESSING (Fire-and-Forget) │
|
|
173
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
174
|
+
│ │ 3a. Initialize Fluent Client │ │
|
|
175
|
+
│ │ - createClient({ ...ctx, log }) │ │
|
|
176
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
177
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
178
|
+
│ │ 3b. Initialize State Services │ │
|
|
179
|
+
│ │ - VersoriKVAdapter for state storage │ │
|
|
180
|
+
│ │ - StateService for concurrency control │ │
|
|
181
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
182
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
183
|
+
│ │ 3c. Reserve Inventory for Each SKU │ │
|
|
184
|
+
│ │ - GraphQL mutation to reserve │ │
|
|
185
|
+
│ │ - Store reservation in KV │ │
|
|
186
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
187
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
188
|
+
│ │ 3d. Track Promotion State │ │
|
|
189
|
+
│ │ - Store promotion metadata in KV │ │
|
|
190
|
+
│ │ - Set expiry timer │ │
|
|
191
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
192
|
+
└─────────────────────────────────────────────────────────────┘
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Response Timing
|
|
196
|
+
|
|
197
|
+
| Stage | Timing | Blocking |
|
|
198
|
+
|-------|--------|----------|
|
|
199
|
+
| **Webhook Validation** | ~10-50ms | ✅ Yes (blocks response) |
|
|
200
|
+
| **Background Processing** | ~2000-5000ms | ❌ No (fire-and-forget) |
|
|
201
|
+
| **Total Response Time** | ~10-50ms | ✅ Fast response |
|
|
202
|
+
|
|
203
|
+
**Key Benefit**: Webhook caller receives immediate acknowledgment (~50ms) while reservations happen in background (~2-5s).
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## STEP 4: Production Modular Structure
|
|
208
|
+
|
|
209
|
+
> **✅ This section shows the COMPLETE production-ready modular structure.**
|
|
210
|
+
> All files are shown with proper imports/exports and folder organization.
|
|
211
|
+
|
|
212
|
+
### Complete Project Structure
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
flash-sale-reserve/
|
|
216
|
+
├── package.json # Dependencies and Versori config
|
|
217
|
+
├── index.ts # Entry point - exports all workflows
|
|
218
|
+
└── src/
|
|
219
|
+
├── workflows/
|
|
220
|
+
│ ├── webhook/
|
|
221
|
+
│ │ ├── start-flash-sale.ts # Webhook: Reserve inventory
|
|
222
|
+
│ │ └── consume-reservation.ts # Webhook: Consume reservations
|
|
223
|
+
│ │
|
|
224
|
+
│ └── scheduled/
|
|
225
|
+
│ └── auto-release.ts # Scheduled: Release expired reservations
|
|
226
|
+
│
|
|
227
|
+
├── services/
|
|
228
|
+
│ └── reservation-service.ts # Shared orchestration logic (reusable)
|
|
229
|
+
│
|
|
230
|
+
├── config/
|
|
231
|
+
│ └── reservation-config.json # Configuration (external JSON)
|
|
232
|
+
│
|
|
233
|
+
└── types/
|
|
234
|
+
└── reservation.types.ts # TypeScript interfaces
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Why This Structure?**
|
|
238
|
+
|
|
239
|
+
- ✅ **Clear separation**: Webhook handlers vs scheduled tasks vs business logic
|
|
240
|
+
- ✅ **Reusable services**: Reservation logic can be reused
|
|
241
|
+
- ✅ **External config**: Configuration changes don't require code changes
|
|
242
|
+
- ✅ **Type safety**: TypeScript interfaces for better IDE support
|
|
243
|
+
- ✅ **Scalable**: Easy to add new reservation types or workflows
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## SDK Imports
|
|
248
|
+
|
|
249
|
+
**All imports validated against SDK v0.1.27 exports** - this code compiles and runs as-is.
|
|
250
|
+
|
|
251
|
+
**Logging (Versori Platform):**
|
|
252
|
+
- Use native `log` from context: `const { log } = ctx;`
|
|
253
|
+
- Do NOT import or use SDK logging utilities
|
|
254
|
+
- Native Versori logs are automatically integrated with platform monitoring
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import {
|
|
258
|
+
createClient,
|
|
259
|
+
VersoriKVAdapter,
|
|
260
|
+
StateService
|
|
261
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
262
|
+
|
|
263
|
+
// Key SDK methods used:
|
|
264
|
+
await createClient(ctx); // Versori-aware client factory
|
|
265
|
+
await client.graphql({ query, variables }); // GraphQL mutations
|
|
266
|
+
new VersoriKVAdapter(ctx.openKv(scope)); // Reservation state storage (KV)
|
|
267
|
+
new StateService(log); // Concurrency control (logger only)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Versori Workflows Structure
|
|
273
|
+
|
|
274
|
+
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
275
|
+
|
|
276
|
+
**Trigger Types:**
|
|
277
|
+
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
278
|
+
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
279
|
+
- **`http()`** → External API calls (chained from webhook/schedule)
|
|
280
|
+
- **`fn()`** → Internal processing (chained from webhook/schedule)
|
|
281
|
+
|
|
282
|
+
### Recommended Project Structure
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
flash-sale-reserve/
|
|
286
|
+
├── index.ts # Entry point - MemoryInterpreter registration
|
|
287
|
+
└── src/
|
|
288
|
+
├── workflows/
|
|
289
|
+
│ ├── webhook/
|
|
290
|
+
│ │ ├── start-flash-sale.ts # Webhook: Reserve inventory
|
|
291
|
+
│ │ └── consume-reservation.ts # Webhook: Consume reservations
|
|
292
|
+
│ │
|
|
293
|
+
│ └── scheduled/
|
|
294
|
+
│ └── auto-release.ts # Scheduled: Release expired reservations
|
|
295
|
+
│
|
|
296
|
+
├── services/
|
|
297
|
+
│ └── reservation-service.ts # Shared orchestration logic (reusable)
|
|
298
|
+
│
|
|
299
|
+
└── config/
|
|
300
|
+
└── reservation-config.json # Configuration
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Benefits:**
|
|
304
|
+
- ✅ Clear trigger separation (`webhook/` vs `scheduled/`)
|
|
305
|
+
- ✅ Descriptive file names (easy to browse and understand)
|
|
306
|
+
- ✅ Scalable (add new workflows without cluttering)
|
|
307
|
+
- ✅ Reusable code in `services/` (DRY principle)
|
|
308
|
+
- ✅ Easy to modify individual workflows without affecting others
|
|
309
|
+
- ✅ MemoryInterpreter pattern prevents "unexpected export" errors
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Project Setup
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
mkdir versori-flash-sale-reserve && cd $_
|
|
317
|
+
npm init -y
|
|
318
|
+
npm install @fluentcommerce/fc-connect-sdk@^0.1.27 @versori/run
|
|
319
|
+
mkdir -p src/services
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Package Configuration (package.json)
|
|
323
|
+
|
|
324
|
+
```json
|
|
325
|
+
{
|
|
326
|
+
"name": "versori-flash-sale-reserve",
|
|
327
|
+
"version": "1.0.0",
|
|
328
|
+
"versori": {
|
|
329
|
+
"workflows": "./src/index.ts"
|
|
330
|
+
},
|
|
331
|
+
"type": "module",
|
|
332
|
+
"scripts": {
|
|
333
|
+
"deploy": "versori deploy",
|
|
334
|
+
"logs": "versori logs"
|
|
335
|
+
},
|
|
336
|
+
"dependencies": {
|
|
337
|
+
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
338
|
+
"@versori/run": "latest"
|
|
339
|
+
},
|
|
340
|
+
"devDependencies": {
|
|
341
|
+
"typescript": "^5.0.0",
|
|
342
|
+
"@types/node": "^20.0.0"
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Activation Variables
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
# Retailer
|
|
351
|
+
retailerId=your-retailer-id
|
|
352
|
+
|
|
353
|
+
# Reservation configuration
|
|
354
|
+
reservationDurationMinutes=120 # 2 hours
|
|
355
|
+
autoReleaseEnabled=true
|
|
356
|
+
|
|
357
|
+
# Concurrency control
|
|
358
|
+
lockTimeoutMs=5000
|
|
359
|
+
maxRetries=3
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Flash Sale Flow
|
|
365
|
+
|
|
366
|
+
```
|
|
367
|
+
Promotion Start Event →
|
|
368
|
+
Parse promotion (SKUs, quantity, duration)
|
|
369
|
+
For each SKU:
|
|
370
|
+
- Query available inventory
|
|
371
|
+
- Create reservation mutation (status: RESERVED, promotion link)
|
|
372
|
+
- Set expiration timestamp in KV
|
|
373
|
+
|
|
374
|
+
During sale:
|
|
375
|
+
- Orders consume reservations (real-time)
|
|
376
|
+
- Track remaining reserved qty in KV
|
|
377
|
+
|
|
378
|
+
After timer expires:
|
|
379
|
+
- Query unreserved inventory
|
|
380
|
+
- Release back to AVAILABLE
|
|
381
|
+
- Generate reconciliation report
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Reservation Service Implementation
|
|
387
|
+
|
|
388
|
+
**File:** `src/services/reservation-service.ts`
|
|
389
|
+
|
|
390
|
+
**Note on Connection Validation:**
|
|
391
|
+
This template uses direct GraphQL queries via FluentClient and does not require explicit connection validation. If you're using SFTP or S3 data sources in your implementation, add `await dataSource.validateConnection()` after initialization.
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
// src/services/reservation-service.ts
|
|
395
|
+
import { FluentClient } from '@fluentcommerce/fc-connect-sdk';
|
|
396
|
+
|
|
397
|
+
export class ReservationService {
|
|
398
|
+
constructor(
|
|
399
|
+
private client: any,
|
|
400
|
+
private stateService: any,
|
|
401
|
+
private retailerId: string,
|
|
402
|
+
private log: any
|
|
403
|
+
) {}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Reserve inventory for flash sale
|
|
407
|
+
*/
|
|
408
|
+
async reserveInventory(
|
|
409
|
+
promotionId: string,
|
|
410
|
+
skus: Array<{ sku: string; quantity: number }>,
|
|
411
|
+
durationMinutes: number
|
|
412
|
+
): Promise<any[]> {
|
|
413
|
+
const results: any[] = [];
|
|
414
|
+
const expiresAt = new Date(Date.now() + durationMinutes * 60000);
|
|
415
|
+
|
|
416
|
+
for (const item of skus) {
|
|
417
|
+
try {
|
|
418
|
+
// Step 1: Query available inventory
|
|
419
|
+
const available = await this.getAvailableInventory(item.sku);
|
|
420
|
+
if (available < item.quantity) {
|
|
421
|
+
this.log.warn('Insufficient inventory for reservation', {
|
|
422
|
+
sku: item.sku,
|
|
423
|
+
requested: item.quantity,
|
|
424
|
+
available
|
|
425
|
+
});
|
|
426
|
+
results.push({
|
|
427
|
+
sku: item.sku,
|
|
428
|
+
success: false,
|
|
429
|
+
error: 'Insufficient inventory'
|
|
430
|
+
});
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Step 2: Create reservation (with distributed lock)
|
|
435
|
+
const lockKey = `lock:inventory:${item.sku}`;
|
|
436
|
+
const acquired = await this.acquireLock(lockKey);
|
|
437
|
+
if (!acquired) {
|
|
438
|
+
results.push({
|
|
439
|
+
sku: item.sku,
|
|
440
|
+
success: false,
|
|
441
|
+
error: 'Failed to acquire lock (concurrent reservation)'
|
|
442
|
+
});
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
// Create reservation via GraphQL mutation
|
|
448
|
+
const reservationRef = `${promotionId}-${item.sku}-${Date.now()}`;
|
|
449
|
+
const reserveMutation = `
|
|
450
|
+
mutation ReserveInventory($input: CreateInventoryInput!) {
|
|
451
|
+
createInventory(input: $input) {
|
|
452
|
+
id
|
|
453
|
+
ref
|
|
454
|
+
productRef
|
|
455
|
+
quantity
|
|
456
|
+
status
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
`;
|
|
460
|
+
|
|
461
|
+
const input = {
|
|
462
|
+
ref: reservationRef,
|
|
463
|
+
retailerId: this.retailerId,
|
|
464
|
+
productRef: item.sku,
|
|
465
|
+
quantity: item.quantity,
|
|
466
|
+
status: 'RESERVED',
|
|
467
|
+
attributes: [
|
|
468
|
+
{ name: 'promotion_id', value: promotionId },
|
|
469
|
+
{ name: 'expires_at', value: expiresAt.toISOString() },
|
|
470
|
+
{ name: 'reservation_type', value: 'FLASH_SALE' }
|
|
471
|
+
]
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const response = await this.client.graphql({
|
|
475
|
+
query: reserveMutation,
|
|
476
|
+
variables: { input }
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const reservation = response?.data?.createInventory;
|
|
480
|
+
|
|
481
|
+
// Store reservation metadata in KV
|
|
482
|
+
const stateKey = `reservation:${reservationRef}`;
|
|
483
|
+
await this.stateService.setState(stateKey, {
|
|
484
|
+
promotionId,
|
|
485
|
+
sku: item.sku,
|
|
486
|
+
quantity: item.quantity,
|
|
487
|
+
reserved: item.quantity,
|
|
488
|
+
consumed: 0,
|
|
489
|
+
expiresAt: expiresAt.toISOString(),
|
|
490
|
+
createdAt: new Date().toISOString()
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
this.log.info('Inventory reserved', {
|
|
494
|
+
sku: item.sku,
|
|
495
|
+
quantity: item.quantity,
|
|
496
|
+
reservationRef,
|
|
497
|
+
expiresAt
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
results.push({
|
|
501
|
+
sku: item.sku,
|
|
502
|
+
success: true,
|
|
503
|
+
reservationRef,
|
|
504
|
+
quantity: item.quantity,
|
|
505
|
+
expiresAt
|
|
506
|
+
});
|
|
507
|
+
} finally {
|
|
508
|
+
await this.releaseLock(lockKey);
|
|
509
|
+
}
|
|
510
|
+
} catch (error: any) {
|
|
511
|
+
// ? Enhanced: Error logging with recommendations
|
|
512
|
+
this.log.error('[FlashSaleReserve] Reservation failed', {
|
|
513
|
+
sku: item.sku,
|
|
514
|
+
error: error instanceof Error ? error.message : String(error),
|
|
515
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
516
|
+
recommendation: error.message?.includes('lock') || error.message?.includes('concurrent')
|
|
517
|
+
? 'Concurrent reservation attempt detected - retry after lock release'
|
|
518
|
+
: error.message?.includes('inventory') || error.message?.includes('quantity')
|
|
519
|
+
? 'Check available inventory quantity and reservation limits'
|
|
520
|
+
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
521
|
+
? 'Check network connectivity and Fluent Commerce API availability'
|
|
522
|
+
: 'Review error details and check SKU/product availability'
|
|
523
|
+
});
|
|
524
|
+
results.push({
|
|
525
|
+
sku: item.sku,
|
|
526
|
+
success: false,
|
|
527
|
+
error: error.message
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return results;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Consume reservation when order is placed
|
|
537
|
+
*/
|
|
538
|
+
async consumeReservation(
|
|
539
|
+
promotionId: string,
|
|
540
|
+
sku: string,
|
|
541
|
+
quantity: number
|
|
542
|
+
): Promise<boolean> {
|
|
543
|
+
const stateKey = `reservation:${promotionId}-${sku}`;
|
|
544
|
+
try {
|
|
545
|
+
const reservation = await this.stateService.getState(stateKey);
|
|
546
|
+
if (!reservation) {
|
|
547
|
+
this.log.warn('Reservation not found', { promotionId, sku });
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const remaining = reservation.reserved - reservation.consumed;
|
|
552
|
+
if (remaining < quantity) {
|
|
553
|
+
this.log.warn('Insufficient reserved quantity', {
|
|
554
|
+
promotionId,
|
|
555
|
+
sku,
|
|
556
|
+
requested: quantity,
|
|
557
|
+
remaining
|
|
558
|
+
});
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Update consumed count
|
|
563
|
+
reservation.consumed += quantity;
|
|
564
|
+
await this.stateService.setState(stateKey, reservation);
|
|
565
|
+
|
|
566
|
+
this.log.info('Reservation consumed', {
|
|
567
|
+
promotionId,
|
|
568
|
+
sku,
|
|
569
|
+
quantity,
|
|
570
|
+
remaining: reservation.reserved - reservation.consumed
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
return true;
|
|
574
|
+
} catch (error: any) {
|
|
575
|
+
// ? Enhanced: Error logging with recommendations
|
|
576
|
+
this.log.error('[FlashSaleReserve] Failed to consume reservation', {
|
|
577
|
+
promotionId,
|
|
578
|
+
sku,
|
|
579
|
+
error: error instanceof Error ? error.message : String(error),
|
|
580
|
+
recommendation: error.message?.includes('not found') || error.message?.includes('missing')
|
|
581
|
+
? 'Reservation may have expired or was not created - verify promotion and reservation state'
|
|
582
|
+
: error.message?.includes('insufficient') || error.message?.includes('quantity')
|
|
583
|
+
? 'Insufficient reserved quantity - check reservation consumption limits'
|
|
584
|
+
: 'Review error details and check reservation state in KV store'
|
|
585
|
+
});
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Release expired reservations back to AVAILABLE
|
|
592
|
+
*/
|
|
593
|
+
async releaseExpiredReservations(promotionId: string): Promise<any> {
|
|
594
|
+
try {
|
|
595
|
+
// Query all reservations for promotion
|
|
596
|
+
const query = `
|
|
597
|
+
query GetReservations($retailerId: ID!, $promotionId: String!) {
|
|
598
|
+
inventories(
|
|
599
|
+
retailerId: $retailerId
|
|
600
|
+
status: "RESERVED"
|
|
601
|
+
attributes: [{ name: "promotion_id", value: $promotionId }]
|
|
602
|
+
) {
|
|
603
|
+
edges {
|
|
604
|
+
node {
|
|
605
|
+
id
|
|
606
|
+
ref
|
|
607
|
+
productRef
|
|
608
|
+
quantity
|
|
609
|
+
attributes {
|
|
610
|
+
name
|
|
611
|
+
value
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
`;
|
|
618
|
+
|
|
619
|
+
const response = await this.client.graphql({
|
|
620
|
+
query,
|
|
621
|
+
variables: { retailerId: this.retailerId, promotionId }
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
const reservations = response?.data?.inventories?.edges || [];
|
|
625
|
+
const released: any[] = [];
|
|
626
|
+
const now = new Date();
|
|
627
|
+
|
|
628
|
+
for (const edge of reservations) {
|
|
629
|
+
const reservation = edge.node;
|
|
630
|
+
const expiresAtAttr = reservation.attributes?.find((a: any) => a.name === 'expires_at');
|
|
631
|
+
const expiresAt = expiresAtAttr ? new Date(expiresAtAttr.value) : null;
|
|
632
|
+
|
|
633
|
+
if (!expiresAt || expiresAt > now) {
|
|
634
|
+
continue; // Not yet expired
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Check state to see if fully consumed
|
|
638
|
+
const stateKey = `reservation:${reservation.ref}`;
|
|
639
|
+
const state = await this.stateService.getState(stateKey);
|
|
640
|
+
if (state && state.consumed >= state.reserved) {
|
|
641
|
+
this.log.debug('Reservation fully consumed, skipping release', { ref: reservation.ref });
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const unreservedQty = state ? state.reserved - state.consumed : reservation.quantity;
|
|
646
|
+
|
|
647
|
+
// Release unreserved quantity back to AVAILABLE
|
|
648
|
+
const releaseMutation = `
|
|
649
|
+
mutation UpdateInventory($input: UpdateInventoryInput!) {
|
|
650
|
+
updateInventory(input: $input) {
|
|
651
|
+
id
|
|
652
|
+
ref
|
|
653
|
+
status
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
`;
|
|
657
|
+
|
|
658
|
+
const input = {
|
|
659
|
+
ref: reservation.ref,
|
|
660
|
+
status: 'AVAILABLE',
|
|
661
|
+
quantity: unreservedQty
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
await this.client.graphql({
|
|
665
|
+
query: releaseMutation,
|
|
666
|
+
variables: { input }
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
released.push({
|
|
670
|
+
ref: reservation.ref,
|
|
671
|
+
sku: reservation.productRef,
|
|
672
|
+
quantity: unreservedQty
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
this.log.info('Reservation released', {
|
|
676
|
+
ref: reservation.ref,
|
|
677
|
+
sku: reservation.productRef,
|
|
678
|
+
quantity: unreservedQty
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
promotionId,
|
|
684
|
+
released: released.length,
|
|
685
|
+
details: released
|
|
686
|
+
};
|
|
687
|
+
} catch (error: any) {
|
|
688
|
+
this.log.error('Failed to release reservations', { promotionId, error: error.message });
|
|
689
|
+
throw error;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Query available inventory (non-reserved)
|
|
695
|
+
*/
|
|
696
|
+
private async getAvailableInventory(sku: string): Promise<number> {
|
|
697
|
+
const query = `
|
|
698
|
+
query GetInventory($sku: String!, $retailerId: ID!) {
|
|
699
|
+
inventories(
|
|
700
|
+
first: 10
|
|
701
|
+
productRef: [$sku]
|
|
702
|
+
retailerId: $retailerId
|
|
703
|
+
status: "AVAILABLE"
|
|
704
|
+
) {
|
|
705
|
+
edges {
|
|
706
|
+
node {
|
|
707
|
+
quantity
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
`;
|
|
713
|
+
|
|
714
|
+
const response = await this.client.graphql({
|
|
715
|
+
query,
|
|
716
|
+
variables: { sku, retailerId: this.retailerId }
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
const inventories = response?.data?.inventories?.edges || [];
|
|
720
|
+
return inventories.reduce((sum: number, edge: any) => sum + (edge.node.quantity || 0), 0);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Distributed lock for concurrency control
|
|
725
|
+
*/
|
|
726
|
+
private async acquireLock(lockKey: string): Promise<boolean> {
|
|
727
|
+
try {
|
|
728
|
+
const existing = await this.stateService.getState(lockKey);
|
|
729
|
+
if (existing && existing.lockedUntil > Date.now()) {
|
|
730
|
+
return false; // Lock already held
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Set lock with timeout
|
|
734
|
+
const lockTimeoutMs = 5000; // 5 seconds
|
|
735
|
+
await this.stateService.setState(lockKey, {
|
|
736
|
+
locked: true,
|
|
737
|
+
lockedAt: Date.now(),
|
|
738
|
+
lockedUntil: Date.now() + lockTimeoutMs
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
return true;
|
|
742
|
+
} catch (error: any) {
|
|
743
|
+
// ? Enhanced: Error logging with recommendations
|
|
744
|
+
this.log.warn('[FlashSaleReserve] Failed to acquire lock', {
|
|
745
|
+
lockKey,
|
|
746
|
+
error: error instanceof Error ? error.message : String(error),
|
|
747
|
+
recommendation: 'Lock may be held by another process - retry after lock timeout or check KV store connectivity'
|
|
748
|
+
});
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Release distributed lock
|
|
755
|
+
*/
|
|
756
|
+
private async releaseLock(lockKey: string): Promise<void> {
|
|
757
|
+
try {
|
|
758
|
+
await this.stateService.setState(lockKey, {
|
|
759
|
+
locked: false,
|
|
760
|
+
releasedAt: Date.now()
|
|
761
|
+
});
|
|
762
|
+
} catch (error: any) {
|
|
763
|
+
this.log.warn('Failed to release lock', { lockKey, error: error.message });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
---
|
|
770
|
+
|
|
771
|
+
## Complete Workflow Implementation
|
|
772
|
+
|
|
773
|
+
### Entry Point with MemoryInterpreter Pattern
|
|
774
|
+
|
|
775
|
+
**File:** `index.ts` (project root)
|
|
776
|
+
|
|
777
|
+
```typescript
|
|
778
|
+
/**
|
|
779
|
+
* Entry Point - Registers all workflows with Versori platform
|
|
780
|
+
*
|
|
781
|
+
* ⚠️ CRITICAL PATTERN: MemoryInterpreter Setup
|
|
782
|
+
* This pattern ensures workflows execute in the correct Versori runtime context
|
|
783
|
+
* and prevents "unexpected export" errors during deployment.
|
|
784
|
+
*
|
|
785
|
+
* COMPONENTS:
|
|
786
|
+
* 1. Import ALL workflows (scheduled, webhook, etc.)
|
|
787
|
+
* 2. Create MemoryInterpreter with workflow array
|
|
788
|
+
* 3. NO direct exports - interpreter handles workflow registration
|
|
789
|
+
*/
|
|
790
|
+
|
|
791
|
+
import { MemoryInterpreter } from '@versori/run';
|
|
792
|
+
|
|
793
|
+
// Import workflows
|
|
794
|
+
import { startFlashSale } from './src/workflows/webhook/start-flash-sale';
|
|
795
|
+
import { consumeReservation } from './src/workflows/webhook/consume-reservation';
|
|
796
|
+
import { autoReleaseReservations } from './src/workflows/scheduled/auto-release';
|
|
797
|
+
|
|
798
|
+
// ✅ CORRECT: Register via MemoryInterpreter (Versori standard)
|
|
799
|
+
new MemoryInterpreter([
|
|
800
|
+
// Webhooks (HTTP-based triggers)
|
|
801
|
+
startFlashSale,
|
|
802
|
+
consumeReservation,
|
|
803
|
+
|
|
804
|
+
// Scheduled (time-based triggers)
|
|
805
|
+
autoReleaseReservations,
|
|
806
|
+
]);
|
|
807
|
+
|
|
808
|
+
// ❌ WRONG: Direct exports cause "unexpected export" errors
|
|
809
|
+
// export { startFlashSale, consumeReservation, autoReleaseReservations };
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
**What Gets Exposed:**
|
|
813
|
+
- ✅ `startFlashSale` → `https://{workspace}.versori.run/start-flash-sale`
|
|
814
|
+
- ✅ `consumeReservation` → `https://{workspace}.versori.run/consume-reservation`
|
|
815
|
+
- ❌ `autoReleaseReservations` → NOT exposed (runs automatically on cron)
|
|
816
|
+
|
|
817
|
+
---
|
|
818
|
+
|
|
819
|
+
### Workflow Implementations
|
|
820
|
+
|
|
821
|
+
**File:** `src/workflows/webhook/start-flash-sale.ts`
|
|
822
|
+
|
|
823
|
+
```typescript
|
|
824
|
+
import { webhook, http } from '@versori/run';
|
|
825
|
+
import {
|
|
826
|
+
createClient,
|
|
827
|
+
VersoriKVAdapter,
|
|
828
|
+
StateService
|
|
829
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
830
|
+
import { ReservationService } from '../../services/reservation-service';
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Webhook: Start flash sale and reserve inventory
|
|
834
|
+
*
|
|
835
|
+
* Endpoint: POST https://{workspace}.versori.run/start-flash-sale
|
|
836
|
+
* Request body: { promotion: { id: "FLASH-001", skus: [{ sku: "SKU-001", quantity: 100 }] } }
|
|
837
|
+
*/
|
|
838
|
+
export const startFlashSale = webhook('start-flash-sale', {
|
|
839
|
+
response: { mode: 'sync' }
|
|
840
|
+
}).then(http('reserve-inventory', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
841
|
+
const { log, openKv, activation, data } = ctx;
|
|
842
|
+
const startTime = Date.now();
|
|
843
|
+
|
|
844
|
+
log.info('🚀 Starting flash sale inventory reservation');
|
|
845
|
+
|
|
846
|
+
try {
|
|
847
|
+
// ========================================
|
|
848
|
+
// STEP 1: VALIDATION & CLIENT INITIALIZATION
|
|
849
|
+
// ========================================
|
|
850
|
+
|
|
851
|
+
// ? Enhanced: Configuration validation with error recommendations
|
|
852
|
+
const retailerId = activation?.getVariable('retailerId');
|
|
853
|
+
if (!retailerId) {
|
|
854
|
+
log.error('❌ Configuration error: Missing retailerId', {
|
|
855
|
+
recommendation: 'Configure retailerId in Activation Variables section'
|
|
856
|
+
});
|
|
857
|
+
return {
|
|
858
|
+
status: 500,
|
|
859
|
+
error: 'Missing retailerId configuration',
|
|
860
|
+
recommendation: 'Add retailerId to Activation Variables'
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const client = await createClient(ctx);
|
|
865
|
+
client.setRetailerId(retailerId);
|
|
866
|
+
log.info('✅ Client initialized', { retailerId });
|
|
867
|
+
|
|
868
|
+
// ========================================
|
|
869
|
+
// STEP 2: SERVICE INITIALIZATION
|
|
870
|
+
// ========================================
|
|
871
|
+
|
|
872
|
+
const kv = new VersoriKVAdapter(openKv(':project:'));
|
|
873
|
+
const stateService = new StateService(log);
|
|
874
|
+
const durationMinutes = Number(activation?.getVariable('reservationDurationMinutes') || 120);
|
|
875
|
+
|
|
876
|
+
const reservationService = new ReservationService(client, stateService, retailerId, log);
|
|
877
|
+
|
|
878
|
+
log.info('🔍 Configuration loaded', { durationMinutes });
|
|
879
|
+
|
|
880
|
+
// ========================================
|
|
881
|
+
// STEP 3: VALIDATE REQUEST PAYLOAD
|
|
882
|
+
// ========================================
|
|
883
|
+
|
|
884
|
+
const promotion = data?.promotion || {};
|
|
885
|
+
const promotionId = promotion.id || promotion.ref;
|
|
886
|
+
const skus = promotion.skus || [];
|
|
887
|
+
|
|
888
|
+
if (!promotionId || skus.length === 0) {
|
|
889
|
+
log.error('❌ Invalid request payload', {
|
|
890
|
+
hasPromotionId: !!promotionId,
|
|
891
|
+
skuCount: skus.length,
|
|
892
|
+
recommendation: 'Ensure request includes: { promotion: { id: "...", skus: [...] } }'
|
|
893
|
+
});
|
|
894
|
+
return {
|
|
895
|
+
status: 400,
|
|
896
|
+
error: 'Invalid promotion data',
|
|
897
|
+
recommendation: 'Request must include promotion.id and promotion.skus array'
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
log.info('🔍 Processing promotion', { promotionId, skuCount: skus.length });
|
|
902
|
+
|
|
903
|
+
// ========================================
|
|
904
|
+
// STEP 4: RESERVE INVENTORY
|
|
905
|
+
// ========================================
|
|
906
|
+
|
|
907
|
+
const results = await reservationService.reserveInventory(promotionId, skus, durationMinutes);
|
|
908
|
+
|
|
909
|
+
const successCount = results.filter(r => r.success).length;
|
|
910
|
+
const errorCount = results.filter(r => !r.success).length;
|
|
911
|
+
|
|
912
|
+
// Store promotion metadata in KV
|
|
913
|
+
const promotionStateKey = `promotion:${promotionId}`;
|
|
914
|
+
await kv.set(promotionStateKey, {
|
|
915
|
+
promotionId,
|
|
916
|
+
startedAt: new Date().toISOString(),
|
|
917
|
+
durationMinutes,
|
|
918
|
+
skus,
|
|
919
|
+
reservationResults: results
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
const duration = Date.now() - startTime;
|
|
923
|
+
|
|
924
|
+
log.info('✅ Flash sale inventory reserved', {
|
|
925
|
+
promotionId,
|
|
926
|
+
success: successCount,
|
|
927
|
+
errors: errorCount,
|
|
928
|
+
duration: `${duration}ms`
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
return {
|
|
932
|
+
status: 200,
|
|
933
|
+
promotionId,
|
|
934
|
+
reservations: results,
|
|
935
|
+
summary: {
|
|
936
|
+
success: successCount,
|
|
937
|
+
errors: errorCount
|
|
938
|
+
},
|
|
939
|
+
duration
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
} catch (error: any) {
|
|
943
|
+
const duration = Date.now() - startTime;
|
|
944
|
+
|
|
945
|
+
log.error('❌ Flash sale reservation failed', {
|
|
946
|
+
error: error instanceof Error ? error.message : String(error),
|
|
947
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
948
|
+
duration: `${duration}ms`,
|
|
949
|
+
recommendation: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
950
|
+
? 'Check Fluent Commerce API connectivity and connection configuration'
|
|
951
|
+
: error.message?.includes('authentication') || error.message?.includes('401')
|
|
952
|
+
? 'Verify Fluent Commerce OAuth2 credentials in connection settings'
|
|
953
|
+
: error.message?.includes('retailerId')
|
|
954
|
+
? 'Ensure retailerId is configured in Activation Variables'
|
|
955
|
+
: 'Review error details and check promotion payload format'
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
return {
|
|
959
|
+
status: 500,
|
|
960
|
+
error: error.message,
|
|
961
|
+
duration
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
}));
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
**File:** `src/workflows/webhook/consume-reservation.ts`
|
|
968
|
+
|
|
969
|
+
```typescript
|
|
970
|
+
import { webhook, http } from '@versori/run';
|
|
971
|
+
import {
|
|
972
|
+
createClient,
|
|
973
|
+
VersoriKVAdapter,
|
|
974
|
+
StateService
|
|
975
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
976
|
+
import { ReservationService } from '../../services/reservation-service';
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* Webhook: Consume reservation when order is placed
|
|
980
|
+
*
|
|
981
|
+
* Endpoint: POST https://{workspace}.versori.run/consume-reservation
|
|
982
|
+
* Request body: { order: { ref: "ORD-001", promotionId: "FLASH-001", items: [{ sku: "SKU-001", quantity: 2 }] } }
|
|
983
|
+
*/
|
|
984
|
+
export const consumeReservation = webhook('consume-reservation', {
|
|
985
|
+
response: { mode: 'sync' }
|
|
986
|
+
}).then(http('consume-inventory', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
987
|
+
const { log, openKv, activation, data } = ctx;
|
|
988
|
+
const startTime = Date.now();
|
|
989
|
+
|
|
990
|
+
log.info('🚀 Starting reservation consumption');
|
|
991
|
+
|
|
992
|
+
try {
|
|
993
|
+
// ========================================
|
|
994
|
+
// STEP 1: CLIENT INITIALIZATION
|
|
995
|
+
// ========================================
|
|
996
|
+
|
|
997
|
+
const retailerId = activation?.getVariable('retailerId');
|
|
998
|
+
if (!retailerId) {
|
|
999
|
+
log.error('❌ Configuration error: Missing retailerId', {
|
|
1000
|
+
recommendation: 'Configure retailerId in Activation Variables'
|
|
1001
|
+
});
|
|
1002
|
+
return {
|
|
1003
|
+
status: 500,
|
|
1004
|
+
error: 'Missing retailerId configuration',
|
|
1005
|
+
recommendation: 'Add retailerId to Activation Variables'
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const client = await createClient(ctx);
|
|
1010
|
+
client.setRetailerId(retailerId);
|
|
1011
|
+
log.info('✅ Client initialized', { retailerId });
|
|
1012
|
+
|
|
1013
|
+
// ========================================
|
|
1014
|
+
// STEP 2: SERVICE INITIALIZATION
|
|
1015
|
+
// ========================================
|
|
1016
|
+
|
|
1017
|
+
const kv = new VersoriKVAdapter(openKv(':project:'));
|
|
1018
|
+
const stateService = new StateService(log);
|
|
1019
|
+
const reservationService = new ReservationService(client, stateService, retailerId, log);
|
|
1020
|
+
|
|
1021
|
+
// ========================================
|
|
1022
|
+
// STEP 3: VALIDATE REQUEST PAYLOAD
|
|
1023
|
+
// ========================================
|
|
1024
|
+
|
|
1025
|
+
const order = data?.order || {};
|
|
1026
|
+
const promotionId = order.promotionId;
|
|
1027
|
+
const items = order.items || [];
|
|
1028
|
+
|
|
1029
|
+
if (!promotionId) {
|
|
1030
|
+
log.error('❌ Missing promotion ID in request', {
|
|
1031
|
+
recommendation: 'Request must include order.promotionId'
|
|
1032
|
+
});
|
|
1033
|
+
return {
|
|
1034
|
+
status: 400,
|
|
1035
|
+
error: 'Missing promotion ID',
|
|
1036
|
+
recommendation: 'Include order.promotionId in request payload'
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
log.info('🔍 Consuming reservations', {
|
|
1041
|
+
orderRef: order.ref,
|
|
1042
|
+
promotionId,
|
|
1043
|
+
itemCount: items.length
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
// ========================================
|
|
1047
|
+
// STEP 4: CONSUME RESERVATIONS
|
|
1048
|
+
// ========================================
|
|
1049
|
+
|
|
1050
|
+
const consumeResults: any[] = [];
|
|
1051
|
+
for (const item of items) {
|
|
1052
|
+
const consumed = await reservationService.consumeReservation(
|
|
1053
|
+
promotionId,
|
|
1054
|
+
item.sku,
|
|
1055
|
+
item.quantity
|
|
1056
|
+
);
|
|
1057
|
+
consumeResults.push({
|
|
1058
|
+
sku: item.sku,
|
|
1059
|
+
quantity: item.quantity,
|
|
1060
|
+
consumed
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const successCount = consumeResults.filter(r => r.consumed).length;
|
|
1065
|
+
const duration = Date.now() - startTime;
|
|
1066
|
+
|
|
1067
|
+
log.info('✅ Reservation consumption complete', {
|
|
1068
|
+
orderRef: order.ref,
|
|
1069
|
+
success: successCount,
|
|
1070
|
+
failed: consumeResults.length - successCount,
|
|
1071
|
+
duration: `${duration}ms`
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
return {
|
|
1075
|
+
status: 200,
|
|
1076
|
+
orderRef: order.ref,
|
|
1077
|
+
consumeResults,
|
|
1078
|
+
duration
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
} catch (error: any) {
|
|
1082
|
+
const duration = Date.now() - startTime;
|
|
1083
|
+
|
|
1084
|
+
log.error('❌ Reservation consumption failed', {
|
|
1085
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1086
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1087
|
+
duration: `${duration}ms`,
|
|
1088
|
+
recommendation: error.message?.includes('not found') || error.message?.includes('missing')
|
|
1089
|
+
? 'Verify promotion exists and reservations were created successfully'
|
|
1090
|
+
: error.message?.includes('insufficient') || error.message?.includes('quantity')
|
|
1091
|
+
? 'Check reservation quantities and consumption limits'
|
|
1092
|
+
: 'Review error details and check order payload format'
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
return {
|
|
1096
|
+
status: 500,
|
|
1097
|
+
error: error.message,
|
|
1098
|
+
duration
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
}));
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
**File:** `src/workflows/scheduled/auto-release.ts`
|
|
1105
|
+
|
|
1106
|
+
```typescript
|
|
1107
|
+
import { schedule, http } from '@versori/run';
|
|
1108
|
+
import {
|
|
1109
|
+
createClient,
|
|
1110
|
+
VersoriKVAdapter,
|
|
1111
|
+
StateService
|
|
1112
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
1113
|
+
import { ReservationService } from '../../services/reservation-service';
|
|
1114
|
+
|
|
1115
|
+
/**
|
|
1116
|
+
* Scheduled: Auto-release expired reservations
|
|
1117
|
+
*
|
|
1118
|
+
* Schedule: Every 10 minutes
|
|
1119
|
+
* Purpose: Release expired reservations back to AVAILABLE inventory
|
|
1120
|
+
*/
|
|
1121
|
+
export const autoReleaseReservations = schedule('auto-release', '*/10 * * * *').then(
|
|
1122
|
+
http('release-expired', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
1123
|
+
const { log, openKv, activation } = ctx;
|
|
1124
|
+
const startTime = Date.now();
|
|
1125
|
+
|
|
1126
|
+
log.info('🚀 Starting auto-release of expired reservations');
|
|
1127
|
+
|
|
1128
|
+
try {
|
|
1129
|
+
// ========================================
|
|
1130
|
+
// STEP 1: CHECK IF AUTO-RELEASE ENABLED
|
|
1131
|
+
// ========================================
|
|
1132
|
+
|
|
1133
|
+
const autoReleaseEnabled = activation?.getVariable('autoReleaseEnabled') === 'true';
|
|
1134
|
+
if (!autoReleaseEnabled) {
|
|
1135
|
+
log.info('⚠️ Auto-release disabled');
|
|
1136
|
+
return { status: 200, message: 'Auto-release disabled' };
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// ========================================
|
|
1140
|
+
// STEP 2: CLIENT INITIALIZATION
|
|
1141
|
+
// ========================================
|
|
1142
|
+
|
|
1143
|
+
const retailerId = activation?.getVariable('retailerId');
|
|
1144
|
+
if (!retailerId) {
|
|
1145
|
+
log.error('❌ Configuration error: Missing retailerId', {
|
|
1146
|
+
recommendation: 'Configure retailerId in Activation Variables'
|
|
1147
|
+
});
|
|
1148
|
+
return {
|
|
1149
|
+
status: 500,
|
|
1150
|
+
error: 'Missing retailerId configuration'
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const client = await createClient(ctx);
|
|
1155
|
+
client.setRetailerId(retailerId);
|
|
1156
|
+
log.info('✅ Client initialized', { retailerId });
|
|
1157
|
+
|
|
1158
|
+
// ========================================
|
|
1159
|
+
// STEP 3: SERVICE INITIALIZATION
|
|
1160
|
+
// ========================================
|
|
1161
|
+
|
|
1162
|
+
const kv = new VersoriKVAdapter(openKv(':project:'));
|
|
1163
|
+
const stateService = new StateService(log);
|
|
1164
|
+
const reservationService = new ReservationService(client, stateService, retailerId, log);
|
|
1165
|
+
|
|
1166
|
+
// ========================================
|
|
1167
|
+
// STEP 4: QUERY ACTIVE PROMOTIONS
|
|
1168
|
+
// ========================================
|
|
1169
|
+
|
|
1170
|
+
// In production, maintain a list of active promotions in KV
|
|
1171
|
+
const activePromotions = ['PROMO-001']; // Replace with actual query from KV
|
|
1172
|
+
|
|
1173
|
+
log.info('🔍 Processing active promotions', { count: activePromotions.length });
|
|
1174
|
+
|
|
1175
|
+
// ========================================
|
|
1176
|
+
// STEP 5: RELEASE EXPIRED RESERVATIONS
|
|
1177
|
+
// ========================================
|
|
1178
|
+
|
|
1179
|
+
const releaseResults: any[] = [];
|
|
1180
|
+
for (const promotionId of activePromotions) {
|
|
1181
|
+
try {
|
|
1182
|
+
const result = await reservationService.releaseExpiredReservations(promotionId);
|
|
1183
|
+
releaseResults.push(result);
|
|
1184
|
+
|
|
1185
|
+
if (result.released > 0) {
|
|
1186
|
+
log.info('✅ Expired reservations released', {
|
|
1187
|
+
promotionId,
|
|
1188
|
+
released: result.released
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
} catch (error: any) {
|
|
1192
|
+
log.error('❌ Failed to release reservations', {
|
|
1193
|
+
promotionId,
|
|
1194
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1195
|
+
recommendation: 'Check promotion exists and has expired reservations'
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
const totalReleased = releaseResults.reduce((sum, r) => sum + r.released, 0);
|
|
1201
|
+
const duration = Date.now() - startTime;
|
|
1202
|
+
|
|
1203
|
+
log.info('✅ Auto-release complete', {
|
|
1204
|
+
totalReleased,
|
|
1205
|
+
promotionsProcessed: activePromotions.length,
|
|
1206
|
+
duration: `${duration}ms`
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
return {
|
|
1210
|
+
status: 200,
|
|
1211
|
+
releaseResults,
|
|
1212
|
+
totalReleased,
|
|
1213
|
+
duration
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
} catch (error: any) {
|
|
1217
|
+
const duration = Date.now() - startTime;
|
|
1218
|
+
|
|
1219
|
+
log.error('❌ Auto-release failed', {
|
|
1220
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1221
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1222
|
+
duration: `${duration}ms`,
|
|
1223
|
+
recommendation: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
1224
|
+
? 'Check Fluent Commerce API connectivity'
|
|
1225
|
+
: error.message?.includes('KV') || error.message?.includes('storage')
|
|
1226
|
+
? 'Check KV storage connectivity and promotion data'
|
|
1227
|
+
: 'Review error details and check active promotions list'
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
return {
|
|
1231
|
+
status: 500,
|
|
1232
|
+
error: error.message,
|
|
1233
|
+
duration
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
})
|
|
1237
|
+
);
|
|
1238
|
+
```
|
|
1239
|
+
|
|
1240
|
+
---
|
|
1241
|
+
|
|
1242
|
+
## Testing
|
|
1243
|
+
|
|
1244
|
+
### Start Flash Sale
|
|
1245
|
+
|
|
1246
|
+
```bash
|
|
1247
|
+
curl -X POST https://{workspace}.versori.run/start-flash-sale \
|
|
1248
|
+
-H "Content-Type: application/json" \
|
|
1249
|
+
-d '{
|
|
1250
|
+
"promotion": {
|
|
1251
|
+
"id": "FLASH-2025-01-17",
|
|
1252
|
+
"skus": [
|
|
1253
|
+
{ "sku": "SKU-001", "quantity": 100 },
|
|
1254
|
+
{ "sku": "SKU-002", "quantity": 50 }
|
|
1255
|
+
]
|
|
1256
|
+
}
|
|
1257
|
+
}'
|
|
1258
|
+
```
|
|
1259
|
+
|
|
1260
|
+
Expected response:
|
|
1261
|
+
|
|
1262
|
+
```json
|
|
1263
|
+
{
|
|
1264
|
+
"status": 200,
|
|
1265
|
+
"promotionId": "FLASH-2025-01-17",
|
|
1266
|
+
"reservations": [
|
|
1267
|
+
{
|
|
1268
|
+
"sku": "SKU-001",
|
|
1269
|
+
"success": true,
|
|
1270
|
+
"reservationRef": "FLASH-2025-01-17-SKU-001-1737110400000",
|
|
1271
|
+
"quantity": 100,
|
|
1272
|
+
"expiresAt": "2025-01-17T12:00:00.000Z"
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
"sku": "SKU-002",
|
|
1276
|
+
"success": true,
|
|
1277
|
+
"reservationRef": "FLASH-2025-01-17-SKU-002-1737110400000",
|
|
1278
|
+
"quantity": 50,
|
|
1279
|
+
"expiresAt": "2025-01-17T12:00:00.000Z"
|
|
1280
|
+
}
|
|
1281
|
+
],
|
|
1282
|
+
"summary": {
|
|
1283
|
+
"success": 2,
|
|
1284
|
+
"errors": 0
|
|
1285
|
+
},
|
|
1286
|
+
"duration": 1234
|
|
1287
|
+
}
|
|
1288
|
+
```
|
|
1289
|
+
|
|
1290
|
+
### Consume Reservation
|
|
1291
|
+
|
|
1292
|
+
```bash
|
|
1293
|
+
curl -X POST https://{workspace}.versori.run/consume-reservation \
|
|
1294
|
+
-H "Content-Type: application/json" \
|
|
1295
|
+
-d '{
|
|
1296
|
+
"order": {
|
|
1297
|
+
"ref": "ORD-5001",
|
|
1298
|
+
"promotionId": "FLASH-2025-01-17",
|
|
1299
|
+
"items": [
|
|
1300
|
+
{ "sku": "SKU-001", "quantity": 2 }
|
|
1301
|
+
]
|
|
1302
|
+
}
|
|
1303
|
+
}'
|
|
1304
|
+
```
|
|
1305
|
+
|
|
1306
|
+
---
|
|
1307
|
+
|
|
1308
|
+
## Common Issues & Solutions
|
|
1309
|
+
|
|
1310
|
+
1. **Concurrent reservation failures**
|
|
1311
|
+
- Distributed lock timeout too short
|
|
1312
|
+
- Increase `lockTimeoutMs` or add retry logic
|
|
1313
|
+
2. **Reservations not releasing**
|
|
1314
|
+
- Verify auto-release schedule is running
|
|
1315
|
+
- Check expiration timestamp calculation
|
|
1316
|
+
3. **Overselling during flash sale**
|
|
1317
|
+
- Ensure consume webhook is called before order creation
|
|
1318
|
+
- Verify lock acquisition working correctly
|
|
1319
|
+
4. **Memory issues with large promotions**
|
|
1320
|
+
- Process SKUs in batches
|
|
1321
|
+
- Use streaming for reservation creation
|
|
1322
|
+
|
|
1323
|
+
---
|
|
1324
|
+
|
|
1325
|
+
## Production Checklist
|
|
1326
|
+
|
|
1327
|
+
- [ ] Reservation duration appropriate for sale type (30-120 min)
|
|
1328
|
+
- [ ] Auto-release schedule configured (every 10-15 min)
|
|
1329
|
+
- [ ] Distributed locking working correctly
|
|
1330
|
+
- [ ] Concurrent reservation handling tested
|
|
1331
|
+
- [ ] Consume webhook integrated with order flow
|
|
1332
|
+
- [ ] Reconciliation report generated after sale
|
|
1333
|
+
- [ ] Monitoring for reservation failures
|
|
1334
|
+
- [ ] Rollback plan for incorrect reservations
|
|
1335
|
+
|
|
1336
|
+
---
|
|
1337
|
+
|
|
1338
|
+
## Key Versori Syntax Patterns
|
|
1339
|
+
|
|
1340
|
+
### Webhook Definition
|
|
1341
|
+
```typescript
|
|
1342
|
+
// ✅ CORRECT - v0.1.27
|
|
1343
|
+
export const myWebhook = webhook('webhook-name', {
|
|
1344
|
+
response: {
|
|
1345
|
+
mode: 'sync' | 'async'
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
```
|
|
1349
|
+
|
|
1350
|
+
### Schedule Definition
|
|
1351
|
+
```typescript
|
|
1352
|
+
// ✅ CORRECT - v0.1.27
|
|
1353
|
+
export const mySchedule = schedule('schedule-name', {
|
|
1354
|
+
cron: '*/10 * * * *' // Cron expression
|
|
1355
|
+
});
|
|
1356
|
+
```
|
|
1357
|
+
|
|
1358
|
+
### HTTP Step with Connection
|
|
1359
|
+
```typescript
|
|
1360
|
+
// ✅ CORRECT - v0.1.27
|
|
1361
|
+
.then(http('step-name', { connection: 'connection_name' }, async (ctx) => {
|
|
1362
|
+
const { log, openKv, activation, connections } = ctx;
|
|
1363
|
+
// ...
|
|
1364
|
+
}));
|
|
1365
|
+
```
|
|
1366
|
+
|
|
1367
|
+
### KV Storage Scopes
|
|
1368
|
+
```typescript
|
|
1369
|
+
// ✅ CORRECT - Three scope options
|
|
1370
|
+
ctx.openKv(':project:') // Project-wide scope (shared across all workflows)
|
|
1371
|
+
ctx.openKv(':workflow:') // Workflow-specific scope
|
|
1372
|
+
ctx.openKv(':activation:') // Activation-specific scope (single execution)
|
|
1373
|
+
|
|
1374
|
+
// Most common for reservation tracking: :project:
|
|
1375
|
+
const kv = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
1376
|
+
```
|
|
1377
|
+
|
|
1378
|
+
### StateService Usage
|
|
1379
|
+
```typescript
|
|
1380
|
+
// ✅ CORRECT - StateService doesn't need KV adapter
|
|
1381
|
+
const stateService = new StateService(log);
|
|
1382
|
+
|
|
1383
|
+
// For persistent state, use KV directly:
|
|
1384
|
+
await kv.set(key, value);
|
|
1385
|
+
const value = await kv.get(key);
|
|
1386
|
+
```
|
|
1387
|
+
|
|
1388
|
+
---
|
|
1389
|
+
|
|
1390
|
+
## Related Guides
|
|
1391
|
+
|
|
1392
|
+
- **Versori Platform**: `fc-connect-sdk/docs/04-REFERENCE/platforms/versori/`
|
|
1393
|
+
- **State Management**: `fc-connect-sdk/docs/03-PATTERN-GUIDES/file-operations/`
|
|
1394
|
+
- **Error Handling**: `fc-connect-sdk/docs/03-PATTERN-GUIDES/error-handling/`
|
|
1395
|
+
- **GraphQL Mutations**: `fc-connect-sdk/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/`
|