@fluentcommerce/fc-connect-sdk 0.1.53 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -2
- package/README.md +39 -0
- package/dist/cjs/auth/index.d.ts +3 -0
- package/dist/cjs/auth/index.js +13 -0
- package/dist/cjs/auth/profile-loader.d.ts +18 -0
- package/dist/cjs/auth/profile-loader.js +208 -0
- package/dist/cjs/client-factory.d.ts +4 -0
- package/dist/cjs/client-factory.js +10 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/index.d.ts +3 -1
- package/dist/cjs/index.js +8 -2
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/auth/index.d.ts +3 -0
- package/dist/esm/auth/index.js +2 -0
- package/dist/esm/auth/profile-loader.d.ts +18 -0
- package/dist/esm/auth/profile-loader.js +169 -0
- package/dist/esm/client-factory.d.ts +4 -0
- package/dist/esm/client-factory.js +9 -0
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/utils/pagination-helpers.js +38 -2
- package/dist/esm/versori/fluent-versori-client.js +11 -5
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/dist/types/auth/index.d.ts +3 -0
- package/dist/types/auth/profile-loader.d.ts +18 -0
- package/dist/types/client-factory.d.ts +4 -0
- package/dist/types/index.d.ts +3 -1
- package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
- package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
- package/docs/00-START-HERE/cli-documentation-index.md +202 -202
- package/docs/00-START-HERE/cli-quick-reference.md +252 -252
- package/docs/00-START-HERE/decision-tree.md +552 -552
- package/docs/00-START-HERE/getting-started.md +1070 -1070
- package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
- package/docs/00-START-HERE/readme.md +237 -237
- package/docs/00-START-HERE/retailerid-configuration.md +404 -404
- package/docs/00-START-HERE/sdk-philosophy.md +794 -794
- package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
- package/docs/01-TEMPLATES/faq.md +686 -686
- package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
- package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
- package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
- package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
- package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
- package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
- package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
- package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
- package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
- package/docs/01-TEMPLATES/readme.md +957 -957
- package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
- package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
- package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
- package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
- package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
- package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
- package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
- package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
- package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
- package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
- package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
- package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
- package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
- package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
- package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
- package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
- package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
- package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
- package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -482
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
- package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
- package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
- package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
- package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
- package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
- package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
- package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
- package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
- package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
- package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
- package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
- package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
- package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
- package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
- package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
- package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
- package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
- package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
- package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
- package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
- package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
- package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
- package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
- package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
- package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
- package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
- package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
- package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
- package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
- package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
- package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
- package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
- package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
- package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
- package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
- package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
- package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
- package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
- package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
- package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
- package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
- package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
- package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
- package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
- package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
- package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
- package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
- package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
- package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
- package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
- package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
- package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
- package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
- package/docs/02-CORE-GUIDES/readme.md +194 -194
- package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
- package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
- package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
- package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
- package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
- package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
- package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
- package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
- package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
- package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
- package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
- package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
- package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
- package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
- package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
- package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
- package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
- package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
- package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
- package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
- package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
- package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
- package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
- package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
- package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
- package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
- package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/readme.md +159 -159
- package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
- package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
- package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
- package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
- package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
- package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
- package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
- package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
- package/docs/04-REFERENCE/architecture/readme.md +279 -279
- package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
- package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
- package/docs/04-REFERENCE/platforms/readme.md +135 -135
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
- package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
- package/docs/04-REFERENCE/readme.md +148 -148
- package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
- package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
- package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
- package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
- package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
- package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
- package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
- package/docs/04-REFERENCE/schema/readme.md +141 -141
- package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
- package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
- package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
- package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
- package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
- package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
- package/docs/04-REFERENCE/testing/readme.md +86 -86
- package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
- package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
- package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
- package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
- package/docs/template-loading-matrix.md +242 -242
- package/package.json +5 -3
|
@@ -1,2478 +1,2478 @@
|
|
|
1
|
-
---
|
|
2
|
-
template_id: tpl-webhook-payment-gateway-integration
|
|
3
|
-
canonical_filename: template-webhook-payment-gateway-integration.md
|
|
4
|
-
version: 2.0.0
|
|
5
|
-
sdk_version: ^0.1.39
|
|
6
|
-
runtime: versori
|
|
7
|
-
direction: integration
|
|
8
|
-
source: webhook-json-payment
|
|
9
|
-
destination: fluent-event-api
|
|
10
|
-
entity: payment
|
|
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 - Payment Gateway Integration (Adyen)
|
|
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**: Intercept Fluent Workflow webhooks for payment operations (Capture, Refund, Auth Cancel, ReAuth), process via payment gateway (Adyen), and send events back to Fluent Commerce
|
|
42
|
-
|
|
43
|
-
**Complexity**: Medium-High
|
|
44
|
-
|
|
45
|
-
**Runtime**: Versori Platform
|
|
46
|
-
|
|
47
|
-
**Estimated Lines**: ~1600 lines (modular structure)
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
|
|
51
|
-
## STEP 1: Understand This Template
|
|
52
|
-
|
|
53
|
-
**What This Template Does:**
|
|
54
|
-
|
|
55
|
-
- **4 Versori HTTP webhooks** for payment operations:
|
|
56
|
-
- **Capture** - Capture an authorized payment (triggered by ORDER/FULFILLMENT workflow)
|
|
57
|
-
- **Refund** - Refund a captured payment (triggered by Payment Entity orchestration)
|
|
58
|
-
- **Auth Cancel** - Cancel an authorization (triggered by Payment Entity orchestration)
|
|
59
|
-
- **ReAuth** - Re-authorize a payment (triggered by Payment Entity orchestration)
|
|
60
|
-
|
|
61
|
-
- **Modular architecture** - Easy to swap payment providers (Adyen → Stripe, etc.)
|
|
62
|
-
- **Shared services** - Webhook validation, event sending, idempotency
|
|
63
|
-
- **Payment provider abstraction** - Switch providers by changing one service
|
|
64
|
-
- **Event mapping** - Payment gateway responses → Fluent event format
|
|
65
|
-
- **Idempotency handling** - Prevent duplicate operations
|
|
66
|
-
- **Error handling** - Comprehensive error handling and retry logic
|
|
67
|
-
- **Trigger source validation** - Ensures webhooks called from correct Fluent workflows
|
|
68
|
-
- **Sync + Fire-and-Forget Pattern**: Fast webhook response, background processing
|
|
69
|
-
|
|
70
|
-
**Key SDK Components:**
|
|
71
|
-
|
|
72
|
-
- `createClient()` - Universal client factory (auto-detects Versori context)
|
|
73
|
-
- `client.sendEvent()` - Send events to Fluent Commerce Event API
|
|
74
|
-
- `VersoriKVAdapter` - Idempotency tracking (KV storage)
|
|
75
|
-
- `WebhookValidationService` - Webhook signature validation
|
|
76
|
-
- Native Versori `log` - Use `log` from context
|
|
77
|
-
|
|
78
|
-
**Entity Type:**
|
|
79
|
-
|
|
80
|
-
- **Payment** - Fluent entity for payment operations
|
|
81
|
-
- **Event API** - Uses `sendEvent()` to send payment events back to Fluent
|
|
82
|
-
|
|
83
|
-
**Critical Patterns:**
|
|
84
|
-
|
|
85
|
-
- **Sync + Fire-and-Forget**: Webhook validates quickly, returns immediately, processes payment in background
|
|
86
|
-
- **External JSON Config**: Payment provider configuration in separate JSON file (`config/payment-provider-config.json`)
|
|
87
|
-
- **Modular Architecture**: Separate services, workflows, config, types folders
|
|
88
|
-
- **Background Processing**: Long-running operations (payment gateway API calls, event sending) happen asynchronously
|
|
89
|
-
- **Idempotency**: KV storage prevents duplicate payment operations
|
|
90
|
-
- **Provider Abstraction**: Easy to swap payment providers (Adyen → Stripe)
|
|
91
|
-
|
|
92
|
-
**When to Use This Template:**
|
|
93
|
-
|
|
94
|
-
- ✅ Payment gateway integration (Adyen, Stripe, etc.)
|
|
95
|
-
- ✅ Payment operations triggered by Fluent Rubix workflows
|
|
96
|
-
- ✅ Need fast webhook response (don't wait for payment gateway API calls)
|
|
97
|
-
- ✅ Idempotency required (prevent duplicate charges/refunds)
|
|
98
|
-
- ✅ Multiple payment operations (Capture, Refund, Cancel, ReAuth)
|
|
99
|
-
|
|
100
|
-
**When NOT to Use:**
|
|
101
|
-
|
|
102
|
-
- ❌ Direct payment processing (use payment gateway SDK directly)
|
|
103
|
-
- ❌ Bulk payment operations (use Batch API or scheduled workflows)
|
|
104
|
-
- ❌ Need synchronous payment confirmation (wait for result before responding)
|
|
105
|
-
|
|
106
|
-
---
|
|
107
|
-
|
|
108
|
-
## 🎯 Flow Mapping & Trigger Sources
|
|
109
|
-
|
|
110
|
-
| Payment Operation | Trigger Source | When It Happens | Context |
|
|
111
|
-
|-------------------|----------------|------------------|---------|
|
|
112
|
-
| **Capture** | ORDER/FULFILLMENT Workflow | Order ready to ship | Initial flow - order moves to fulfillment |
|
|
113
|
-
| **Refund** | Payment Entity Orchestration | Post-purchase refund | After RMA processing, order cancellation |
|
|
114
|
-
| **Auth Cancel** | Payment Entity Orchestration | Pre-capture cancellation | Order cancelled before capture |
|
|
115
|
-
| **ReAuth** | Payment Entity Orchestration | Authorization renewal | Authorization expires or needs extension |
|
|
116
|
-
|
|
117
|
-
**Note**: All webhooks are called from Rubix workflows, which handle all business logic validation before calling endpoints.
|
|
118
|
-
|
|
119
|
-
---
|
|
120
|
-
|
|
121
|
-
## 🔑 Key Principle: Rubix Workflow Validation
|
|
122
|
-
|
|
123
|
-
**IMPORTANT**: All webhooks are called from **Rubix workflows** within Fluent Commerce.
|
|
124
|
-
|
|
125
|
-
### What This Means
|
|
126
|
-
|
|
127
|
-
✅ **Rubix workflows handle ALL business logic validation**:
|
|
128
|
-
- Order status checks (is order ready to ship?)
|
|
129
|
-
- Payment state checks (is payment authorized? captured?)
|
|
130
|
-
- Entity existence checks (does order exist? does payment exist?)
|
|
131
|
-
- Business rules (can we refund? can we cancel?)
|
|
132
|
-
|
|
133
|
-
✅ **If Rubix workflow calls an endpoint, the entity is already in the correct state**:
|
|
134
|
-
- If Capture endpoint is called → Order is ready to ship, payment is authorized
|
|
135
|
-
- If Refund endpoint is called → Payment is captured, refund is approved
|
|
136
|
-
- If Cancel endpoint is called → Authorization exists, order is cancelled
|
|
137
|
-
- If ReAuth endpoint is called → Authorization exists, needs renewal
|
|
138
|
-
|
|
139
|
-
### What We Validate (Technical Only)
|
|
140
|
-
|
|
141
|
-
✅ **Payload structure** - Required fields present
|
|
142
|
-
✅ **Trigger source** - Correct Rubix workflow calling endpoint
|
|
143
|
-
✅ **Data format** - Amounts, currencies, references are valid
|
|
144
|
-
✅ **Idempotency** - Prevent duplicate operations
|
|
145
|
-
✅ **Signature** - Webhook authenticity (if configured)
|
|
146
|
-
|
|
147
|
-
### What We DON'T Validate
|
|
148
|
-
|
|
149
|
-
❌ **Entity status** - Rubix workflow ensures correct status
|
|
150
|
-
❌ **Payment state** - Rubix workflow ensures correct state
|
|
151
|
-
❌ **Business rules** - Rubix workflow enforces rules
|
|
152
|
-
❌ **Entity existence** - Rubix workflow verified before calling
|
|
153
|
-
|
|
154
|
-
### Why This Matters
|
|
155
|
-
|
|
156
|
-
- **Simpler code** - No complex business logic in webhook handlers
|
|
157
|
-
- **Single source of truth** - Rubix workflows own business logic
|
|
158
|
-
- **Reliability** - Rubix workflows ensure correct state before calling
|
|
159
|
-
- **Performance** - No redundant GraphQL queries to check entity status
|
|
160
|
-
|
|
161
|
-
---
|
|
162
|
-
|
|
163
|
-
## STEP 2: Implementation Prompt for Claude Code
|
|
164
|
-
|
|
165
|
-
**Copy this prompt and send to Claude Code to generate the complete implementation:**
|
|
166
|
-
|
|
167
|
-
```
|
|
168
|
-
Create Versori webhook workflows for payment gateway integration (Adyen) to Fluent Commerce.
|
|
169
|
-
|
|
170
|
-
REQUIREMENTS:
|
|
171
|
-
1. Runtime: Versori Platform (HTTP webhooks)
|
|
172
|
-
2. Source: Payment operations via HTTP POST webhooks (JSON, from Fluent Rubix workflows)
|
|
173
|
-
3. Destination: Fluent Commerce Event API (sendEvent for payment events)
|
|
174
|
-
4. Format: JSON payment payload
|
|
175
|
-
5. Entity: Payment (Event API for event sending)
|
|
176
|
-
|
|
177
|
-
KEY FEATURES:
|
|
178
|
-
- Sync + fire-and-forget pattern (fast webhook response, background processing)
|
|
179
|
-
- External JSON configuration (config/payment-provider-config.json)
|
|
180
|
-
- Modular architecture (workflows/, services/, config/, types/)
|
|
181
|
-
- 4 webhooks: Capture, Refund, Auth Cancel, ReAuth
|
|
182
|
-
- Payment provider abstraction (easy to swap Adyen → Stripe)
|
|
183
|
-
- Idempotency handling (prevent duplicate operations)
|
|
184
|
-
- Webhook signature validation
|
|
185
|
-
- Trigger source validation (Rubix workflow validation)
|
|
186
|
-
|
|
187
|
-
CRITICAL REQUIREMENTS:
|
|
188
|
-
1. Webhook Mode: response: { mode: 'sync' } (fast response)
|
|
189
|
-
2. Background Processing: Fire-and-forget pattern (no await on long operations)
|
|
190
|
-
3. Provider Config: External JSON file (config/payment-provider-config.json)
|
|
191
|
-
4. Modular Structure: Separate services/, config/, types/ folders
|
|
192
|
-
5. Native Logging: Use log from context (no LoggingService)
|
|
193
|
-
6. Idempotency: VersoriKVAdapter for duplicate prevention
|
|
194
|
-
|
|
195
|
-
SDK METHODS TO USE:
|
|
196
|
-
- createClient({ ...ctx, log }) - Pass full Versori context
|
|
197
|
-
- client.setRetailerId(retailerId) - REQUIRED for Event API
|
|
198
|
-
- client.sendEvent(event) - Send payment events to Fluent
|
|
199
|
-
- new VersoriKVAdapter(openKv(':project:')) - Idempotency tracking
|
|
200
|
-
- new WebhookValidationService(log) - Webhook signature validation
|
|
201
|
-
|
|
202
|
-
FORBIDDEN PATTERNS:
|
|
203
|
-
- ❌ Inline config (use external JSON)
|
|
204
|
-
- ❌ await on background processing (use fire-and-forget)
|
|
205
|
-
- ❌ LoggingService (use native log from context)
|
|
206
|
-
- ❌ All code in one file (use modular structure)
|
|
207
|
-
- ❌ async mode webhook (use sync + fire-and-forget)
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
---
|
|
211
|
-
|
|
212
|
-
## STEP 3: Detailed Flow Documentation
|
|
213
|
-
|
|
214
|
-
### Complete Processing Flow (Payment Capture Example)
|
|
215
|
-
|
|
216
|
-
```
|
|
217
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
218
|
-
│ 1. WEBHOOK RECEIVED (from Rubix Workflow) │
|
|
219
|
-
│ POST https://{workspace}.versori.run/payment-capture │
|
|
220
|
-
│ Content-Type: application/json │
|
|
221
|
-
│ Body: { paymentReference: "...", amount: 100, ... } │
|
|
222
|
-
└────────────────────┬────────────────────────────────────────┘
|
|
223
|
-
│
|
|
224
|
-
▼
|
|
225
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
226
|
-
│ 2. QUICK VALIDATION (Synchronous, ~10-50ms) │
|
|
227
|
-
│ - Check fluent_commerce connection exists │
|
|
228
|
-
│ - Validate payment payload present │
|
|
229
|
-
│ - Validate webhook signature (if configured) │
|
|
230
|
-
│ - Validate trigger source (ORDER_FULFILLMENT) │
|
|
231
|
-
│ - Check idempotency (prevent duplicates) │
|
|
232
|
-
│ - Return HTTP 200 OK immediately │
|
|
233
|
-
└────────────────────┬────────────────────────────────────────┘
|
|
234
|
-
│
|
|
235
|
-
▼
|
|
236
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
237
|
-
│ 3. BACKGROUND PROCESSING (Fire-and-Forget) │
|
|
238
|
-
│ ┌─────────────────────────────────────────────────────┐ │
|
|
239
|
-
│ │ 3a. Initialize Fluent Client │ │
|
|
240
|
-
│ │ - createClient({ ...ctx, log }) │ │
|
|
241
|
-
│ │ - setRetailerId(retailerId) │ │
|
|
242
|
-
│ └─────────────────────────────────────────────────────┘ │
|
|
243
|
-
│ ┌─────────────────────────────────────────────────────┐ │
|
|
244
|
-
│ │ 3b. Call Payment Gateway API │ │
|
|
245
|
-
│ │ - Adyen Capture API call │ │
|
|
246
|
-
│ │ - Handle payment gateway response │ │
|
|
247
|
-
│ └─────────────────────────────────────────────────────┘ │
|
|
248
|
-
│ ┌─────────────────────────────────────────────────────┐ │
|
|
249
|
-
│ │ 3c. Send Event to Fluent │ │
|
|
250
|
-
│ │ - Map gateway response to Fluent event format │ │
|
|
251
|
-
│ │ - client.sendEvent(event) │ │
|
|
252
|
-
│ └─────────────────────────────────────────────────────┘ │
|
|
253
|
-
│ ┌─────────────────────────────────────────────────────┐ │
|
|
254
|
-
│ │ 3d. Update Idempotency Record │ │
|
|
255
|
-
│ │ - Store successful operation in KV │ │
|
|
256
|
-
│ └─────────────────────────────────────────────────────┘ │
|
|
257
|
-
└─────────────────────────────────────────────────────────────┘
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
### Response Timing
|
|
261
|
-
|
|
262
|
-
| Stage | Timing | Blocking |
|
|
263
|
-
|-------|--------|----------|
|
|
264
|
-
| **Webhook Validation** | ~10-50ms | ✅ Yes (blocks response) |
|
|
265
|
-
| **Background Processing** | ~1000-3000ms | ❌ No (fire-and-forget) |
|
|
266
|
-
| **Total Response Time** | ~10-50ms | ✅ Fast response |
|
|
267
|
-
|
|
268
|
-
**Key Benefit**: Rubix workflow receives immediate acknowledgment (~50ms) while payment processing happens in background (~1-3s).
|
|
269
|
-
|
|
270
|
-
---
|
|
271
|
-
|
|
272
|
-
## STEP 4: Production Modular Structure
|
|
273
|
-
|
|
274
|
-
> **✅ This section shows the COMPLETE production-ready modular structure.**
|
|
275
|
-
> All files are shown with proper imports/exports and folder organization.
|
|
276
|
-
|
|
277
|
-
### Complete Project Structure
|
|
278
|
-
|
|
279
|
-
```
|
|
280
|
-
payment-gateway-integration/
|
|
281
|
-
├── package.json # Dependencies and Versori config
|
|
282
|
-
├── index.ts # Entry point - exports all workflows
|
|
283
|
-
└── src/
|
|
284
|
-
├── workflows/
|
|
285
|
-
│ └── webhook/
|
|
286
|
-
│ ├── payment-capture.ts # Webhook: Capture payment
|
|
287
|
-
│ ├── payment-refund.ts # Webhook: Refund payment
|
|
288
|
-
│ ├── payment-auth-cancel.ts # Webhook: Cancel authorization
|
|
289
|
-
│ └── payment-reauth.ts # Webhook: Re-authorize payment
|
|
290
|
-
│
|
|
291
|
-
├── services/
|
|
292
|
-
│ ├── payment-provider.service.ts # Payment provider abstraction
|
|
293
|
-
│ ├── payment-event.service.ts # Event sending to Fluent
|
|
294
|
-
│ ├── idempotency.service.ts # Idempotency handling
|
|
295
|
-
│ └── webhook-validation.service.ts # Webhook signature validation
|
|
296
|
-
│
|
|
297
|
-
├── config/
|
|
298
|
-
│ └── payment-provider-config.json # Provider configuration (external JSON)
|
|
299
|
-
│
|
|
300
|
-
└── types/
|
|
301
|
-
└── payment.types.ts # TypeScript interfaces
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
**Why This Structure?**
|
|
305
|
-
|
|
306
|
-
- ✅ **Clear separation**: Webhook handlers vs business logic vs provider abstraction
|
|
307
|
-
- ✅ **Reusable services**: Payment logic can be reused across webhooks
|
|
308
|
-
- ✅ **External config**: Provider configuration changes don't require code changes
|
|
309
|
-
- ✅ **Provider abstraction**: Easy to swap payment providers (Adyen → Stripe)
|
|
310
|
-
- ✅ **Type safety**: TypeScript interfaces for better IDE support
|
|
311
|
-
- ✅ **Scalable**: Easy to add new payment operations or providers
|
|
312
|
-
|
|
313
|
-
---
|
|
314
|
-
|
|
315
|
-
## SDK Methods Used
|
|
316
|
-
|
|
317
|
-
- `webhook(name, handler)` - HTTP webhook endpoint (from `@versori/run`)
|
|
318
|
-
- `createClient(ctx)` - Auto-detects Versori context, creates FluentClient
|
|
319
|
-
- `client.setRetailerId()` - REQUIRED for Event API (after createClient)
|
|
320
|
-
- `client.sendEvent()` - Send events to Fluent Commerce Event API
|
|
321
|
-
- `VersoriKVAdapter(ctx.openKv(':project:'))` - Idempotency tracking
|
|
322
|
-
- Native Versori log from context (no console.log, no LoggingService)
|
|
323
|
-
|
|
324
|
-
---
|
|
325
|
-
|
|
326
|
-
## Architecture Overview
|
|
327
|
-
|
|
328
|
-
### Modular Design Pattern
|
|
329
|
-
|
|
330
|
-
```
|
|
331
|
-
┌─────────────────────────────────────────────────────────┐
|
|
332
|
-
│ Webhook Layer (4 webhooks) │
|
|
333
|
-
│ - adyen-capture │
|
|
334
|
-
│ - adyen-refund │
|
|
335
|
-
│ - adyen-auth-cancel │
|
|
336
|
-
│ - adyen-reauth │
|
|
337
|
-
└─────────────────────────────────────────────────────────┘
|
|
338
|
-
↓
|
|
339
|
-
┌─────────────────────────────────────────────────────────┐
|
|
340
|
-
│ Shared Services (Reusable) │
|
|
341
|
-
│ - WebhookValidationService │
|
|
342
|
-
│ - PaymentEventService │
|
|
343
|
-
│ - IdempotencyService │
|
|
344
|
-
└─────────────────────────────────────────────────────────┘
|
|
345
|
-
↓
|
|
346
|
-
┌─────────────────────────────────────────────────────────┐
|
|
347
|
-
│ Payment Provider Abstraction │
|
|
348
|
-
│ - PaymentProviderService (Interface) │
|
|
349
|
-
│ - AdyenPaymentProviderService (Implementation) │
|
|
350
|
-
│ - StripePaymentProviderService (Future) │
|
|
351
|
-
└─────────────────────────────────────────────────────────┘
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
### Key Benefits
|
|
355
|
-
|
|
356
|
-
✅ **Single Connector** - All 4 flows in one document/workflow
|
|
357
|
-
✅ **Provider Agnostic** - Easy to swap Adyen → Stripe (change one service)
|
|
358
|
-
✅ **DRY Principle** - Shared services used by all webhooks
|
|
359
|
-
✅ **Maintainable** - Clear separation of concerns
|
|
360
|
-
✅ **Testable** - Each layer can be tested independently
|
|
361
|
-
|
|
362
|
-
---
|
|
363
|
-
|
|
364
|
-
## Versori Workflows Structure
|
|
365
|
-
|
|
366
|
-
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
367
|
-
|
|
368
|
-
**Trigger Types:**
|
|
369
|
-
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
370
|
-
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
371
|
-
- **`http()`** → External API calls (chained from webhook/schedule)
|
|
372
|
-
- **`fn()`** → Internal processing (chained from webhook/schedule)
|
|
373
|
-
|
|
374
|
-
### Recommended Project Structure
|
|
375
|
-
|
|
376
|
-
```
|
|
377
|
-
payment-gateway-integration/
|
|
378
|
-
├── index.ts # Entry point - exports all workflows
|
|
379
|
-
└── src/
|
|
380
|
-
├── workflows/
|
|
381
|
-
│ └── webhook/
|
|
382
|
-
│ ├── payment-capture.ts # Webhook: Capture payment
|
|
383
|
-
│ ├── payment-refund.ts # Webhook: Refund payment
|
|
384
|
-
│ ├── payment-auth-cancel.ts # Webhook: Cancel authorization
|
|
385
|
-
│ └── payment-reauth.ts # Webhook: Re-authorize payment
|
|
386
|
-
│
|
|
387
|
-
├── services/
|
|
388
|
-
│ ├── payment-provider.service.ts # Payment provider abstraction (interface)
|
|
389
|
-
│ ├── adyen-provider.service.ts # Adyen implementation
|
|
390
|
-
│ ├── payment-event.service.ts # Fluent Event API service
|
|
391
|
-
│ ├── webhook-validation.service.ts # Webhook validation & security
|
|
392
|
-
│ └── idempotency.service.ts # Idempotency tracking
|
|
393
|
-
│
|
|
394
|
-
└── config/
|
|
395
|
-
├── adyen-config.json # Adyen-specific configuration
|
|
396
|
-
└── event-mapping.json # Fluent event mapping config
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
---
|
|
400
|
-
|
|
401
|
-
## Project Setup
|
|
402
|
-
|
|
403
|
-
```bash
|
|
404
|
-
mkdir versori-payment-gateway-integration && cd $_
|
|
405
|
-
npm init -y
|
|
406
|
-
npm install @fluentcommerce/fc-connect-sdk@latest @versori/run
|
|
407
|
-
mkdir -p src/{workflows/webhook,services,config}
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
### Package Configuration (package.json)
|
|
411
|
-
|
|
412
|
-
```json
|
|
413
|
-
{
|
|
414
|
-
"name": "versori-payment-gateway-integration",
|
|
415
|
-
"version": "1.0.0",
|
|
416
|
-
"versori": {
|
|
417
|
-
"workflows": "./src/index.ts"
|
|
418
|
-
},
|
|
419
|
-
"type": "module",
|
|
420
|
-
"scripts": {
|
|
421
|
-
"deploy": "versori deploy",
|
|
422
|
-
"logs": "versori logs"
|
|
423
|
-
},
|
|
424
|
-
"dependencies": {
|
|
425
|
-
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
426
|
-
"@versori/run": "latest"
|
|
427
|
-
},
|
|
428
|
-
"devDependencies": {
|
|
429
|
-
"typescript": "^5.0.0",
|
|
430
|
-
"@types/node": "^20.0.0"
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
---
|
|
436
|
-
|
|
437
|
-
## Activation Variables
|
|
438
|
-
|
|
439
|
-
Configure these in Versori Activation Variables:
|
|
440
|
-
|
|
441
|
-
```bash
|
|
442
|
-
# Fluent Commerce Configuration
|
|
443
|
-
fluentRetailerId=your-retailer-id
|
|
444
|
-
|
|
445
|
-
# Payment Provider Configuration
|
|
446
|
-
paymentProvider=adyen # adyen | stripe | paypal (future)
|
|
447
|
-
paymentProviderEnvironment=test # test | live
|
|
448
|
-
|
|
449
|
-
# Adyen-Specific Configuration
|
|
450
|
-
adyenMerchantAccount=YourMerchantAccount
|
|
451
|
-
|
|
452
|
-
# Stripe-Specific Configuration (if using Stripe)
|
|
453
|
-
# stripeSecretKey=sk_test_...
|
|
454
|
-
# stripePublishableKey=pk_test_...
|
|
455
|
-
|
|
456
|
-
# Webhook Security
|
|
457
|
-
webhookSecret=your-shared-secret-for-signature-validation
|
|
458
|
-
|
|
459
|
-
# Trigger Source Validation (Optional but Recommended)
|
|
460
|
-
validateTriggerSource=true
|
|
461
|
-
allowedTriggerSources=ORDER_FULFILLMENT,PAYMENT_ENTITY
|
|
462
|
-
|
|
463
|
-
# Optional: Retry Configuration
|
|
464
|
-
enableRetry=true
|
|
465
|
-
maxRetries=3
|
|
466
|
-
retryDelayMs=1000
|
|
467
|
-
|
|
468
|
-
# Optional: Feature Flags
|
|
469
|
-
enableIdempotency=true
|
|
470
|
-
enableEventLogging=true
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
---
|
|
474
|
-
|
|
475
|
-
## Versori Connections Setup
|
|
476
|
-
|
|
477
|
-
### Connection 1: Fluent Commerce (OAuth2)
|
|
478
|
-
|
|
479
|
-
**Name**: `fluent_commerce`
|
|
480
|
-
|
|
481
|
-
**Type**: OAuth2
|
|
482
|
-
|
|
483
|
-
**Configuration**:
|
|
484
|
-
- `base_url`: Your Fluent Commerce API URL (e.g., `https://api.fluentcommerce.com`)
|
|
485
|
-
- `client_id`: OAuth2 client ID
|
|
486
|
-
- `client_secret`: OAuth2 client secret
|
|
487
|
-
- `username`: Your Fluent Commerce username
|
|
488
|
-
- `password`: Your Fluent Commerce password
|
|
489
|
-
|
|
490
|
-
**Note**: SDK auto-detects and uses this connection via `createClient(ctx)`
|
|
491
|
-
|
|
492
|
-
### Connection 2: Payment Gateway (API Key or OAuth2)
|
|
493
|
-
|
|
494
|
-
**Name**: `payment_gateway` (or `adyen_payment_gateway`, `stripe_payment_gateway`)
|
|
495
|
-
|
|
496
|
-
**Type**: API Key (for Adyen) or OAuth2 (for Stripe)
|
|
497
|
-
|
|
498
|
-
**Configuration**:
|
|
499
|
-
- **Adyen**: `api_key` - Your Adyen API key
|
|
500
|
-
- **Stripe**: `secret_key` - Your Stripe secret key
|
|
501
|
-
|
|
502
|
-
**Alternative**: Store API key in activation variable `paymentGatewayApiKey` if preferred
|
|
503
|
-
|
|
504
|
-
---
|
|
505
|
-
|
|
506
|
-
## Complete Working Code
|
|
507
|
-
|
|
508
|
-
### File: `src/index.ts`
|
|
509
|
-
|
|
510
|
-
```typescript
|
|
511
|
-
/**
|
|
512
|
-
* ═══════════════════════════════════════════════════════════════
|
|
513
|
-
* 🚀 VERSORI PAYMENT GATEWAY INTEGRATION - ENTRY POINT
|
|
514
|
-
* ═══════════════════════════════════════════════════════════════
|
|
515
|
-
*
|
|
516
|
-
* Entry Point - Registers all payment webhooks with Versori platform
|
|
517
|
-
*
|
|
518
|
-
* Pattern: MemoryInterpreter
|
|
519
|
-
* - Export all workflows
|
|
520
|
-
* - Clean separation of concerns
|
|
521
|
-
* - Easy to add new workflows
|
|
522
|
-
*/
|
|
523
|
-
|
|
524
|
-
import { MemoryInterpreter } from '@versori/run';
|
|
525
|
-
import { paymentCapture } from './workflows/webhook/payment-capture';
|
|
526
|
-
import { paymentRefund } from './workflows/webhook/payment-refund';
|
|
527
|
-
import { paymentAuthCancel } from './workflows/webhook/payment-auth-cancel';
|
|
528
|
-
import { paymentReauth } from './workflows/webhook/payment-reauth';
|
|
529
|
-
|
|
530
|
-
async function main(): Promise<void> {
|
|
531
|
-
const mi = await MemoryInterpreter.newInstance();
|
|
532
|
-
|
|
533
|
-
// Register all payment webhook workflows
|
|
534
|
-
mi.register(paymentCapture);
|
|
535
|
-
mi.register(paymentRefund);
|
|
536
|
-
mi.register(paymentAuthCancel);
|
|
537
|
-
mi.register(paymentReauth);
|
|
538
|
-
|
|
539
|
-
await mi.start();
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
main().then().catch((err) => console.error('Failed to run main()', err));
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
---
|
|
546
|
-
|
|
547
|
-
### File: `src/services/payment-provider.service.ts`
|
|
548
|
-
|
|
549
|
-
```typescript
|
|
550
|
-
/**
|
|
551
|
-
* Payment Provider Service Interface
|
|
552
|
-
*
|
|
553
|
-
* Abstract interface for payment gateway operations
|
|
554
|
-
* Allows easy swapping of payment providers (Adyen → Stripe → PayPal)
|
|
555
|
-
*/
|
|
556
|
-
|
|
557
|
-
import type { Logger } from '@fluentcommerce/fc-connect-sdk';
|
|
558
|
-
|
|
559
|
-
export interface PaymentAmount {
|
|
560
|
-
value: number; // Amount in minor units (e.g., 1000 = $10.00)
|
|
561
|
-
currency: string; // ISO 4217 currency code (e.g., "USD")
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
export interface PaymentContext {
|
|
565
|
-
orderRef: string;
|
|
566
|
-
paymentReference: string;
|
|
567
|
-
retailerId?: string;
|
|
568
|
-
merchantReference?: string;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
export interface CaptureResult {
|
|
572
|
-
success: boolean;
|
|
573
|
-
paymentReference: string;
|
|
574
|
-
amount: PaymentAmount;
|
|
575
|
-
capturedAt: string;
|
|
576
|
-
resultCode?: string;
|
|
577
|
-
error?: string;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
export interface RefundResult {
|
|
581
|
-
success: boolean;
|
|
582
|
-
refundReference: string;
|
|
583
|
-
originalPaymentReference: string;
|
|
584
|
-
amount: PaymentAmount;
|
|
585
|
-
refundedAt: string;
|
|
586
|
-
refundType?: 'FULL' | 'PARTIAL';
|
|
587
|
-
error?: string;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
export interface CancelResult {
|
|
591
|
-
success: boolean;
|
|
592
|
-
cancellationReference: string;
|
|
593
|
-
originalPaymentReference: string;
|
|
594
|
-
cancelledAt: string;
|
|
595
|
-
error?: string;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
export interface ReauthResult {
|
|
599
|
-
success: boolean;
|
|
600
|
-
reauthReference: string;
|
|
601
|
-
originalPaymentReference: string;
|
|
602
|
-
amount: PaymentAmount;
|
|
603
|
-
reauthorizedAt: string;
|
|
604
|
-
expiryDate?: string;
|
|
605
|
-
error?: string;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
/**
|
|
609
|
-
* Payment Provider Service Interface
|
|
610
|
-
*
|
|
611
|
-
* Implement this interface for each payment provider:
|
|
612
|
-
* - AdyenPaymentProviderService
|
|
613
|
-
* - StripePaymentProviderService
|
|
614
|
-
* - PayPalPaymentProviderService
|
|
615
|
-
*/
|
|
616
|
-
export interface PaymentProviderService {
|
|
617
|
-
/**
|
|
618
|
-
* Capture an authorized payment
|
|
619
|
-
*/
|
|
620
|
-
capturePayment(
|
|
621
|
-
paymentReference: string,
|
|
622
|
-
amount: PaymentAmount,
|
|
623
|
-
context: PaymentContext
|
|
624
|
-
): Promise<CaptureResult>;
|
|
625
|
-
|
|
626
|
-
/**
|
|
627
|
-
* Refund a captured payment
|
|
628
|
-
*/
|
|
629
|
-
refundPayment(
|
|
630
|
-
paymentReference: string,
|
|
631
|
-
amount: PaymentAmount,
|
|
632
|
-
context: PaymentContext
|
|
633
|
-
): Promise<RefundResult>;
|
|
634
|
-
|
|
635
|
-
/**
|
|
636
|
-
* Cancel an authorization
|
|
637
|
-
*/
|
|
638
|
-
cancelAuthorization(
|
|
639
|
-
paymentReference: string,
|
|
640
|
-
context: PaymentContext
|
|
641
|
-
): Promise<CancelResult>;
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Re-authorize a payment
|
|
645
|
-
*/
|
|
646
|
-
reauthorizePayment(
|
|
647
|
-
paymentReference: string,
|
|
648
|
-
amount: PaymentAmount,
|
|
649
|
-
context: PaymentContext
|
|
650
|
-
): Promise<ReauthResult>;
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* Get provider name (for logging/debugging)
|
|
654
|
-
*/
|
|
655
|
-
getProviderName(): string;
|
|
656
|
-
}
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
---
|
|
660
|
-
|
|
661
|
-
### File: `src/services/adyen-provider.service.ts`
|
|
662
|
-
|
|
663
|
-
```typescript
|
|
664
|
-
/**
|
|
665
|
-
* Adyen Payment Provider Service Implementation
|
|
666
|
-
*
|
|
667
|
-
* Implements PaymentProviderService for Adyen payment gateway
|
|
668
|
-
*/
|
|
669
|
-
|
|
670
|
-
import type { Logger } from '@fluentcommerce/fc-connect-sdk';
|
|
671
|
-
import type {
|
|
672
|
-
PaymentProviderService,
|
|
673
|
-
PaymentAmount,
|
|
674
|
-
PaymentContext,
|
|
675
|
-
CaptureResult,
|
|
676
|
-
RefundResult,
|
|
677
|
-
CancelResult,
|
|
678
|
-
ReauthResult,
|
|
679
|
-
} from './payment-provider.service';
|
|
680
|
-
|
|
681
|
-
export class AdyenPaymentProviderService implements PaymentProviderService {
|
|
682
|
-
constructor(
|
|
683
|
-
private apiKey: string,
|
|
684
|
-
private environment: 'test' | 'live',
|
|
685
|
-
private merchantAccount: string,
|
|
686
|
-
private log: Logger
|
|
687
|
-
) {}
|
|
688
|
-
|
|
689
|
-
getProviderName(): string {
|
|
690
|
-
return 'Adyen';
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
/**
|
|
694
|
-
* Get Adyen API base URL based on environment
|
|
695
|
-
*/
|
|
696
|
-
private getApiUrl(): string {
|
|
697
|
-
return this.environment === 'live'
|
|
698
|
-
? 'https://pal-live.adyen.com/pal/servlet/Payment'
|
|
699
|
-
: 'https://pal-test.adyen.com/pal/servlet/Payment';
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* Capture an authorized payment
|
|
704
|
-
*/
|
|
705
|
-
async capturePayment(
|
|
706
|
-
paymentReference: string,
|
|
707
|
-
amount: PaymentAmount,
|
|
708
|
-
context: PaymentContext
|
|
709
|
-
): Promise<CaptureResult> {
|
|
710
|
-
const url = `${this.getApiUrl()}/v68/payments/${paymentReference}/capture`;
|
|
711
|
-
|
|
712
|
-
this.log.info('🔧 [Adyen] Calling capture API', {
|
|
713
|
-
paymentReference,
|
|
714
|
-
amount: amount.value,
|
|
715
|
-
currency: amount.currency,
|
|
716
|
-
merchantAccount: this.merchantAccount,
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
try {
|
|
720
|
-
const response = await fetch(url, {
|
|
721
|
-
method: 'POST',
|
|
722
|
-
headers: {
|
|
723
|
-
'Content-Type': 'application/json',
|
|
724
|
-
'X-API-Key': this.apiKey,
|
|
725
|
-
},
|
|
726
|
-
body: JSON.stringify({
|
|
727
|
-
amount: amount,
|
|
728
|
-
merchantAccount: this.merchantAccount,
|
|
729
|
-
}),
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
const responseData = await response.json();
|
|
733
|
-
|
|
734
|
-
if (!response.ok) {
|
|
735
|
-
const error = responseData.error || {
|
|
736
|
-
errorCode: 'UNKNOWN_ERROR',
|
|
737
|
-
errorType: 'API_ERROR',
|
|
738
|
-
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
739
|
-
};
|
|
740
|
-
|
|
741
|
-
this.log.error('❌ [Adyen] Capture API error', {
|
|
742
|
-
paymentReference,
|
|
743
|
-
errorCode: error.errorCode,
|
|
744
|
-
message: error.message,
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
return {
|
|
748
|
-
success: false,
|
|
749
|
-
paymentReference: paymentReference,
|
|
750
|
-
amount: amount,
|
|
751
|
-
capturedAt: new Date().toISOString(),
|
|
752
|
-
error: `${error.errorCode}: ${error.message}`,
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
this.log.info('✅ [Adyen] Capture successful', {
|
|
757
|
-
pspReference: responseData.pspReference,
|
|
758
|
-
resultCode: responseData.resultCode,
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
return {
|
|
762
|
-
success: true,
|
|
763
|
-
paymentReference: responseData.pspReference,
|
|
764
|
-
amount: responseData.amount || amount,
|
|
765
|
-
capturedAt: responseData.eventDate || new Date().toISOString(),
|
|
766
|
-
resultCode: responseData.resultCode,
|
|
767
|
-
};
|
|
768
|
-
} catch (error: any) {
|
|
769
|
-
this.log.error('❌ [Adyen] Capture API call failed', {
|
|
770
|
-
paymentReference,
|
|
771
|
-
error: error.message,
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
return {
|
|
775
|
-
success: false,
|
|
776
|
-
paymentReference: paymentReference,
|
|
777
|
-
amount: amount,
|
|
778
|
-
capturedAt: new Date().toISOString(),
|
|
779
|
-
error: error.message,
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
/**
|
|
785
|
-
* Refund a captured payment
|
|
786
|
-
*/
|
|
787
|
-
async refundPayment(
|
|
788
|
-
paymentReference: string,
|
|
789
|
-
amount: PaymentAmount,
|
|
790
|
-
context: PaymentContext
|
|
791
|
-
): Promise<RefundResult> {
|
|
792
|
-
const url = `${this.getApiUrl()}/v68/payments/${paymentReference}/refunds`;
|
|
793
|
-
|
|
794
|
-
this.log.info('🔧 [Adyen] Calling refund API', {
|
|
795
|
-
paymentReference,
|
|
796
|
-
amount: amount.value,
|
|
797
|
-
currency: amount.currency,
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
try {
|
|
801
|
-
const response = await fetch(url, {
|
|
802
|
-
method: 'POST',
|
|
803
|
-
headers: {
|
|
804
|
-
'Content-Type': 'application/json',
|
|
805
|
-
'X-API-Key': this.apiKey,
|
|
806
|
-
},
|
|
807
|
-
body: JSON.stringify({
|
|
808
|
-
amount: amount,
|
|
809
|
-
merchantAccount: this.merchantAccount,
|
|
810
|
-
reference: context.merchantReference || context.orderRef,
|
|
811
|
-
}),
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
const responseData = await response.json();
|
|
815
|
-
|
|
816
|
-
if (!response.ok) {
|
|
817
|
-
const error = responseData.error || {
|
|
818
|
-
errorCode: 'UNKNOWN_ERROR',
|
|
819
|
-
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
820
|
-
};
|
|
821
|
-
|
|
822
|
-
return {
|
|
823
|
-
success: false,
|
|
824
|
-
refundReference: '',
|
|
825
|
-
originalPaymentReference: paymentReference,
|
|
826
|
-
amount: amount,
|
|
827
|
-
refundedAt: new Date().toISOString(),
|
|
828
|
-
error: `${error.errorCode}: ${error.message}`,
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// Determine refund type (full vs partial)
|
|
833
|
-
const refundType = responseData.refundType || 'FULL';
|
|
834
|
-
|
|
835
|
-
return {
|
|
836
|
-
success: true,
|
|
837
|
-
refundReference: responseData.pspReference,
|
|
838
|
-
originalPaymentReference: paymentReference,
|
|
839
|
-
amount: responseData.amount || amount,
|
|
840
|
-
refundedAt: responseData.eventDate || new Date().toISOString(),
|
|
841
|
-
refundType: refundType as 'FULL' | 'PARTIAL',
|
|
842
|
-
};
|
|
843
|
-
} catch (error: any) {
|
|
844
|
-
return {
|
|
845
|
-
success: false,
|
|
846
|
-
refundReference: '',
|
|
847
|
-
originalPaymentReference: paymentReference,
|
|
848
|
-
amount: amount,
|
|
849
|
-
refundedAt: new Date().toISOString(),
|
|
850
|
-
error: error.message,
|
|
851
|
-
};
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
/**
|
|
856
|
-
* Cancel an authorization
|
|
857
|
-
*/
|
|
858
|
-
async cancelAuthorization(
|
|
859
|
-
paymentReference: string,
|
|
860
|
-
context: PaymentContext
|
|
861
|
-
): Promise<CancelResult> {
|
|
862
|
-
const url = `${this.getApiUrl()}/v68/payments/${paymentReference}/cancels`;
|
|
863
|
-
|
|
864
|
-
this.log.info('🔧 [Adyen] Calling cancel API', {
|
|
865
|
-
paymentReference,
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
try {
|
|
869
|
-
const response = await fetch(url, {
|
|
870
|
-
method: 'POST',
|
|
871
|
-
headers: {
|
|
872
|
-
'Content-Type': 'application/json',
|
|
873
|
-
'X-API-Key': this.apiKey,
|
|
874
|
-
},
|
|
875
|
-
body: JSON.stringify({
|
|
876
|
-
merchantAccount: this.merchantAccount,
|
|
877
|
-
reference: context.merchantReference || context.orderRef,
|
|
878
|
-
}),
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
const responseData = await response.json();
|
|
882
|
-
|
|
883
|
-
if (!response.ok) {
|
|
884
|
-
const error = responseData.error || {
|
|
885
|
-
errorCode: 'UNKNOWN_ERROR',
|
|
886
|
-
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
887
|
-
};
|
|
888
|
-
|
|
889
|
-
return {
|
|
890
|
-
success: false,
|
|
891
|
-
cancellationReference: '',
|
|
892
|
-
originalPaymentReference: paymentReference,
|
|
893
|
-
cancelledAt: new Date().toISOString(),
|
|
894
|
-
error: `${error.errorCode}: ${error.message}`,
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
return {
|
|
899
|
-
success: true,
|
|
900
|
-
cancellationReference: responseData.pspReference,
|
|
901
|
-
originalPaymentReference: paymentReference,
|
|
902
|
-
cancelledAt: responseData.eventDate || new Date().toISOString(),
|
|
903
|
-
};
|
|
904
|
-
} catch (error: any) {
|
|
905
|
-
return {
|
|
906
|
-
success: false,
|
|
907
|
-
cancellationReference: '',
|
|
908
|
-
originalPaymentReference: paymentReference,
|
|
909
|
-
cancelledAt: new Date().toISOString(),
|
|
910
|
-
error: error.message,
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
/**
|
|
916
|
-
* Re-authorize a payment
|
|
917
|
-
*/
|
|
918
|
-
async reauthorizePayment(
|
|
919
|
-
paymentReference: string,
|
|
920
|
-
amount: PaymentAmount,
|
|
921
|
-
context: PaymentContext
|
|
922
|
-
): Promise<ReauthResult> {
|
|
923
|
-
const url = `${this.getApiUrl()}/v68/payments/${paymentReference}/reauthorise`;
|
|
924
|
-
|
|
925
|
-
this.log.info('🔧 [Adyen] Calling reauthorize API', {
|
|
926
|
-
paymentReference,
|
|
927
|
-
amount: amount.value,
|
|
928
|
-
currency: amount.currency,
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
try {
|
|
932
|
-
const response = await fetch(url, {
|
|
933
|
-
method: 'POST',
|
|
934
|
-
headers: {
|
|
935
|
-
'Content-Type': 'application/json',
|
|
936
|
-
'X-API-Key': this.apiKey,
|
|
937
|
-
},
|
|
938
|
-
body: JSON.stringify({
|
|
939
|
-
amount: amount,
|
|
940
|
-
merchantAccount: this.merchantAccount,
|
|
941
|
-
reference: context.merchantReference || context.orderRef,
|
|
942
|
-
}),
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
const responseData = await response.json();
|
|
946
|
-
|
|
947
|
-
if (!response.ok) {
|
|
948
|
-
const error = responseData.error || {
|
|
949
|
-
errorCode: 'UNKNOWN_ERROR',
|
|
950
|
-
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
951
|
-
};
|
|
952
|
-
|
|
953
|
-
return {
|
|
954
|
-
success: false,
|
|
955
|
-
reauthReference: '',
|
|
956
|
-
originalPaymentReference: paymentReference,
|
|
957
|
-
amount: amount,
|
|
958
|
-
reauthorizedAt: new Date().toISOString(),
|
|
959
|
-
error: `${error.errorCode}: ${error.message}`,
|
|
960
|
-
};
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
return {
|
|
964
|
-
success: true,
|
|
965
|
-
reauthReference: responseData.pspReference,
|
|
966
|
-
originalPaymentReference: paymentReference,
|
|
967
|
-
amount: responseData.amount || amount,
|
|
968
|
-
reauthorizedAt: responseData.eventDate || new Date().toISOString(),
|
|
969
|
-
expiryDate: responseData.expiryDate,
|
|
970
|
-
};
|
|
971
|
-
} catch (error: any) {
|
|
972
|
-
return {
|
|
973
|
-
success: false,
|
|
974
|
-
reauthReference: '',
|
|
975
|
-
originalPaymentReference: paymentReference,
|
|
976
|
-
amount: amount,
|
|
977
|
-
reauthorizedAt: new Date().toISOString(),
|
|
978
|
-
error: error.message,
|
|
979
|
-
};
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
```
|
|
984
|
-
|
|
985
|
-
---
|
|
986
|
-
|
|
987
|
-
### File: `src/services/webhook-validation.service.ts`
|
|
988
|
-
|
|
989
|
-
```typescript
|
|
990
|
-
/**
|
|
991
|
-
* Webhook Validation Service
|
|
992
|
-
*
|
|
993
|
-
* Validates incoming webhooks from Fluent Workflow
|
|
994
|
-
* - Signature validation (HMAC-SHA256)
|
|
995
|
-
* - Payload structure validation
|
|
996
|
-
* - Required field checking
|
|
997
|
-
*/
|
|
998
|
-
|
|
999
|
-
import { createHmac } from 'node:crypto';
|
|
1000
|
-
import type { Logger } from '@fluentcommerce/fc-connect-sdk';
|
|
1001
|
-
|
|
1002
|
-
export interface WebhookPayload {
|
|
1003
|
-
orderRef: string;
|
|
1004
|
-
paymentReference: string;
|
|
1005
|
-
amount: {
|
|
1006
|
-
value: number;
|
|
1007
|
-
currency: string;
|
|
1008
|
-
};
|
|
1009
|
-
retailerId?: string;
|
|
1010
|
-
merchantReference?: string;
|
|
1011
|
-
triggerSource?: string; // ORDER_FULFILLMENT | PAYMENT_ENTITY
|
|
1012
|
-
orderStatus?: string; // For ORDER_FULFILLMENT triggers
|
|
1013
|
-
refundReason?: string; // For Payment Entity refunds
|
|
1014
|
-
cancelReason?: string; // For Payment Entity cancellations
|
|
1015
|
-
reauthReason?: string; // For Payment Entity reauthorizations
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
export interface ValidationResult {
|
|
1019
|
-
valid: boolean;
|
|
1020
|
-
error?: string;
|
|
1021
|
-
payload?: WebhookPayload;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
export class WebhookValidationService {
|
|
1025
|
-
constructor(private log: Logger) {}
|
|
1026
|
-
|
|
1027
|
-
/**
|
|
1028
|
-
* Validate webhook signature using HMAC-SHA256
|
|
1029
|
-
*/
|
|
1030
|
-
validateSignature(
|
|
1031
|
-
payload: string,
|
|
1032
|
-
signature: string,
|
|
1033
|
-
secret: string
|
|
1034
|
-
): boolean {
|
|
1035
|
-
if (!signature || !secret) {
|
|
1036
|
-
return false;
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
const hmac = createHmac('sha256', secret);
|
|
1040
|
-
hmac.update(payload);
|
|
1041
|
-
const expectedSignature = hmac.digest('hex');
|
|
1042
|
-
|
|
1043
|
-
// Constant-time comparison to prevent timing attacks
|
|
1044
|
-
if (signature.length !== expectedSignature.length) {
|
|
1045
|
-
return false;
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
let match = true;
|
|
1049
|
-
for (let i = 0; i < signature.length; i++) {
|
|
1050
|
-
if (signature[i] !== expectedSignature[i]) {
|
|
1051
|
-
match = false;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
return match;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
/**
|
|
1059
|
-
* Validate webhook payload structure
|
|
1060
|
-
*/
|
|
1061
|
-
validatePayload(payload: any): ValidationResult {
|
|
1062
|
-
const requiredFields = ['orderRef', 'paymentReference', 'amount'];
|
|
1063
|
-
const missingFields = requiredFields.filter((field) => !payload[field]);
|
|
1064
|
-
|
|
1065
|
-
if (missingFields.length > 0) {
|
|
1066
|
-
return {
|
|
1067
|
-
valid: false,
|
|
1068
|
-
error: `Missing required fields: ${missingFields.join(', ')}`,
|
|
1069
|
-
};
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
if (!payload.amount || typeof payload.amount.value !== 'number') {
|
|
1073
|
-
return {
|
|
1074
|
-
valid: false,
|
|
1075
|
-
error: 'Invalid amount structure: amount.value must be a number',
|
|
1076
|
-
};
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
if (!payload.amount.currency || typeof payload.amount.currency !== 'string') {
|
|
1080
|
-
return {
|
|
1081
|
-
valid: false,
|
|
1082
|
-
error: 'Invalid amount structure: amount.currency must be a string',
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
if (payload.amount.value <= 0) {
|
|
1087
|
-
return {
|
|
1088
|
-
valid: false,
|
|
1089
|
-
error: 'Invalid amount: value must be positive',
|
|
1090
|
-
};
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
if (typeof payload.paymentReference !== 'string' || payload.paymentReference.length === 0) {
|
|
1094
|
-
return {
|
|
1095
|
-
valid: false,
|
|
1096
|
-
error: 'Invalid paymentReference: must be a non-empty string',
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
return {
|
|
1101
|
-
valid: true,
|
|
1102
|
-
payload: {
|
|
1103
|
-
orderRef: payload.orderRef,
|
|
1104
|
-
paymentReference: payload.paymentReference,
|
|
1105
|
-
amount: {
|
|
1106
|
-
value: payload.amount.value,
|
|
1107
|
-
currency: payload.amount.currency,
|
|
1108
|
-
},
|
|
1109
|
-
retailerId: payload.retailerId,
|
|
1110
|
-
merchantReference: payload.merchantReference,
|
|
1111
|
-
},
|
|
1112
|
-
};
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
/**
|
|
1116
|
-
* Extract payment context from validated payload
|
|
1117
|
-
*/
|
|
1118
|
-
extractPaymentContext(payload: WebhookPayload) {
|
|
1119
|
-
return {
|
|
1120
|
-
orderRef: payload.orderRef,
|
|
1121
|
-
paymentReference: payload.paymentReference,
|
|
1122
|
-
amount: payload.amount,
|
|
1123
|
-
retailerId: payload.retailerId,
|
|
1124
|
-
merchantReference: payload.merchantReference || payload.orderRef,
|
|
1125
|
-
triggerSource: payload.triggerSource,
|
|
1126
|
-
orderStatus: payload.orderStatus,
|
|
1127
|
-
refundReason: payload.refundReason,
|
|
1128
|
-
cancelReason: payload.cancelReason,
|
|
1129
|
-
reauthReason: payload.reauthReason,
|
|
1130
|
-
};
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
/**
|
|
1134
|
-
* Validate trigger source (ensures webhook called from correct workflow)
|
|
1135
|
-
*/
|
|
1136
|
-
validateTriggerSource(
|
|
1137
|
-
payload: any,
|
|
1138
|
-
allowedSources: string[],
|
|
1139
|
-
requiredSource?: string
|
|
1140
|
-
): ValidationResult {
|
|
1141
|
-
const source = payload.triggerSource;
|
|
1142
|
-
|
|
1143
|
-
if (!source) {
|
|
1144
|
-
return {
|
|
1145
|
-
valid: false,
|
|
1146
|
-
error: 'Missing triggerSource in payload',
|
|
1147
|
-
};
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
if (requiredSource && source !== requiredSource) {
|
|
1151
|
-
return {
|
|
1152
|
-
valid: false,
|
|
1153
|
-
error: `Invalid trigger source: expected ${requiredSource}, got ${source}`,
|
|
1154
|
-
};
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
if (!allowedSources.includes(source)) {
|
|
1158
|
-
return {
|
|
1159
|
-
valid: false,
|
|
1160
|
-
error: `Trigger source not allowed: ${source}. Allowed: ${allowedSources.join(', ')}`,
|
|
1161
|
-
};
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
return {
|
|
1165
|
-
valid: true,
|
|
1166
|
-
};
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
```
|
|
1170
|
-
|
|
1171
|
-
---
|
|
1172
|
-
|
|
1173
|
-
### File: `src/services/idempotency.service.ts`
|
|
1174
|
-
|
|
1175
|
-
```typescript
|
|
1176
|
-
/**
|
|
1177
|
-
* Idempotency Service
|
|
1178
|
-
*
|
|
1179
|
-
* Tracks processed payments to prevent duplicate operations
|
|
1180
|
-
*/
|
|
1181
|
-
|
|
1182
|
-
import { VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
1183
|
-
import type { Logger } from '@fluentcommerce/fc-connect-sdk';
|
|
1184
|
-
|
|
1185
|
-
export interface IdempotencyRecord {
|
|
1186
|
-
operationType: 'capture' | 'refund' | 'cancel' | 'reauth';
|
|
1187
|
-
paymentReference: string;
|
|
1188
|
-
processedAt: string;
|
|
1189
|
-
resultReference: string;
|
|
1190
|
-
amount?: number;
|
|
1191
|
-
currency?: string;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
export class IdempotencyService {
|
|
1195
|
-
constructor(
|
|
1196
|
-
private kvAdapter: VersoriKVAdapter,
|
|
1197
|
-
private log: Logger
|
|
1198
|
-
) {}
|
|
1199
|
-
|
|
1200
|
-
/**
|
|
1201
|
-
* Generate idempotency key
|
|
1202
|
-
*/
|
|
1203
|
-
private getIdempotencyKey(
|
|
1204
|
-
operationType: string,
|
|
1205
|
-
paymentReference: string
|
|
1206
|
-
): string[] {
|
|
1207
|
-
return [`payment:${operationType}:${paymentReference}`];
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
/**
|
|
1211
|
-
* Check if operation was already processed
|
|
1212
|
-
*/
|
|
1213
|
-
async checkAndStore(
|
|
1214
|
-
operationType: 'capture' | 'refund' | 'cancel' | 'reauth',
|
|
1215
|
-
paymentReference: string,
|
|
1216
|
-
resultReference: string,
|
|
1217
|
-
amount?: { value: number; currency: string }
|
|
1218
|
-
): Promise<{ alreadyProcessed: boolean; existingRecord?: IdempotencyRecord }> {
|
|
1219
|
-
const key = this.getIdempotencyKey(operationType, paymentReference);
|
|
1220
|
-
const existing = await this.kvAdapter.get(key);
|
|
1221
|
-
|
|
1222
|
-
if (existing?.value) {
|
|
1223
|
-
const record = typeof existing.value === 'string'
|
|
1224
|
-
? JSON.parse(existing.value)
|
|
1225
|
-
: existing.value;
|
|
1226
|
-
|
|
1227
|
-
this.log.info('✅ [Idempotency] Operation already processed', {
|
|
1228
|
-
operationType,
|
|
1229
|
-
paymentReference,
|
|
1230
|
-
processedAt: record.processedAt,
|
|
1231
|
-
});
|
|
1232
|
-
|
|
1233
|
-
return {
|
|
1234
|
-
alreadyProcessed: true,
|
|
1235
|
-
existingRecord: record,
|
|
1236
|
-
};
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
// Store new record
|
|
1240
|
-
const record: IdempotencyRecord = {
|
|
1241
|
-
operationType,
|
|
1242
|
-
paymentReference,
|
|
1243
|
-
processedAt: new Date().toISOString(),
|
|
1244
|
-
resultReference,
|
|
1245
|
-
amount: amount?.value,
|
|
1246
|
-
currency: amount?.currency,
|
|
1247
|
-
};
|
|
1248
|
-
|
|
1249
|
-
await this.kvAdapter.set(key, JSON.stringify(record));
|
|
1250
|
-
|
|
1251
|
-
this.log.info('✅ [Idempotency] Operation recorded', {
|
|
1252
|
-
operationType,
|
|
1253
|
-
paymentReference,
|
|
1254
|
-
resultReference,
|
|
1255
|
-
});
|
|
1256
|
-
|
|
1257
|
-
return {
|
|
1258
|
-
alreadyProcessed: false,
|
|
1259
|
-
};
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
/**
|
|
1263
|
-
* Get existing record (without storing)
|
|
1264
|
-
*/
|
|
1265
|
-
async getExisting(
|
|
1266
|
-
operationType: 'capture' | 'refund' | 'cancel' | 'reauth',
|
|
1267
|
-
paymentReference: string
|
|
1268
|
-
): Promise<IdempotencyRecord | null> {
|
|
1269
|
-
const key = this.getIdempotencyKey(operationType, paymentReference);
|
|
1270
|
-
const existing = await this.kvAdapter.get(key);
|
|
1271
|
-
|
|
1272
|
-
if (existing?.value) {
|
|
1273
|
-
return typeof existing.value === 'string'
|
|
1274
|
-
? JSON.parse(existing.value)
|
|
1275
|
-
: existing.value;
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
return null;
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
```
|
|
1282
|
-
|
|
1283
|
-
---
|
|
1284
|
-
|
|
1285
|
-
### File: `src/services/payment-event.service.ts`
|
|
1286
|
-
|
|
1287
|
-
```typescript
|
|
1288
|
-
/**
|
|
1289
|
-
* Payment Event Service
|
|
1290
|
-
*
|
|
1291
|
-
* Maps payment provider responses to Fluent event format and sends events
|
|
1292
|
-
*/
|
|
1293
|
-
|
|
1294
|
-
import type { FluentClient, Logger } from '@fluentcommerce/fc-connect-sdk';
|
|
1295
|
-
import type {
|
|
1296
|
-
CaptureResult,
|
|
1297
|
-
RefundResult,
|
|
1298
|
-
CancelResult,
|
|
1299
|
-
ReauthResult,
|
|
1300
|
-
PaymentContext,
|
|
1301
|
-
} from './payment-provider.service';
|
|
1302
|
-
|
|
1303
|
-
export class PaymentEventService {
|
|
1304
|
-
constructor(
|
|
1305
|
-
private client: FluentClient,
|
|
1306
|
-
private log: Logger
|
|
1307
|
-
) {}
|
|
1308
|
-
|
|
1309
|
-
/**
|
|
1310
|
-
* Send payment capture event to Fluent Commerce
|
|
1311
|
-
*/
|
|
1312
|
-
async sendCaptureEvent(
|
|
1313
|
-
result: CaptureResult,
|
|
1314
|
-
context: PaymentContext
|
|
1315
|
-
): Promise<void> {
|
|
1316
|
-
this.log.info('📤 [Event] Sending payment capture event', {
|
|
1317
|
-
orderRef: context.orderRef,
|
|
1318
|
-
paymentReference: result.paymentReference,
|
|
1319
|
-
});
|
|
1320
|
-
|
|
1321
|
-
const eventPayload = {
|
|
1322
|
-
name: 'PaymentCaptured',
|
|
1323
|
-
entityType: 'ORDER',
|
|
1324
|
-
entityRef: context.orderRef,
|
|
1325
|
-
data: {
|
|
1326
|
-
paymentReference: result.paymentReference,
|
|
1327
|
-
originalPaymentReference: context.paymentReference,
|
|
1328
|
-
amount: result.amount.value,
|
|
1329
|
-
currency: result.amount.currency,
|
|
1330
|
-
capturedAt: result.capturedAt,
|
|
1331
|
-
resultCode: result.resultCode,
|
|
1332
|
-
merchantReference: context.merchantReference,
|
|
1333
|
-
},
|
|
1334
|
-
};
|
|
1335
|
-
|
|
1336
|
-
await this.client.sendEvent(eventPayload);
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
/**
|
|
1340
|
-
* Send payment refund event to Fluent Commerce
|
|
1341
|
-
*/
|
|
1342
|
-
async sendRefundEvent(
|
|
1343
|
-
result: RefundResult,
|
|
1344
|
-
context: PaymentContext
|
|
1345
|
-
): Promise<void> {
|
|
1346
|
-
this.log.info('📤 [Event] Sending payment refund event', {
|
|
1347
|
-
orderRef: context.orderRef,
|
|
1348
|
-
refundReference: result.refundReference,
|
|
1349
|
-
});
|
|
1350
|
-
|
|
1351
|
-
const eventPayload = {
|
|
1352
|
-
name: 'PaymentRefunded',
|
|
1353
|
-
entityType: 'ORDER',
|
|
1354
|
-
entityRef: context.orderRef,
|
|
1355
|
-
data: {
|
|
1356
|
-
refundReference: result.refundReference,
|
|
1357
|
-
originalPaymentReference: result.originalPaymentReference,
|
|
1358
|
-
amount: result.amount.value,
|
|
1359
|
-
currency: result.amount.currency,
|
|
1360
|
-
refundType: result.refundType || 'FULL',
|
|
1361
|
-
refundedAt: result.refundedAt,
|
|
1362
|
-
merchantReference: context.merchantReference,
|
|
1363
|
-
},
|
|
1364
|
-
};
|
|
1365
|
-
|
|
1366
|
-
await this.client.sendEvent(eventPayload);
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
/**
|
|
1370
|
-
* Send payment cancellation event to Fluent Commerce
|
|
1371
|
-
*/
|
|
1372
|
-
async sendCancelEvent(
|
|
1373
|
-
result: CancelResult,
|
|
1374
|
-
context: PaymentContext
|
|
1375
|
-
): Promise<void> {
|
|
1376
|
-
this.log.info('📤 [Event] Sending payment cancellation event', {
|
|
1377
|
-
orderRef: context.orderRef,
|
|
1378
|
-
cancellationReference: result.cancellationReference,
|
|
1379
|
-
});
|
|
1380
|
-
|
|
1381
|
-
const eventPayload = {
|
|
1382
|
-
name: 'PaymentAuthorizationCancelled',
|
|
1383
|
-
entityType: 'ORDER',
|
|
1384
|
-
entityRef: context.orderRef,
|
|
1385
|
-
data: {
|
|
1386
|
-
cancellationReference: result.cancellationReference,
|
|
1387
|
-
originalPaymentReference: result.originalPaymentReference,
|
|
1388
|
-
cancelledAt: result.cancelledAt,
|
|
1389
|
-
merchantReference: context.merchantReference,
|
|
1390
|
-
},
|
|
1391
|
-
};
|
|
1392
|
-
|
|
1393
|
-
await this.client.sendEvent(eventPayload);
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
/**
|
|
1397
|
-
* Send payment reauthorization event to Fluent Commerce
|
|
1398
|
-
*/
|
|
1399
|
-
async sendReauthEvent(
|
|
1400
|
-
result: ReauthResult,
|
|
1401
|
-
context: PaymentContext
|
|
1402
|
-
): Promise<void> {
|
|
1403
|
-
this.log.info('📤 [Event] Sending payment reauthorization event', {
|
|
1404
|
-
orderRef: context.orderRef,
|
|
1405
|
-
reauthReference: result.reauthReference,
|
|
1406
|
-
});
|
|
1407
|
-
|
|
1408
|
-
const eventPayload = {
|
|
1409
|
-
name: 'PaymentReauthorized',
|
|
1410
|
-
entityType: 'ORDER',
|
|
1411
|
-
entityRef: context.orderRef,
|
|
1412
|
-
data: {
|
|
1413
|
-
reauthReference: result.reauthReference,
|
|
1414
|
-
originalPaymentReference: result.originalPaymentReference,
|
|
1415
|
-
amount: result.amount.value,
|
|
1416
|
-
currency: result.amount.currency,
|
|
1417
|
-
reauthorizedAt: result.reauthorizedAt,
|
|
1418
|
-
expiryDate: result.expiryDate,
|
|
1419
|
-
merchantReference: context.merchantReference,
|
|
1420
|
-
},
|
|
1421
|
-
};
|
|
1422
|
-
|
|
1423
|
-
await this.client.sendEvent(eventPayload);
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
```
|
|
1427
|
-
|
|
1428
|
-
---
|
|
1429
|
-
|
|
1430
|
-
### File: `src/services/payment-provider-factory.service.ts`
|
|
1431
|
-
|
|
1432
|
-
```typescript
|
|
1433
|
-
/**
|
|
1434
|
-
* Payment Provider Factory
|
|
1435
|
-
*
|
|
1436
|
-
* Creates payment provider service based on configuration
|
|
1437
|
-
* Easy to swap providers: Adyen → Stripe → PayPal
|
|
1438
|
-
*/
|
|
1439
|
-
|
|
1440
|
-
import type { Logger } from '@fluentcommerce/fc-connect-sdk';
|
|
1441
|
-
import type { PaymentProviderService } from './payment-provider.service';
|
|
1442
|
-
import { AdyenPaymentProviderService } from './adyen-provider.service';
|
|
1443
|
-
// Future: import { StripePaymentProviderService } from './stripe-provider.service';
|
|
1444
|
-
|
|
1445
|
-
export class PaymentProviderFactory {
|
|
1446
|
-
/**
|
|
1447
|
-
* Create payment provider service
|
|
1448
|
-
*
|
|
1449
|
-
* @param provider - Provider name (adyen, stripe, paypal)
|
|
1450
|
-
* @param config - Provider-specific configuration
|
|
1451
|
-
* @param log - Logger instance
|
|
1452
|
-
*/
|
|
1453
|
-
static createProvider(
|
|
1454
|
-
provider: string,
|
|
1455
|
-
config: any,
|
|
1456
|
-
log: Logger
|
|
1457
|
-
): PaymentProviderService {
|
|
1458
|
-
switch (provider.toLowerCase()) {
|
|
1459
|
-
case 'adyen':
|
|
1460
|
-
return new AdyenPaymentProviderService(
|
|
1461
|
-
config.apiKey,
|
|
1462
|
-
config.environment,
|
|
1463
|
-
config.merchantAccount,
|
|
1464
|
-
log
|
|
1465
|
-
);
|
|
1466
|
-
|
|
1467
|
-
// Future: Stripe implementation
|
|
1468
|
-
// case 'stripe':
|
|
1469
|
-
// return new StripePaymentProviderService(
|
|
1470
|
-
// config.secretKey,
|
|
1471
|
-
// config.environment,
|
|
1472
|
-
// log
|
|
1473
|
-
// );
|
|
1474
|
-
|
|
1475
|
-
// Future: PayPal implementation
|
|
1476
|
-
// case 'paypal':
|
|
1477
|
-
// return new PayPalPaymentProviderService(
|
|
1478
|
-
// config.clientId,
|
|
1479
|
-
// config.clientSecret,
|
|
1480
|
-
// config.environment,
|
|
1481
|
-
// log
|
|
1482
|
-
// );
|
|
1483
|
-
|
|
1484
|
-
default:
|
|
1485
|
-
throw new Error(`Unsupported payment provider: ${provider}`);
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
```
|
|
1490
|
-
|
|
1491
|
-
---
|
|
1492
|
-
|
|
1493
|
-
### File: `src/workflows/webhook/payment-capture.ts`
|
|
1494
|
-
|
|
1495
|
-
```typescript
|
|
1496
|
-
/**
|
|
1497
|
-
* ═══════════════════════════════════════════════════════════════
|
|
1498
|
-
* 🚀 PAYMENT CAPTURE WEBHOOK
|
|
1499
|
-
* ═══════════════════════════════════════════════════════════════
|
|
1500
|
-
*
|
|
1501
|
-
* Intercepts Fluent Workflow webhook for payment capture
|
|
1502
|
-
*
|
|
1503
|
-
* TRIGGER SOURCE: ORDER/FULFILLMENT Workflow (Rubix)
|
|
1504
|
-
* - Triggered when order is ready to ship
|
|
1505
|
-
* - Validates triggerSource: ORDER_FULFILLMENT
|
|
1506
|
-
* - Note: Rubix workflow ensures order/payment is in correct state before calling
|
|
1507
|
-
*
|
|
1508
|
-
* Flow:
|
|
1509
|
-
* - Validates webhook payload and signature
|
|
1510
|
-
* - Validates trigger source (must be ORDER_FULFILLMENT)
|
|
1511
|
-
* - Calls payment provider Capture API
|
|
1512
|
-
* - Sends event to Fluent Commerce Event API
|
|
1513
|
-
* - Handles idempotency
|
|
1514
|
-
*
|
|
1515
|
-
* IMPORTANT: No entity status validation needed - Rubix workflow handles business logic
|
|
1516
|
-
*/
|
|
1517
|
-
|
|
1518
|
-
import { webhook } from '@versori/run';
|
|
1519
|
-
import { createClient, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
1520
|
-
import { WebhookValidationService } from '../../services/webhook-validation.service';
|
|
1521
|
-
import { PaymentProviderFactory } from '../../services/payment-provider-factory.service';
|
|
1522
|
-
import { PaymentEventService } from '../../services/payment-event.service';
|
|
1523
|
-
import { IdempotencyService } from '../../services/idempotency.service';
|
|
1524
|
-
import type { PaymentProviderService } from '../../services/payment-provider.service';
|
|
1525
|
-
|
|
1526
|
-
/**
|
|
1527
|
-
* Background processing function for payment capture
|
|
1528
|
-
* Handles all long-running operations (payment gateway API, event sending)
|
|
1529
|
-
*/
|
|
1530
|
-
async function processPaymentCapture(ctx: any, executionStartTime: number): Promise<void> {
|
|
1531
|
-
const { log, activation, connections } = ctx;
|
|
1532
|
-
|
|
1533
|
-
try {
|
|
1534
|
-
log.info('🔄 [BACKGROUND] Starting background payment capture processing', {
|
|
1535
|
-
correlationId: activation.id,
|
|
1536
|
-
});
|
|
1537
|
-
|
|
1538
|
-
// =================================================================
|
|
1539
|
-
// STEP 1: CONFIGURATION VALIDATION
|
|
1540
|
-
// =================================================================
|
|
1541
|
-
|
|
1542
|
-
if (!connections?.fluent_commerce) {
|
|
1543
|
-
log.error('❌ [BACKGROUND] Missing fluent_commerce connection');
|
|
1544
|
-
return;
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
const retailerId = activation.getVariable('fluentRetailerId');
|
|
1548
|
-
const paymentProvider = activation.getVariable('paymentProvider') || 'adyen';
|
|
1549
|
-
const paymentProviderEnv = (activation.getVariable('paymentProviderEnvironment') || 'test') as 'test' | 'live';
|
|
1550
|
-
const enableIdempotency = activation.getVariable('enableIdempotency') !== 'false';
|
|
1551
|
-
|
|
1552
|
-
// Get payment provider API key
|
|
1553
|
-
let apiKey: string | undefined;
|
|
1554
|
-
if (connections.payment_gateway?.credentials?.api_key) {
|
|
1555
|
-
apiKey = connections.payment_gateway.credentials.api_key;
|
|
1556
|
-
} else if (connections.adyen_payment_gateway?.credentials?.api_key) {
|
|
1557
|
-
apiKey = connections.adyen_payment_gateway.credentials.api_key;
|
|
1558
|
-
} else {
|
|
1559
|
-
apiKey = activation.getVariable('paymentGatewayApiKey') || activation.getVariable('adyenApiKey');
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
if (!apiKey) {
|
|
1563
|
-
log.error('❌ [BACKGROUND] Missing payment gateway API key');
|
|
1564
|
-
return;
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
const merchantAccount = activation.getVariable('adyenMerchantAccount');
|
|
1568
|
-
if (!merchantAccount && paymentProvider === 'adyen') {
|
|
1569
|
-
log.error('❌ [BACKGROUND] Missing adyenMerchantAccount');
|
|
1570
|
-
return;
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
// =================================================================
|
|
1574
|
-
// STEP 2: PAYLOAD VALIDATION
|
|
1575
|
-
// =================================================================
|
|
1576
|
-
|
|
1577
|
-
const rawPayload = activation.body || ctx.data;
|
|
1578
|
-
const payload = typeof rawPayload === 'string' ? JSON.parse(rawPayload) : rawPayload;
|
|
1579
|
-
const validator = new WebhookValidationService(log);
|
|
1580
|
-
const validation = validator.validatePayload(payload);
|
|
1581
|
-
|
|
1582
|
-
if (!validation.valid) {
|
|
1583
|
-
log.error('❌ [BACKGROUND] Invalid payload', { error: validation.error });
|
|
1584
|
-
return;
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
const paymentContext = validator.extractPaymentContext(validation.payload!);
|
|
1588
|
-
const finalRetailerId = paymentContext.retailerId || retailerId;
|
|
1589
|
-
|
|
1590
|
-
if (!finalRetailerId) {
|
|
1591
|
-
log.error('❌ [BACKGROUND] Missing retailerId');
|
|
1592
|
-
return;
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
// =================================================================
|
|
1596
|
-
// STEP 2.5: VALIDATE TRIGGER SOURCE (Capture must come from ORDER/FULFILLMENT)
|
|
1597
|
-
// =================================================================
|
|
1598
|
-
|
|
1599
|
-
const validateTriggerSource = activation.getVariable('validateTriggerSource') !== 'false';
|
|
1600
|
-
const allowedSources = (activation.getVariable('allowedTriggerSources') || 'ORDER_FULFILLMENT,PAYMENT_ENTITY')
|
|
1601
|
-
.split(',').map(s => s.trim());
|
|
1602
|
-
|
|
1603
|
-
if (validateTriggerSource) {
|
|
1604
|
-
const triggerValidation = validator.validateTriggerSource(
|
|
1605
|
-
payload,
|
|
1606
|
-
allowedSources,
|
|
1607
|
-
'ORDER_FULFILLMENT' // Capture MUST come from ORDER_FULFILLMENT
|
|
1608
|
-
);
|
|
1609
|
-
|
|
1610
|
-
if (!triggerValidation.valid) {
|
|
1611
|
-
log.error('❌ [BACKGROUND] Invalid trigger source', {
|
|
1612
|
-
triggerSource: payload.triggerSource,
|
|
1613
|
-
expected: 'ORDER_FULFILLMENT',
|
|
1614
|
-
error: triggerValidation.error,
|
|
1615
|
-
});
|
|
1616
|
-
return;
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
|
-
log.info('✅ [BACKGROUND] Trigger source validated', {
|
|
1620
|
-
triggerSource: payload.triggerSource,
|
|
1621
|
-
});
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
// =================================================================
|
|
1625
|
-
// STEP 3: INITIALIZE SERVICES
|
|
1626
|
-
// =================================================================
|
|
1627
|
-
|
|
1628
|
-
const fluentClient = await createClient(ctx, { validateConnection: true });
|
|
1629
|
-
await fluentClient.setRetailerId(finalRetailerId);
|
|
1630
|
-
|
|
1631
|
-
const kvAdapter = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
1632
|
-
const idempotencyService = new IdempotencyService(kvAdapter, log);
|
|
1633
|
-
|
|
1634
|
-
// Create payment provider service (easily swappable)
|
|
1635
|
-
const paymentProviderService: PaymentProviderService = PaymentProviderFactory.createProvider(
|
|
1636
|
-
paymentProvider,
|
|
1637
|
-
{
|
|
1638
|
-
apiKey,
|
|
1639
|
-
environment: paymentProviderEnv,
|
|
1640
|
-
merchantAccount,
|
|
1641
|
-
},
|
|
1642
|
-
log
|
|
1643
|
-
);
|
|
1644
|
-
|
|
1645
|
-
const eventService = new PaymentEventService(fluentClient, log);
|
|
1646
|
-
|
|
1647
|
-
// =================================================================
|
|
1648
|
-
// STEP 4: IDEMPOTENCY CHECK
|
|
1649
|
-
// =================================================================
|
|
1650
|
-
|
|
1651
|
-
if (enableIdempotency) {
|
|
1652
|
-
const idempotencyCheck = await idempotencyService.checkAndStore(
|
|
1653
|
-
'capture',
|
|
1654
|
-
paymentContext.paymentReference,
|
|
1655
|
-
'', // Will be updated after successful capture
|
|
1656
|
-
paymentContext.amount
|
|
1657
|
-
);
|
|
1658
|
-
|
|
1659
|
-
if (idempotencyCheck.alreadyProcessed && idempotencyCheck.existingRecord) {
|
|
1660
|
-
log.info('✅ [BACKGROUND] Payment already processed (idempotency)', {
|
|
1661
|
-
paymentReference: paymentContext.paymentReference,
|
|
1662
|
-
resultReference: idempotencyCheck.existingRecord.resultReference,
|
|
1663
|
-
});
|
|
1664
|
-
return; // Already processed, exit early
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
// =================================================================
|
|
1669
|
-
// STEP 5: CALL PAYMENT PROVIDER
|
|
1670
|
-
// =================================================================
|
|
1671
|
-
|
|
1672
|
-
log.info('💳 [BACKGROUND] Calling payment provider', {
|
|
1673
|
-
provider: paymentProviderService.getProviderName(),
|
|
1674
|
-
paymentReference: paymentContext.paymentReference,
|
|
1675
|
-
});
|
|
1676
|
-
|
|
1677
|
-
const captureResult = await paymentProviderService.capturePayment(
|
|
1678
|
-
paymentContext.paymentReference,
|
|
1679
|
-
paymentContext.amount,
|
|
1680
|
-
{
|
|
1681
|
-
...paymentContext,
|
|
1682
|
-
retailerId: finalRetailerId,
|
|
1683
|
-
}
|
|
1684
|
-
);
|
|
1685
|
-
|
|
1686
|
-
if (!captureResult.success) {
|
|
1687
|
-
log.error('❌ [BACKGROUND] Payment capture failed', {
|
|
1688
|
-
providerError: captureResult.error,
|
|
1689
|
-
paymentReference: paymentContext.paymentReference,
|
|
1690
|
-
});
|
|
1691
|
-
return; // Failed, exit
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
// =================================================================
|
|
1695
|
-
// STEP 6: UPDATE IDEMPOTENCY KEY
|
|
1696
|
-
// =================================================================
|
|
1697
|
-
|
|
1698
|
-
if (enableIdempotency) {
|
|
1699
|
-
await idempotencyService.checkAndStore(
|
|
1700
|
-
'capture',
|
|
1701
|
-
paymentContext.paymentReference,
|
|
1702
|
-
captureResult.paymentReference,
|
|
1703
|
-
captureResult.amount
|
|
1704
|
-
);
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
// =================================================================
|
|
1708
|
-
// STEP 7: SEND EVENT TO FLUENT
|
|
1709
|
-
// =================================================================
|
|
1710
|
-
|
|
1711
|
-
try {
|
|
1712
|
-
await eventService.sendCaptureEvent(captureResult, {
|
|
1713
|
-
...paymentContext,
|
|
1714
|
-
retailerId: finalRetailerId,
|
|
1715
|
-
});
|
|
1716
|
-
log.info('✅ [BACKGROUND] Payment event sent to Fluent', {
|
|
1717
|
-
paymentReference: captureResult.paymentReference,
|
|
1718
|
-
});
|
|
1719
|
-
} catch (error: any) {
|
|
1720
|
-
log.error('❌ [BACKGROUND] Event send failed (non-blocking)', {
|
|
1721
|
-
error: error.message,
|
|
1722
|
-
paymentReference: captureResult.paymentReference,
|
|
1723
|
-
note: 'Payment capture succeeded, but event failed - will be retried',
|
|
1724
|
-
});
|
|
1725
|
-
// Continue - event failure doesn't block success
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
// =================================================================
|
|
1729
|
-
// STEP 8: LOG SUCCESS
|
|
1730
|
-
// =================================================================
|
|
1731
|
-
|
|
1732
|
-
const duration = Date.now() - executionStartTime;
|
|
1733
|
-
log.info('✅ [BACKGROUND] Payment capture completed successfully', {
|
|
1734
|
-
paymentReference: paymentContext.paymentReference,
|
|
1735
|
-
resultReference: captureResult.paymentReference,
|
|
1736
|
-
provider: paymentProviderService.getProviderName(),
|
|
1737
|
-
duration: `${duration}ms`,
|
|
1738
|
-
});
|
|
1739
|
-
} catch (error: any) {
|
|
1740
|
-
log.error('❌ [BACKGROUND] Payment capture failed', {
|
|
1741
|
-
error: error.message,
|
|
1742
|
-
stack: error.stack,
|
|
1743
|
-
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
1744
|
-
});
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
/**
|
|
1749
|
-
* Payment Capture Webhook Handler
|
|
1750
|
-
* Uses sync + fire-and-forget pattern for fast response
|
|
1751
|
-
*/
|
|
1752
|
-
export const paymentCapture = webhook('payment-capture', {
|
|
1753
|
-
response: { mode: 'sync' }, // ✅ Sync mode: response sent when handler returns
|
|
1754
|
-
}, async (ctx) => {
|
|
1755
|
-
const { log, activation, connections } = ctx;
|
|
1756
|
-
const executionStartTime = Date.now();
|
|
1757
|
-
|
|
1758
|
-
log.info('🚀 [WEBHOOK] Received payment capture webhook', {
|
|
1759
|
-
correlationId: activation.id,
|
|
1760
|
-
timestamp: new Date().toISOString(),
|
|
1761
|
-
});
|
|
1762
|
-
|
|
1763
|
-
// =================================================================
|
|
1764
|
-
// QUICK VALIDATION (Synchronous, ~10-50ms)
|
|
1765
|
-
// =================================================================
|
|
1766
|
-
|
|
1767
|
-
if (!connections?.fluent_commerce) {
|
|
1768
|
-
log.error('❌ [WEBHOOK] Missing fluent_commerce connection');
|
|
1769
|
-
return {
|
|
1770
|
-
status: 500,
|
|
1771
|
-
body: {
|
|
1772
|
-
success: false,
|
|
1773
|
-
error: 'Missing fluent_commerce connection',
|
|
1774
|
-
recommendation: 'Configure fluent_commerce connection in Connections section',
|
|
1775
|
-
},
|
|
1776
|
-
};
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
const retailerId = activation.getVariable('fluentRetailerId');
|
|
1780
|
-
const paymentProvider = activation.getVariable('paymentProvider') || 'adyen';
|
|
1781
|
-
const webhookSecret = activation.getVariable('webhookSecret');
|
|
1782
|
-
|
|
1783
|
-
// Quick payload validation
|
|
1784
|
-
const rawPayload = activation.body || ctx.data;
|
|
1785
|
-
const payload = typeof rawPayload === 'string' ? JSON.parse(rawPayload) : rawPayload;
|
|
1786
|
-
|
|
1787
|
-
if (!payload.paymentReference || !payload.amount) {
|
|
1788
|
-
log.error('❌ [WEBHOOK] Invalid payload', {
|
|
1789
|
-
hasPaymentReference: !!payload.paymentReference,
|
|
1790
|
-
hasAmount: !!payload.amount,
|
|
1791
|
-
});
|
|
1792
|
-
return {
|
|
1793
|
-
status: 400,
|
|
1794
|
-
body: {
|
|
1795
|
-
success: false,
|
|
1796
|
-
error: 'Invalid payload: missing paymentReference or amount',
|
|
1797
|
-
},
|
|
1798
|
-
};
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
// Quick webhook signature validation (if configured)
|
|
1802
|
-
if (webhookSecret) {
|
|
1803
|
-
const payloadString = typeof rawPayload === 'string' ? rawPayload : JSON.stringify(rawPayload);
|
|
1804
|
-
const signature = activation.headers?.['x-webhook-signature'] ||
|
|
1805
|
-
activation.headers?.['webhook-signature'] ||
|
|
1806
|
-
activation.headers?.['signature'];
|
|
1807
|
-
|
|
1808
|
-
if (signature) {
|
|
1809
|
-
const validator = new WebhookValidationService(log);
|
|
1810
|
-
const isValid = validator.validateSignature(payloadString, signature, webhookSecret);
|
|
1811
|
-
|
|
1812
|
-
if (!isValid) {
|
|
1813
|
-
log.error('❌ [WEBHOOK] Invalid webhook signature');
|
|
1814
|
-
return {
|
|
1815
|
-
status: 401,
|
|
1816
|
-
body: {
|
|
1817
|
-
success: false,
|
|
1818
|
-
error: 'Invalid webhook signature',
|
|
1819
|
-
},
|
|
1820
|
-
};
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
log.info('✅ [WEBHOOK] Validation passed, starting background processing', {
|
|
1826
|
-
paymentReference: payload.paymentReference,
|
|
1827
|
-
});
|
|
1828
|
-
|
|
1829
|
-
// ✅ Fire-and-forget: Start background processing WITHOUT await
|
|
1830
|
-
// The promise continues execution after we return the response
|
|
1831
|
-
processPaymentCapture(ctx, executionStartTime)
|
|
1832
|
-
.then(() => {
|
|
1833
|
-
log.info('✅ [BACKGROUND] Payment capture processing completed successfully', {
|
|
1834
|
-
correlationId: activation.id,
|
|
1835
|
-
});
|
|
1836
|
-
})
|
|
1837
|
-
.catch((error: unknown) => {
|
|
1838
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1839
|
-
log.error('❌ [BACKGROUND] Payment capture processing failed', {
|
|
1840
|
-
correlationId: activation.id,
|
|
1841
|
-
error: errorMessage,
|
|
1842
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1843
|
-
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
1844
|
-
});
|
|
1845
|
-
});
|
|
1846
|
-
|
|
1847
|
-
// Return immediately (response sent with this return value)
|
|
1848
|
-
return {
|
|
1849
|
-
status: 200,
|
|
1850
|
-
body: {
|
|
1851
|
-
success: true,
|
|
1852
|
-
message: 'Payment capture started in background',
|
|
1853
|
-
paymentReference: payload.paymentReference,
|
|
1854
|
-
timestamp: new Date().toISOString(),
|
|
1855
|
-
},
|
|
1856
|
-
};
|
|
1857
|
-
});
|
|
1858
|
-
```
|
|
1859
|
-
|
|
1860
|
-
---
|
|
1861
|
-
|
|
1862
|
-
### File: `src/workflows/webhook/payment-refund.ts`
|
|
1863
|
-
|
|
1864
|
-
```typescript
|
|
1865
|
-
/**
|
|
1866
|
-
* ═══════════════════════════════════════════════════════════════
|
|
1867
|
-
* 🚀 PAYMENT REFUND WEBHOOK
|
|
1868
|
-
* ═══════════════════════════════════════════════════════════════
|
|
1869
|
-
*
|
|
1870
|
-
* Intercepts Fluent Workflow webhook for payment refund
|
|
1871
|
-
*
|
|
1872
|
-
* TRIGGER SOURCE: Payment Entity Orchestration (Rubix)
|
|
1873
|
-
* - Triggered when refund is approved (after RMA processing, order cancellation)
|
|
1874
|
-
* - Validates triggerSource: PAYMENT_ENTITY
|
|
1875
|
-
* - Note: Rubix workflow ensures payment is in correct state before calling
|
|
1876
|
-
*
|
|
1877
|
-
* IMPORTANT: No entity status validation needed - Rubix workflow handles business logic
|
|
1878
|
-
*/
|
|
1879
|
-
|
|
1880
|
-
import { webhook } from '@versori/run';
|
|
1881
|
-
import { createClient, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
1882
|
-
import { WebhookValidationService } from '../../services/webhook-validation.service';
|
|
1883
|
-
import { PaymentProviderFactory } from '../../services/payment-provider-factory.service';
|
|
1884
|
-
import { PaymentEventService } from '../../services/payment-event.service';
|
|
1885
|
-
import { IdempotencyService } from '../../services/idempotency.service';
|
|
1886
|
-
import type { PaymentProviderService } from '../../services/payment-provider.service';
|
|
1887
|
-
|
|
1888
|
-
export const paymentRefund = webhook('payment-refund', async (ctx) => {
|
|
1889
|
-
const { log, activation, connections } = ctx;
|
|
1890
|
-
const executionStartTime = Date.now();
|
|
1891
|
-
|
|
1892
|
-
try {
|
|
1893
|
-
log.info('🚀 [PaymentRefund] Processing payment refund webhook', {
|
|
1894
|
-
correlationId: activation.id,
|
|
1895
|
-
});
|
|
1896
|
-
|
|
1897
|
-
// Similar structure to payment-capture.ts, but calls refundPayment()
|
|
1898
|
-
// ... (same configuration validation, webhook validation, etc.)
|
|
1899
|
-
|
|
1900
|
-
const fluentClient = await createClient(ctx, { validateConnection: true });
|
|
1901
|
-
await fluentClient.setRetailerId(finalRetailerId);
|
|
1902
|
-
|
|
1903
|
-
const kvAdapter = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
1904
|
-
const idempotencyService = new IdempotencyService(kvAdapter, log);
|
|
1905
|
-
|
|
1906
|
-
const paymentProviderService: PaymentProviderService = PaymentProviderFactory.createProvider(
|
|
1907
|
-
paymentProvider,
|
|
1908
|
-
{ apiKey, environment: paymentProviderEnv, merchantAccount },
|
|
1909
|
-
log
|
|
1910
|
-
);
|
|
1911
|
-
|
|
1912
|
-
const eventService = new PaymentEventService(fluentClient, log);
|
|
1913
|
-
|
|
1914
|
-
// Idempotency check
|
|
1915
|
-
if (enableIdempotency) {
|
|
1916
|
-
const idempotencyCheck = await idempotencyService.checkAndStore(
|
|
1917
|
-
'refund',
|
|
1918
|
-
paymentContext.paymentReference,
|
|
1919
|
-
'',
|
|
1920
|
-
paymentContext.amount
|
|
1921
|
-
);
|
|
1922
|
-
|
|
1923
|
-
if (idempotencyCheck.alreadyProcessed) {
|
|
1924
|
-
return {
|
|
1925
|
-
status: 200,
|
|
1926
|
-
body: {
|
|
1927
|
-
success: true,
|
|
1928
|
-
alreadyProcessed: true,
|
|
1929
|
-
refundReference: idempotencyCheck.existingRecord!.resultReference,
|
|
1930
|
-
},
|
|
1931
|
-
};
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
// Call payment provider
|
|
1936
|
-
const refundResult = await paymentProviderService.refundPayment(
|
|
1937
|
-
paymentContext.paymentReference,
|
|
1938
|
-
paymentContext.amount,
|
|
1939
|
-
{ ...paymentContext, retailerId: finalRetailerId }
|
|
1940
|
-
);
|
|
1941
|
-
|
|
1942
|
-
if (!refundResult.success) {
|
|
1943
|
-
return {
|
|
1944
|
-
status: 502,
|
|
1945
|
-
body: {
|
|
1946
|
-
success: false,
|
|
1947
|
-
error: 'Payment refund failed',
|
|
1948
|
-
providerError: refundResult.error,
|
|
1949
|
-
},
|
|
1950
|
-
};
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
// Update idempotency
|
|
1954
|
-
if (enableIdempotency) {
|
|
1955
|
-
await idempotencyService.checkAndStore(
|
|
1956
|
-
'refund',
|
|
1957
|
-
paymentContext.paymentReference,
|
|
1958
|
-
refundResult.refundReference,
|
|
1959
|
-
refundResult.amount
|
|
1960
|
-
);
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
// Send event
|
|
1964
|
-
await eventService.sendRefundEvent(refundResult, {
|
|
1965
|
-
...paymentContext,
|
|
1966
|
-
retailerId: finalRetailerId,
|
|
1967
|
-
});
|
|
1968
|
-
|
|
1969
|
-
const duration = Date.now() - executionStartTime;
|
|
1970
|
-
|
|
1971
|
-
return {
|
|
1972
|
-
status: 200,
|
|
1973
|
-
body: {
|
|
1974
|
-
success: true,
|
|
1975
|
-
refundReference: refundResult.refundReference,
|
|
1976
|
-
originalPaymentReference: refundResult.originalPaymentReference,
|
|
1977
|
-
amount: refundResult.amount.value,
|
|
1978
|
-
currency: refundResult.amount.currency,
|
|
1979
|
-
refundType: refundResult.refundType,
|
|
1980
|
-
refundedAt: refundResult.refundedAt,
|
|
1981
|
-
provider: paymentProviderService.getProviderName(),
|
|
1982
|
-
duration: `${duration}ms`,
|
|
1983
|
-
},
|
|
1984
|
-
};
|
|
1985
|
-
} catch (error: any) {
|
|
1986
|
-
log.error('❌ [PaymentRefund] Unexpected error', { error: error.message });
|
|
1987
|
-
return {
|
|
1988
|
-
status: 500,
|
|
1989
|
-
body: {
|
|
1990
|
-
success: false,
|
|
1991
|
-
error: 'Internal server error',
|
|
1992
|
-
message: error.message,
|
|
1993
|
-
},
|
|
1994
|
-
};
|
|
1995
|
-
}
|
|
1996
|
-
});
|
|
1997
|
-
```
|
|
1998
|
-
|
|
1999
|
-
---
|
|
2000
|
-
|
|
2001
|
-
### File: `src/workflows/webhook/payment-auth-cancel.ts`
|
|
2002
|
-
|
|
2003
|
-
```typescript
|
|
2004
|
-
/**
|
|
2005
|
-
* ═══════════════════════════════════════════════════════════════
|
|
2006
|
-
* 🚀 PAYMENT AUTH CANCEL WEBHOOK
|
|
2007
|
-
* ═══════════════════════════════════════════════════════════════
|
|
2008
|
-
*
|
|
2009
|
-
* Intercepts Fluent Workflow webhook for authorization cancellation
|
|
2010
|
-
*
|
|
2011
|
-
* TRIGGER SOURCE: Payment Entity Orchestration (Rubix)
|
|
2012
|
-
* - Triggered when order is cancelled before capture
|
|
2013
|
-
* - Validates triggerSource: PAYMENT_ENTITY
|
|
2014
|
-
* - Note: Rubix workflow ensures payment is in correct state before calling
|
|
2015
|
-
*
|
|
2016
|
-
* IMPORTANT: No entity status validation needed - Rubix workflow handles business logic
|
|
2017
|
-
*/
|
|
2018
|
-
|
|
2019
|
-
import { webhook } from '@versori/run';
|
|
2020
|
-
import { createClient, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
2021
|
-
import { WebhookValidationService } from '../../services/webhook-validation.service';
|
|
2022
|
-
import { PaymentProviderFactory } from '../../services/payment-provider-factory.service';
|
|
2023
|
-
import { PaymentEventService } from '../../services/payment-event.service';
|
|
2024
|
-
import { IdempotencyService } from '../../services/idempotency.service';
|
|
2025
|
-
import type { PaymentProviderService } from '../../services/payment-provider.service';
|
|
2026
|
-
|
|
2027
|
-
export const paymentAuthCancel = webhook('payment-auth-cancel', async (ctx) => {
|
|
2028
|
-
const { log, activation, connections } = ctx;
|
|
2029
|
-
const executionStartTime = Date.now();
|
|
2030
|
-
|
|
2031
|
-
try {
|
|
2032
|
-
log.info('🚀 [PaymentAuthCancel] Processing authorization cancellation webhook', {
|
|
2033
|
-
correlationId: activation.id,
|
|
2034
|
-
});
|
|
2035
|
-
|
|
2036
|
-
// Similar structure, but:
|
|
2037
|
-
// - No amount validation (cancellation doesn't need amount)
|
|
2038
|
-
// - Calls cancelAuthorization()
|
|
2039
|
-
// - Sends PaymentAuthorizationCancelled event
|
|
2040
|
-
|
|
2041
|
-
// ... (configuration validation, webhook validation similar to capture)
|
|
2042
|
-
|
|
2043
|
-
const paymentProviderService: PaymentProviderService = PaymentProviderFactory.createProvider(
|
|
2044
|
-
paymentProvider,
|
|
2045
|
-
{ apiKey, environment: paymentProviderEnv, merchantAccount },
|
|
2046
|
-
log
|
|
2047
|
-
);
|
|
2048
|
-
|
|
2049
|
-
const cancelResult = await paymentProviderService.cancelAuthorization(
|
|
2050
|
-
paymentContext.paymentReference,
|
|
2051
|
-
{ ...paymentContext, retailerId: finalRetailerId }
|
|
2052
|
-
);
|
|
2053
|
-
|
|
2054
|
-
if (!cancelResult.success) {
|
|
2055
|
-
return {
|
|
2056
|
-
status: 502,
|
|
2057
|
-
body: {
|
|
2058
|
-
success: false,
|
|
2059
|
-
error: 'Authorization cancellation failed',
|
|
2060
|
-
providerError: cancelResult.error,
|
|
2061
|
-
},
|
|
2062
|
-
};
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
await eventService.sendCancelEvent(cancelResult, {
|
|
2066
|
-
...paymentContext,
|
|
2067
|
-
retailerId: finalRetailerId,
|
|
2068
|
-
});
|
|
2069
|
-
|
|
2070
|
-
return {
|
|
2071
|
-
status: 200,
|
|
2072
|
-
body: {
|
|
2073
|
-
success: true,
|
|
2074
|
-
cancellationReference: cancelResult.cancellationReference,
|
|
2075
|
-
originalPaymentReference: cancelResult.originalPaymentReference,
|
|
2076
|
-
cancelledAt: cancelResult.cancelledAt,
|
|
2077
|
-
provider: paymentProviderService.getProviderName(),
|
|
2078
|
-
},
|
|
2079
|
-
};
|
|
2080
|
-
} catch (error: any) {
|
|
2081
|
-
log.error('❌ [PaymentAuthCancel] Unexpected error', { error: error.message });
|
|
2082
|
-
return {
|
|
2083
|
-
status: 500,
|
|
2084
|
-
body: {
|
|
2085
|
-
success: false,
|
|
2086
|
-
error: 'Internal server error',
|
|
2087
|
-
message: error.message,
|
|
2088
|
-
},
|
|
2089
|
-
};
|
|
2090
|
-
}
|
|
2091
|
-
});
|
|
2092
|
-
```
|
|
2093
|
-
|
|
2094
|
-
---
|
|
2095
|
-
|
|
2096
|
-
### File: `src/workflows/webhook/payment-reauth.ts`
|
|
2097
|
-
|
|
2098
|
-
```typescript
|
|
2099
|
-
/**
|
|
2100
|
-
* ═══════════════════════════════════════════════════════════════
|
|
2101
|
-
* 🚀 PAYMENT REAUTH WEBHOOK
|
|
2102
|
-
* ═══════════════════════════════════════════════════════════════
|
|
2103
|
-
*
|
|
2104
|
-
* Intercepts Fluent Workflow webhook for payment reauthorization
|
|
2105
|
-
*
|
|
2106
|
-
* TRIGGER SOURCE: Payment Entity Orchestration (Rubix)
|
|
2107
|
-
* - Triggered when authorization expires or needs extension
|
|
2108
|
-
* - Validates triggerSource: PAYMENT_ENTITY
|
|
2109
|
-
* - Note: Rubix workflow ensures authorization is in correct state before calling
|
|
2110
|
-
*
|
|
2111
|
-
* IMPORTANT: No entity status validation needed - Rubix workflow handles business logic
|
|
2112
|
-
*/
|
|
2113
|
-
|
|
2114
|
-
import { webhook } from '@versori/run';
|
|
2115
|
-
import { createClient, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
2116
|
-
import { WebhookValidationService } from '../../services/webhook-validation.service';
|
|
2117
|
-
import { PaymentProviderFactory } from '../../services/payment-provider-factory.service';
|
|
2118
|
-
import { PaymentEventService } from '../../services/payment-event.service';
|
|
2119
|
-
import { IdempotencyService } from '../../services/idempotency.service';
|
|
2120
|
-
import type { PaymentProviderService } from '../../services/payment-provider.service';
|
|
2121
|
-
|
|
2122
|
-
export const paymentReauth = webhook('payment-reauth', async (ctx) => {
|
|
2123
|
-
const { log, activation, connections } = ctx;
|
|
2124
|
-
const executionStartTime = Date.now();
|
|
2125
|
-
|
|
2126
|
-
try {
|
|
2127
|
-
log.info('🚀 [PaymentReauth] Processing payment reauthorization webhook', {
|
|
2128
|
-
correlationId: activation.id,
|
|
2129
|
-
});
|
|
2130
|
-
|
|
2131
|
-
// Similar structure to capture, but:
|
|
2132
|
-
// - Calls reauthorizePayment()
|
|
2133
|
-
// - Sends PaymentReauthorized event
|
|
2134
|
-
|
|
2135
|
-
// ... (configuration validation, webhook validation similar to capture)
|
|
2136
|
-
|
|
2137
|
-
const paymentProviderService: PaymentProviderService = PaymentProviderFactory.createProvider(
|
|
2138
|
-
paymentProvider,
|
|
2139
|
-
{ apiKey, environment: paymentProviderEnv, merchantAccount },
|
|
2140
|
-
log
|
|
2141
|
-
);
|
|
2142
|
-
|
|
2143
|
-
const reauthResult = await paymentProviderService.reauthorizePayment(
|
|
2144
|
-
paymentContext.paymentReference,
|
|
2145
|
-
paymentContext.amount,
|
|
2146
|
-
{ ...paymentContext, retailerId: finalRetailerId }
|
|
2147
|
-
);
|
|
2148
|
-
|
|
2149
|
-
if (!reauthResult.success) {
|
|
2150
|
-
return {
|
|
2151
|
-
status: 502,
|
|
2152
|
-
body: {
|
|
2153
|
-
success: false,
|
|
2154
|
-
error: 'Payment reauthorization failed',
|
|
2155
|
-
providerError: reauthResult.error,
|
|
2156
|
-
},
|
|
2157
|
-
};
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
await eventService.sendReauthEvent(reauthResult, {
|
|
2161
|
-
...paymentContext,
|
|
2162
|
-
retailerId: finalRetailerId,
|
|
2163
|
-
});
|
|
2164
|
-
|
|
2165
|
-
return {
|
|
2166
|
-
status: 200,
|
|
2167
|
-
body: {
|
|
2168
|
-
success: true,
|
|
2169
|
-
reauthReference: reauthResult.reauthReference,
|
|
2170
|
-
originalPaymentReference: reauthResult.originalPaymentReference,
|
|
2171
|
-
amount: reauthResult.amount.value,
|
|
2172
|
-
currency: reauthResult.amount.currency,
|
|
2173
|
-
reauthorizedAt: reauthResult.reauthorizedAt,
|
|
2174
|
-
expiryDate: reauthResult.expiryDate,
|
|
2175
|
-
provider: paymentProviderService.getProviderName(),
|
|
2176
|
-
},
|
|
2177
|
-
};
|
|
2178
|
-
} catch (error: any) {
|
|
2179
|
-
log.error('❌ [PaymentReauth] Unexpected error', { error: error.message });
|
|
2180
|
-
return {
|
|
2181
|
-
status: 500,
|
|
2182
|
-
body: {
|
|
2183
|
-
success: false,
|
|
2184
|
-
error: 'Internal server error',
|
|
2185
|
-
message: error.message,
|
|
2186
|
-
},
|
|
2187
|
-
};
|
|
2188
|
-
}
|
|
2189
|
-
});
|
|
2190
|
-
```
|
|
2191
|
-
|
|
2192
|
-
---
|
|
2193
|
-
|
|
2194
|
-
## How to Swap Payment Providers
|
|
2195
|
-
|
|
2196
|
-
### Current: Adyen → Stripe
|
|
2197
|
-
|
|
2198
|
-
**Step 1**: Create `src/services/stripe-provider.service.ts`
|
|
2199
|
-
|
|
2200
|
-
```typescript
|
|
2201
|
-
import type { PaymentProviderService } from './payment-provider.service';
|
|
2202
|
-
// Implement StripePaymentProviderService with same interface
|
|
2203
|
-
```
|
|
2204
|
-
|
|
2205
|
-
**Step 2**: Update `PaymentProviderFactory`
|
|
2206
|
-
|
|
2207
|
-
```typescript
|
|
2208
|
-
case 'stripe':
|
|
2209
|
-
return new StripePaymentProviderService(
|
|
2210
|
-
config.secretKey,
|
|
2211
|
-
config.environment,
|
|
2212
|
-
log
|
|
2213
|
-
);
|
|
2214
|
-
```
|
|
2215
|
-
|
|
2216
|
-
**Step 3**: Update Activation Variables
|
|
2217
|
-
|
|
2218
|
-
```bash
|
|
2219
|
-
paymentProvider=stripe # Change from adyen to stripe
|
|
2220
|
-
stripeSecretKey=sk_test_... # Add Stripe config
|
|
2221
|
-
```
|
|
2222
|
-
|
|
2223
|
-
**That's it!** All 4 webhooks automatically use Stripe. No changes needed to webhook code.
|
|
2224
|
-
|
|
2225
|
-
---
|
|
2226
|
-
|
|
2227
|
-
## Expected Webhook Payloads
|
|
2228
|
-
|
|
2229
|
-
### Capture Payload (from ORDER/FULFILLMENT Workflow)
|
|
2230
|
-
|
|
2231
|
-
```json
|
|
2232
|
-
{
|
|
2233
|
-
"orderRef": "ORDER-12345",
|
|
2234
|
-
"paymentReference": "8826162495174985",
|
|
2235
|
-
"amount": {
|
|
2236
|
-
"value": 10000,
|
|
2237
|
-
"currency": "USD"
|
|
2238
|
-
},
|
|
2239
|
-
"retailerId": "2",
|
|
2240
|
-
"merchantReference": "ORDER-12345",
|
|
2241
|
-
"triggerSource": "ORDER_FULFILLMENT",
|
|
2242
|
-
"orderStatus": "READY_TO_SHIP"
|
|
2243
|
-
}
|
|
2244
|
-
```
|
|
2245
|
-
|
|
2246
|
-
### Refund Payload (from Payment Entity Orchestration)
|
|
2247
|
-
|
|
2248
|
-
```json
|
|
2249
|
-
{
|
|
2250
|
-
"orderRef": "ORDER-12345",
|
|
2251
|
-
"paymentReference": "8826162495174985",
|
|
2252
|
-
"amount": {
|
|
2253
|
-
"value": 10000,
|
|
2254
|
-
"currency": "USD"
|
|
2255
|
-
},
|
|
2256
|
-
"retailerId": "2",
|
|
2257
|
-
"merchantReference": "ORDER-12345",
|
|
2258
|
-
"triggerSource": "PAYMENT_ENTITY",
|
|
2259
|
-
"refundReason": "RETURN_APPROVED",
|
|
2260
|
-
"rmaRef": "RMA-67890",
|
|
2261
|
-
"refundType": "FULL"
|
|
2262
|
-
}
|
|
2263
|
-
```
|
|
2264
|
-
|
|
2265
|
-
### Auth Cancel Payload (from Payment Entity Orchestration)
|
|
2266
|
-
|
|
2267
|
-
```json
|
|
2268
|
-
{
|
|
2269
|
-
"orderRef": "ORDER-12345",
|
|
2270
|
-
"paymentReference": "8826162495174985",
|
|
2271
|
-
"retailerId": "2",
|
|
2272
|
-
"merchantReference": "ORDER-12345",
|
|
2273
|
-
"triggerSource": "PAYMENT_ENTITY",
|
|
2274
|
-
"cancelReason": "ORDER_CANCELLED",
|
|
2275
|
-
"orderStatus": "CANCELLED"
|
|
2276
|
-
}
|
|
2277
|
-
```
|
|
2278
|
-
|
|
2279
|
-
### ReAuth Payload (from Payment Entity Orchestration)
|
|
2280
|
-
|
|
2281
|
-
```json
|
|
2282
|
-
{
|
|
2283
|
-
"orderRef": "ORDER-12345",
|
|
2284
|
-
"paymentReference": "8826162495174985",
|
|
2285
|
-
"amount": {
|
|
2286
|
-
"value": 10000,
|
|
2287
|
-
"currency": "USD"
|
|
2288
|
-
},
|
|
2289
|
-
"retailerId": "2",
|
|
2290
|
-
"merchantReference": "ORDER-12345",
|
|
2291
|
-
"triggerSource": "PAYMENT_ENTITY",
|
|
2292
|
-
"reauthReason": "AUTHORIZATION_EXPIRING"
|
|
2293
|
-
}
|
|
2294
|
-
```
|
|
2295
|
-
|
|
2296
|
-
---
|
|
2297
|
-
|
|
2298
|
-
## Expected Events Sent to Fluent Commerce
|
|
2299
|
-
|
|
2300
|
-
### PaymentCaptured
|
|
2301
|
-
|
|
2302
|
-
```json
|
|
2303
|
-
{
|
|
2304
|
-
"name": "PaymentCaptured",
|
|
2305
|
-
"entityType": "ORDER",
|
|
2306
|
-
"entityRef": "ORDER-12345",
|
|
2307
|
-
"data": {
|
|
2308
|
-
"paymentReference": "8826162495174985",
|
|
2309
|
-
"originalPaymentReference": "8826162495174985",
|
|
2310
|
-
"amount": 10000,
|
|
2311
|
-
"currency": "USD",
|
|
2312
|
-
"capturedAt": "2025-01-22T12:34:56.789Z",
|
|
2313
|
-
"resultCode": "Received",
|
|
2314
|
-
"merchantReference": "ORDER-12345"
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
|
-
```
|
|
2318
|
-
|
|
2319
|
-
### PaymentRefunded
|
|
2320
|
-
|
|
2321
|
-
```json
|
|
2322
|
-
{
|
|
2323
|
-
"name": "PaymentRefunded",
|
|
2324
|
-
"entityType": "ORDER",
|
|
2325
|
-
"entityRef": "ORDER-12345",
|
|
2326
|
-
"data": {
|
|
2327
|
-
"refundReference": "8826162495174986",
|
|
2328
|
-
"originalPaymentReference": "8826162495174985",
|
|
2329
|
-
"amount": 10000,
|
|
2330
|
-
"currency": "USD",
|
|
2331
|
-
"refundType": "FULL",
|
|
2332
|
-
"refundedAt": "2025-01-22T12:34:56.789Z",
|
|
2333
|
-
"merchantReference": "ORDER-12345"
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
|
-
```
|
|
2337
|
-
|
|
2338
|
-
### PaymentAuthorizationCancelled
|
|
2339
|
-
|
|
2340
|
-
```json
|
|
2341
|
-
{
|
|
2342
|
-
"name": "PaymentAuthorizationCancelled",
|
|
2343
|
-
"entityType": "ORDER",
|
|
2344
|
-
"entityRef": "ORDER-12345",
|
|
2345
|
-
"data": {
|
|
2346
|
-
"cancellationReference": "8826162495174987",
|
|
2347
|
-
"originalPaymentReference": "8826162495174985",
|
|
2348
|
-
"cancelledAt": "2025-01-22T12:34:56.789Z",
|
|
2349
|
-
"merchantReference": "ORDER-12345"
|
|
2350
|
-
}
|
|
2351
|
-
}
|
|
2352
|
-
```
|
|
2353
|
-
|
|
2354
|
-
### PaymentReauthorized
|
|
2355
|
-
|
|
2356
|
-
```json
|
|
2357
|
-
{
|
|
2358
|
-
"name": "PaymentReauthorized",
|
|
2359
|
-
"entityType": "ORDER",
|
|
2360
|
-
"entityRef": "ORDER-12345",
|
|
2361
|
-
"data": {
|
|
2362
|
-
"reauthReference": "8826162495174988",
|
|
2363
|
-
"originalPaymentReference": "8826162495174985",
|
|
2364
|
-
"amount": 10000,
|
|
2365
|
-
"currency": "USD",
|
|
2366
|
-
"reauthorizedAt": "2025-01-22T12:34:56.789Z",
|
|
2367
|
-
"expiryDate": "2025-02-22T12:34:56.789Z",
|
|
2368
|
-
"merchantReference": "ORDER-12345"
|
|
2369
|
-
}
|
|
2370
|
-
}
|
|
2371
|
-
```
|
|
2372
|
-
|
|
2373
|
-
---
|
|
2374
|
-
|
|
2375
|
-
## Testing Checklist
|
|
2376
|
-
|
|
2377
|
-
### 1. Configuration Testing
|
|
2378
|
-
- [ ] Fluent Commerce connection configured
|
|
2379
|
-
- [ ] Payment gateway connection configured (Adyen)
|
|
2380
|
-
- [ ] All activation variables set
|
|
2381
|
-
- [ ] Provider can be swapped (test Stripe if implemented)
|
|
2382
|
-
|
|
2383
|
-
### 2. Webhook Testing (All 4)
|
|
2384
|
-
- [ ] Capture webhook works
|
|
2385
|
-
- [ ] Refund webhook works
|
|
2386
|
-
- [ ] Auth Cancel webhook works
|
|
2387
|
-
- [ ] ReAuth webhook works
|
|
2388
|
-
|
|
2389
|
-
### 3. Idempotency Testing
|
|
2390
|
-
- [ ] Duplicate capture calls return same result
|
|
2391
|
-
- [ ] Duplicate refund calls return same result
|
|
2392
|
-
- [ ] Duplicate cancel calls return same result
|
|
2393
|
-
- [ ] Duplicate reauth calls return same result
|
|
2394
|
-
|
|
2395
|
-
### 4. Event Testing
|
|
2396
|
-
- [ ] Capture event sent to Fluent
|
|
2397
|
-
- [ ] Refund event sent to Fluent
|
|
2398
|
-
- [ ] Cancel event sent to Fluent
|
|
2399
|
-
- [ ] ReAuth event sent to Fluent
|
|
2400
|
-
|
|
2401
|
-
### 5. Provider Swap Testing (Future)
|
|
2402
|
-
- [ ] Switch to Stripe provider
|
|
2403
|
-
- [ ] Verify all 4 flows work with Stripe
|
|
2404
|
-
- [ ] Verify events still sent correctly
|
|
2405
|
-
|
|
2406
|
-
---
|
|
2407
|
-
|
|
2408
|
-
## Deployment Steps
|
|
2409
|
-
|
|
2410
|
-
1. **Set up Connections**:
|
|
2411
|
-
- Configure `fluent_commerce` connection (OAuth2)
|
|
2412
|
-
- Configure `payment_gateway` or `adyen_payment_gateway` connection (API key)
|
|
2413
|
-
|
|
2414
|
-
2. **Set Activation Variables**:
|
|
2415
|
-
- `fluentRetailerId`
|
|
2416
|
-
- `paymentProvider` (default: `adyen`)
|
|
2417
|
-
- `paymentProviderEnvironment` (test/live)
|
|
2418
|
-
- `adyenMerchantAccount` (if using Adyen)
|
|
2419
|
-
- `webhookSecret` (optional)
|
|
2420
|
-
|
|
2421
|
-
3. **Deploy Workflow**:
|
|
2422
|
-
```bash
|
|
2423
|
-
cd versori-payment-gateway-integration
|
|
2424
|
-
npm install
|
|
2425
|
-
versori deploy
|
|
2426
|
-
```
|
|
2427
|
-
|
|
2428
|
-
4. **Configure Fluent Workflows**:
|
|
2429
|
-
- Point webhook URLs to Versori endpoints:
|
|
2430
|
-
- `/payment-capture`
|
|
2431
|
-
- `/payment-refund`
|
|
2432
|
-
- `/payment-auth-cancel`
|
|
2433
|
-
- `/payment-reauth`
|
|
2434
|
-
|
|
2435
|
-
5. **Test End-to-End**:
|
|
2436
|
-
- Test each webhook from Fluent Workflow
|
|
2437
|
-
- Verify payment provider API calls
|
|
2438
|
-
- Verify events sent to Fluent Commerce
|
|
2439
|
-
- Check Versori logs for errors
|
|
2440
|
-
|
|
2441
|
-
---
|
|
2442
|
-
|
|
2443
|
-
## Key Benefits of This Architecture
|
|
2444
|
-
|
|
2445
|
-
✅ **Single Connector** - All 4 flows in one document/workflow
|
|
2446
|
-
✅ **Provider Agnostic** - Easy to swap Adyen → Stripe (change one service)
|
|
2447
|
-
✅ **DRY Principle** - Shared services used by all webhooks
|
|
2448
|
-
✅ **Maintainable** - Clear separation of concerns
|
|
2449
|
-
✅ **Testable** - Each layer can be tested independently
|
|
2450
|
-
✅ **Scalable** - Easy to add new payment providers or operations
|
|
2451
|
-
|
|
2452
|
-
---
|
|
2453
|
-
|
|
2454
|
-
## Future Enhancements
|
|
2455
|
-
|
|
2456
|
-
### Adding Stripe Support
|
|
2457
|
-
|
|
2458
|
-
1. Create `StripePaymentProviderService` implementing `PaymentProviderService`
|
|
2459
|
-
2. Add case to `PaymentProviderFactory`
|
|
2460
|
-
3. Update activation variables
|
|
2461
|
-
4. **Done!** All 4 webhooks automatically use Stripe
|
|
2462
|
-
|
|
2463
|
-
### Adding PayPal Support
|
|
2464
|
-
|
|
2465
|
-
Same pattern - implement interface, add to factory, configure variables.
|
|
2466
|
-
|
|
2467
|
-
### Adding New Operations
|
|
2468
|
-
|
|
2469
|
-
1. Add method to `PaymentProviderService` interface
|
|
2470
|
-
2. Implement in all provider services
|
|
2471
|
-
3. Add webhook workflow
|
|
2472
|
-
4. Add event service method
|
|
2473
|
-
5. **Done!**
|
|
2474
|
-
|
|
2475
|
-
---
|
|
2476
|
-
|
|
2477
|
-
This template provides a complete, modular, provider-agnostic payment gateway integration that's easy to maintain and extend!
|
|
2478
|
-
|
|
1
|
+
---
|
|
2
|
+
template_id: tpl-webhook-payment-gateway-integration
|
|
3
|
+
canonical_filename: template-webhook-payment-gateway-integration.md
|
|
4
|
+
version: 2.0.0
|
|
5
|
+
sdk_version: ^0.1.39
|
|
6
|
+
runtime: versori
|
|
7
|
+
direction: integration
|
|
8
|
+
source: webhook-json-payment
|
|
9
|
+
destination: fluent-event-api
|
|
10
|
+
entity: payment
|
|
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 - Payment Gateway Integration (Adyen)
|
|
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**: Intercept Fluent Workflow webhooks for payment operations (Capture, Refund, Auth Cancel, ReAuth), process via payment gateway (Adyen), and send events back to Fluent Commerce
|
|
42
|
+
|
|
43
|
+
**Complexity**: Medium-High
|
|
44
|
+
|
|
45
|
+
**Runtime**: Versori Platform
|
|
46
|
+
|
|
47
|
+
**Estimated Lines**: ~1600 lines (modular structure)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## STEP 1: Understand This Template
|
|
52
|
+
|
|
53
|
+
**What This Template Does:**
|
|
54
|
+
|
|
55
|
+
- **4 Versori HTTP webhooks** for payment operations:
|
|
56
|
+
- **Capture** - Capture an authorized payment (triggered by ORDER/FULFILLMENT workflow)
|
|
57
|
+
- **Refund** - Refund a captured payment (triggered by Payment Entity orchestration)
|
|
58
|
+
- **Auth Cancel** - Cancel an authorization (triggered by Payment Entity orchestration)
|
|
59
|
+
- **ReAuth** - Re-authorize a payment (triggered by Payment Entity orchestration)
|
|
60
|
+
|
|
61
|
+
- **Modular architecture** - Easy to swap payment providers (Adyen → Stripe, etc.)
|
|
62
|
+
- **Shared services** - Webhook validation, event sending, idempotency
|
|
63
|
+
- **Payment provider abstraction** - Switch providers by changing one service
|
|
64
|
+
- **Event mapping** - Payment gateway responses → Fluent event format
|
|
65
|
+
- **Idempotency handling** - Prevent duplicate operations
|
|
66
|
+
- **Error handling** - Comprehensive error handling and retry logic
|
|
67
|
+
- **Trigger source validation** - Ensures webhooks called from correct Fluent workflows
|
|
68
|
+
- **Sync + Fire-and-Forget Pattern**: Fast webhook response, background processing
|
|
69
|
+
|
|
70
|
+
**Key SDK Components:**
|
|
71
|
+
|
|
72
|
+
- `createClient()` - Universal client factory (auto-detects Versori context)
|
|
73
|
+
- `client.sendEvent()` - Send events to Fluent Commerce Event API
|
|
74
|
+
- `VersoriKVAdapter` - Idempotency tracking (KV storage)
|
|
75
|
+
- `WebhookValidationService` - Webhook signature validation
|
|
76
|
+
- Native Versori `log` - Use `log` from context
|
|
77
|
+
|
|
78
|
+
**Entity Type:**
|
|
79
|
+
|
|
80
|
+
- **Payment** - Fluent entity for payment operations
|
|
81
|
+
- **Event API** - Uses `sendEvent()` to send payment events back to Fluent
|
|
82
|
+
|
|
83
|
+
**Critical Patterns:**
|
|
84
|
+
|
|
85
|
+
- **Sync + Fire-and-Forget**: Webhook validates quickly, returns immediately, processes payment in background
|
|
86
|
+
- **External JSON Config**: Payment provider configuration in separate JSON file (`config/payment-provider-config.json`)
|
|
87
|
+
- **Modular Architecture**: Separate services, workflows, config, types folders
|
|
88
|
+
- **Background Processing**: Long-running operations (payment gateway API calls, event sending) happen asynchronously
|
|
89
|
+
- **Idempotency**: KV storage prevents duplicate payment operations
|
|
90
|
+
- **Provider Abstraction**: Easy to swap payment providers (Adyen → Stripe)
|
|
91
|
+
|
|
92
|
+
**When to Use This Template:**
|
|
93
|
+
|
|
94
|
+
- ✅ Payment gateway integration (Adyen, Stripe, etc.)
|
|
95
|
+
- ✅ Payment operations triggered by Fluent Rubix workflows
|
|
96
|
+
- ✅ Need fast webhook response (don't wait for payment gateway API calls)
|
|
97
|
+
- ✅ Idempotency required (prevent duplicate charges/refunds)
|
|
98
|
+
- ✅ Multiple payment operations (Capture, Refund, Cancel, ReAuth)
|
|
99
|
+
|
|
100
|
+
**When NOT to Use:**
|
|
101
|
+
|
|
102
|
+
- ❌ Direct payment processing (use payment gateway SDK directly)
|
|
103
|
+
- ❌ Bulk payment operations (use Batch API or scheduled workflows)
|
|
104
|
+
- ❌ Need synchronous payment confirmation (wait for result before responding)
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 🎯 Flow Mapping & Trigger Sources
|
|
109
|
+
|
|
110
|
+
| Payment Operation | Trigger Source | When It Happens | Context |
|
|
111
|
+
|-------------------|----------------|------------------|---------|
|
|
112
|
+
| **Capture** | ORDER/FULFILLMENT Workflow | Order ready to ship | Initial flow - order moves to fulfillment |
|
|
113
|
+
| **Refund** | Payment Entity Orchestration | Post-purchase refund | After RMA processing, order cancellation |
|
|
114
|
+
| **Auth Cancel** | Payment Entity Orchestration | Pre-capture cancellation | Order cancelled before capture |
|
|
115
|
+
| **ReAuth** | Payment Entity Orchestration | Authorization renewal | Authorization expires or needs extension |
|
|
116
|
+
|
|
117
|
+
**Note**: All webhooks are called from Rubix workflows, which handle all business logic validation before calling endpoints.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 🔑 Key Principle: Rubix Workflow Validation
|
|
122
|
+
|
|
123
|
+
**IMPORTANT**: All webhooks are called from **Rubix workflows** within Fluent Commerce.
|
|
124
|
+
|
|
125
|
+
### What This Means
|
|
126
|
+
|
|
127
|
+
✅ **Rubix workflows handle ALL business logic validation**:
|
|
128
|
+
- Order status checks (is order ready to ship?)
|
|
129
|
+
- Payment state checks (is payment authorized? captured?)
|
|
130
|
+
- Entity existence checks (does order exist? does payment exist?)
|
|
131
|
+
- Business rules (can we refund? can we cancel?)
|
|
132
|
+
|
|
133
|
+
✅ **If Rubix workflow calls an endpoint, the entity is already in the correct state**:
|
|
134
|
+
- If Capture endpoint is called → Order is ready to ship, payment is authorized
|
|
135
|
+
- If Refund endpoint is called → Payment is captured, refund is approved
|
|
136
|
+
- If Cancel endpoint is called → Authorization exists, order is cancelled
|
|
137
|
+
- If ReAuth endpoint is called → Authorization exists, needs renewal
|
|
138
|
+
|
|
139
|
+
### What We Validate (Technical Only)
|
|
140
|
+
|
|
141
|
+
✅ **Payload structure** - Required fields present
|
|
142
|
+
✅ **Trigger source** - Correct Rubix workflow calling endpoint
|
|
143
|
+
✅ **Data format** - Amounts, currencies, references are valid
|
|
144
|
+
✅ **Idempotency** - Prevent duplicate operations
|
|
145
|
+
✅ **Signature** - Webhook authenticity (if configured)
|
|
146
|
+
|
|
147
|
+
### What We DON'T Validate
|
|
148
|
+
|
|
149
|
+
❌ **Entity status** - Rubix workflow ensures correct status
|
|
150
|
+
❌ **Payment state** - Rubix workflow ensures correct state
|
|
151
|
+
❌ **Business rules** - Rubix workflow enforces rules
|
|
152
|
+
❌ **Entity existence** - Rubix workflow verified before calling
|
|
153
|
+
|
|
154
|
+
### Why This Matters
|
|
155
|
+
|
|
156
|
+
- **Simpler code** - No complex business logic in webhook handlers
|
|
157
|
+
- **Single source of truth** - Rubix workflows own business logic
|
|
158
|
+
- **Reliability** - Rubix workflows ensure correct state before calling
|
|
159
|
+
- **Performance** - No redundant GraphQL queries to check entity status
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## STEP 2: Implementation Prompt for Claude Code
|
|
164
|
+
|
|
165
|
+
**Copy this prompt and send to Claude Code to generate the complete implementation:**
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
Create Versori webhook workflows for payment gateway integration (Adyen) to Fluent Commerce.
|
|
169
|
+
|
|
170
|
+
REQUIREMENTS:
|
|
171
|
+
1. Runtime: Versori Platform (HTTP webhooks)
|
|
172
|
+
2. Source: Payment operations via HTTP POST webhooks (JSON, from Fluent Rubix workflows)
|
|
173
|
+
3. Destination: Fluent Commerce Event API (sendEvent for payment events)
|
|
174
|
+
4. Format: JSON payment payload
|
|
175
|
+
5. Entity: Payment (Event API for event sending)
|
|
176
|
+
|
|
177
|
+
KEY FEATURES:
|
|
178
|
+
- Sync + fire-and-forget pattern (fast webhook response, background processing)
|
|
179
|
+
- External JSON configuration (config/payment-provider-config.json)
|
|
180
|
+
- Modular architecture (workflows/, services/, config/, types/)
|
|
181
|
+
- 4 webhooks: Capture, Refund, Auth Cancel, ReAuth
|
|
182
|
+
- Payment provider abstraction (easy to swap Adyen → Stripe)
|
|
183
|
+
- Idempotency handling (prevent duplicate operations)
|
|
184
|
+
- Webhook signature validation
|
|
185
|
+
- Trigger source validation (Rubix workflow validation)
|
|
186
|
+
|
|
187
|
+
CRITICAL REQUIREMENTS:
|
|
188
|
+
1. Webhook Mode: response: { mode: 'sync' } (fast response)
|
|
189
|
+
2. Background Processing: Fire-and-forget pattern (no await on long operations)
|
|
190
|
+
3. Provider Config: External JSON file (config/payment-provider-config.json)
|
|
191
|
+
4. Modular Structure: Separate services/, config/, types/ folders
|
|
192
|
+
5. Native Logging: Use log from context (no LoggingService)
|
|
193
|
+
6. Idempotency: VersoriKVAdapter for duplicate prevention
|
|
194
|
+
|
|
195
|
+
SDK METHODS TO USE:
|
|
196
|
+
- createClient({ ...ctx, log }) - Pass full Versori context
|
|
197
|
+
- client.setRetailerId(retailerId) - REQUIRED for Event API
|
|
198
|
+
- client.sendEvent(event) - Send payment events to Fluent
|
|
199
|
+
- new VersoriKVAdapter(openKv(':project:')) - Idempotency tracking
|
|
200
|
+
- new WebhookValidationService(log) - Webhook signature validation
|
|
201
|
+
|
|
202
|
+
FORBIDDEN PATTERNS:
|
|
203
|
+
- ❌ Inline config (use external JSON)
|
|
204
|
+
- ❌ await on background processing (use fire-and-forget)
|
|
205
|
+
- ❌ LoggingService (use native log from context)
|
|
206
|
+
- ❌ All code in one file (use modular structure)
|
|
207
|
+
- ❌ async mode webhook (use sync + fire-and-forget)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## STEP 3: Detailed Flow Documentation
|
|
213
|
+
|
|
214
|
+
### Complete Processing Flow (Payment Capture Example)
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
218
|
+
│ 1. WEBHOOK RECEIVED (from Rubix Workflow) │
|
|
219
|
+
│ POST https://{workspace}.versori.run/payment-capture │
|
|
220
|
+
│ Content-Type: application/json │
|
|
221
|
+
│ Body: { paymentReference: "...", amount: 100, ... } │
|
|
222
|
+
└────────────────────┬────────────────────────────────────────┘
|
|
223
|
+
│
|
|
224
|
+
▼
|
|
225
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
226
|
+
│ 2. QUICK VALIDATION (Synchronous, ~10-50ms) │
|
|
227
|
+
│ - Check fluent_commerce connection exists │
|
|
228
|
+
│ - Validate payment payload present │
|
|
229
|
+
│ - Validate webhook signature (if configured) │
|
|
230
|
+
│ - Validate trigger source (ORDER_FULFILLMENT) │
|
|
231
|
+
│ - Check idempotency (prevent duplicates) │
|
|
232
|
+
│ - Return HTTP 200 OK immediately │
|
|
233
|
+
└────────────────────┬────────────────────────────────────────┘
|
|
234
|
+
│
|
|
235
|
+
▼
|
|
236
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
237
|
+
│ 3. BACKGROUND PROCESSING (Fire-and-Forget) │
|
|
238
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
239
|
+
│ │ 3a. Initialize Fluent Client │ │
|
|
240
|
+
│ │ - createClient({ ...ctx, log }) │ │
|
|
241
|
+
│ │ - setRetailerId(retailerId) │ │
|
|
242
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
243
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
244
|
+
│ │ 3b. Call Payment Gateway API │ │
|
|
245
|
+
│ │ - Adyen Capture API call │ │
|
|
246
|
+
│ │ - Handle payment gateway response │ │
|
|
247
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
248
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
249
|
+
│ │ 3c. Send Event to Fluent │ │
|
|
250
|
+
│ │ - Map gateway response to Fluent event format │ │
|
|
251
|
+
│ │ - client.sendEvent(event) │ │
|
|
252
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
253
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
254
|
+
│ │ 3d. Update Idempotency Record │ │
|
|
255
|
+
│ │ - Store successful operation in KV │ │
|
|
256
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
257
|
+
└─────────────────────────────────────────────────────────────┘
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Response Timing
|
|
261
|
+
|
|
262
|
+
| Stage | Timing | Blocking |
|
|
263
|
+
|-------|--------|----------|
|
|
264
|
+
| **Webhook Validation** | ~10-50ms | ✅ Yes (blocks response) |
|
|
265
|
+
| **Background Processing** | ~1000-3000ms | ❌ No (fire-and-forget) |
|
|
266
|
+
| **Total Response Time** | ~10-50ms | ✅ Fast response |
|
|
267
|
+
|
|
268
|
+
**Key Benefit**: Rubix workflow receives immediate acknowledgment (~50ms) while payment processing happens in background (~1-3s).
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## STEP 4: Production Modular Structure
|
|
273
|
+
|
|
274
|
+
> **✅ This section shows the COMPLETE production-ready modular structure.**
|
|
275
|
+
> All files are shown with proper imports/exports and folder organization.
|
|
276
|
+
|
|
277
|
+
### Complete Project Structure
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
payment-gateway-integration/
|
|
281
|
+
├── package.json # Dependencies and Versori config
|
|
282
|
+
├── index.ts # Entry point - exports all workflows
|
|
283
|
+
└── src/
|
|
284
|
+
├── workflows/
|
|
285
|
+
│ └── webhook/
|
|
286
|
+
│ ├── payment-capture.ts # Webhook: Capture payment
|
|
287
|
+
│ ├── payment-refund.ts # Webhook: Refund payment
|
|
288
|
+
│ ├── payment-auth-cancel.ts # Webhook: Cancel authorization
|
|
289
|
+
│ └── payment-reauth.ts # Webhook: Re-authorize payment
|
|
290
|
+
│
|
|
291
|
+
├── services/
|
|
292
|
+
│ ├── payment-provider.service.ts # Payment provider abstraction
|
|
293
|
+
│ ├── payment-event.service.ts # Event sending to Fluent
|
|
294
|
+
│ ├── idempotency.service.ts # Idempotency handling
|
|
295
|
+
│ └── webhook-validation.service.ts # Webhook signature validation
|
|
296
|
+
│
|
|
297
|
+
├── config/
|
|
298
|
+
│ └── payment-provider-config.json # Provider configuration (external JSON)
|
|
299
|
+
│
|
|
300
|
+
└── types/
|
|
301
|
+
└── payment.types.ts # TypeScript interfaces
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Why This Structure?**
|
|
305
|
+
|
|
306
|
+
- ✅ **Clear separation**: Webhook handlers vs business logic vs provider abstraction
|
|
307
|
+
- ✅ **Reusable services**: Payment logic can be reused across webhooks
|
|
308
|
+
- ✅ **External config**: Provider configuration changes don't require code changes
|
|
309
|
+
- ✅ **Provider abstraction**: Easy to swap payment providers (Adyen → Stripe)
|
|
310
|
+
- ✅ **Type safety**: TypeScript interfaces for better IDE support
|
|
311
|
+
- ✅ **Scalable**: Easy to add new payment operations or providers
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## SDK Methods Used
|
|
316
|
+
|
|
317
|
+
- `webhook(name, handler)` - HTTP webhook endpoint (from `@versori/run`)
|
|
318
|
+
- `createClient(ctx)` - Auto-detects Versori context, creates FluentClient
|
|
319
|
+
- `client.setRetailerId()` - REQUIRED for Event API (after createClient)
|
|
320
|
+
- `client.sendEvent()` - Send events to Fluent Commerce Event API
|
|
321
|
+
- `VersoriKVAdapter(ctx.openKv(':project:'))` - Idempotency tracking
|
|
322
|
+
- Native Versori log from context (no console.log, no LoggingService)
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Architecture Overview
|
|
327
|
+
|
|
328
|
+
### Modular Design Pattern
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
┌─────────────────────────────────────────────────────────┐
|
|
332
|
+
│ Webhook Layer (4 webhooks) │
|
|
333
|
+
│ - adyen-capture │
|
|
334
|
+
│ - adyen-refund │
|
|
335
|
+
│ - adyen-auth-cancel │
|
|
336
|
+
│ - adyen-reauth │
|
|
337
|
+
└─────────────────────────────────────────────────────────┘
|
|
338
|
+
↓
|
|
339
|
+
┌─────────────────────────────────────────────────────────┐
|
|
340
|
+
│ Shared Services (Reusable) │
|
|
341
|
+
│ - WebhookValidationService │
|
|
342
|
+
│ - PaymentEventService │
|
|
343
|
+
│ - IdempotencyService │
|
|
344
|
+
└─────────────────────────────────────────────────────────┘
|
|
345
|
+
↓
|
|
346
|
+
┌─────────────────────────────────────────────────────────┐
|
|
347
|
+
│ Payment Provider Abstraction │
|
|
348
|
+
│ - PaymentProviderService (Interface) │
|
|
349
|
+
│ - AdyenPaymentProviderService (Implementation) │
|
|
350
|
+
│ - StripePaymentProviderService (Future) │
|
|
351
|
+
└─────────────────────────────────────────────────────────┘
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Key Benefits
|
|
355
|
+
|
|
356
|
+
✅ **Single Connector** - All 4 flows in one document/workflow
|
|
357
|
+
✅ **Provider Agnostic** - Easy to swap Adyen → Stripe (change one service)
|
|
358
|
+
✅ **DRY Principle** - Shared services used by all webhooks
|
|
359
|
+
✅ **Maintainable** - Clear separation of concerns
|
|
360
|
+
✅ **Testable** - Each layer can be tested independently
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Versori Workflows Structure
|
|
365
|
+
|
|
366
|
+
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
367
|
+
|
|
368
|
+
**Trigger Types:**
|
|
369
|
+
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
370
|
+
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
371
|
+
- **`http()`** → External API calls (chained from webhook/schedule)
|
|
372
|
+
- **`fn()`** → Internal processing (chained from webhook/schedule)
|
|
373
|
+
|
|
374
|
+
### Recommended Project Structure
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
payment-gateway-integration/
|
|
378
|
+
├── index.ts # Entry point - exports all workflows
|
|
379
|
+
└── src/
|
|
380
|
+
├── workflows/
|
|
381
|
+
│ └── webhook/
|
|
382
|
+
│ ├── payment-capture.ts # Webhook: Capture payment
|
|
383
|
+
│ ├── payment-refund.ts # Webhook: Refund payment
|
|
384
|
+
│ ├── payment-auth-cancel.ts # Webhook: Cancel authorization
|
|
385
|
+
│ └── payment-reauth.ts # Webhook: Re-authorize payment
|
|
386
|
+
│
|
|
387
|
+
├── services/
|
|
388
|
+
│ ├── payment-provider.service.ts # Payment provider abstraction (interface)
|
|
389
|
+
│ ├── adyen-provider.service.ts # Adyen implementation
|
|
390
|
+
│ ├── payment-event.service.ts # Fluent Event API service
|
|
391
|
+
│ ├── webhook-validation.service.ts # Webhook validation & security
|
|
392
|
+
│ └── idempotency.service.ts # Idempotency tracking
|
|
393
|
+
│
|
|
394
|
+
└── config/
|
|
395
|
+
├── adyen-config.json # Adyen-specific configuration
|
|
396
|
+
└── event-mapping.json # Fluent event mapping config
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Project Setup
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
mkdir versori-payment-gateway-integration && cd $_
|
|
405
|
+
npm init -y
|
|
406
|
+
npm install @fluentcommerce/fc-connect-sdk@latest @versori/run
|
|
407
|
+
mkdir -p src/{workflows/webhook,services,config}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Package Configuration (package.json)
|
|
411
|
+
|
|
412
|
+
```json
|
|
413
|
+
{
|
|
414
|
+
"name": "versori-payment-gateway-integration",
|
|
415
|
+
"version": "1.0.0",
|
|
416
|
+
"versori": {
|
|
417
|
+
"workflows": "./src/index.ts"
|
|
418
|
+
},
|
|
419
|
+
"type": "module",
|
|
420
|
+
"scripts": {
|
|
421
|
+
"deploy": "versori deploy",
|
|
422
|
+
"logs": "versori logs"
|
|
423
|
+
},
|
|
424
|
+
"dependencies": {
|
|
425
|
+
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
426
|
+
"@versori/run": "latest"
|
|
427
|
+
},
|
|
428
|
+
"devDependencies": {
|
|
429
|
+
"typescript": "^5.0.0",
|
|
430
|
+
"@types/node": "^20.0.0"
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## Activation Variables
|
|
438
|
+
|
|
439
|
+
Configure these in Versori Activation Variables:
|
|
440
|
+
|
|
441
|
+
```bash
|
|
442
|
+
# Fluent Commerce Configuration
|
|
443
|
+
fluentRetailerId=your-retailer-id
|
|
444
|
+
|
|
445
|
+
# Payment Provider Configuration
|
|
446
|
+
paymentProvider=adyen # adyen | stripe | paypal (future)
|
|
447
|
+
paymentProviderEnvironment=test # test | live
|
|
448
|
+
|
|
449
|
+
# Adyen-Specific Configuration
|
|
450
|
+
adyenMerchantAccount=YourMerchantAccount
|
|
451
|
+
|
|
452
|
+
# Stripe-Specific Configuration (if using Stripe)
|
|
453
|
+
# stripeSecretKey=sk_test_...
|
|
454
|
+
# stripePublishableKey=pk_test_...
|
|
455
|
+
|
|
456
|
+
# Webhook Security
|
|
457
|
+
webhookSecret=your-shared-secret-for-signature-validation
|
|
458
|
+
|
|
459
|
+
# Trigger Source Validation (Optional but Recommended)
|
|
460
|
+
validateTriggerSource=true
|
|
461
|
+
allowedTriggerSources=ORDER_FULFILLMENT,PAYMENT_ENTITY
|
|
462
|
+
|
|
463
|
+
# Optional: Retry Configuration
|
|
464
|
+
enableRetry=true
|
|
465
|
+
maxRetries=3
|
|
466
|
+
retryDelayMs=1000
|
|
467
|
+
|
|
468
|
+
# Optional: Feature Flags
|
|
469
|
+
enableIdempotency=true
|
|
470
|
+
enableEventLogging=true
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
## Versori Connections Setup
|
|
476
|
+
|
|
477
|
+
### Connection 1: Fluent Commerce (OAuth2)
|
|
478
|
+
|
|
479
|
+
**Name**: `fluent_commerce`
|
|
480
|
+
|
|
481
|
+
**Type**: OAuth2
|
|
482
|
+
|
|
483
|
+
**Configuration**:
|
|
484
|
+
- `base_url`: Your Fluent Commerce API URL (e.g., `https://api.fluentcommerce.com`)
|
|
485
|
+
- `client_id`: OAuth2 client ID
|
|
486
|
+
- `client_secret`: OAuth2 client secret
|
|
487
|
+
- `username`: Your Fluent Commerce username
|
|
488
|
+
- `password`: Your Fluent Commerce password
|
|
489
|
+
|
|
490
|
+
**Note**: SDK auto-detects and uses this connection via `createClient(ctx)`
|
|
491
|
+
|
|
492
|
+
### Connection 2: Payment Gateway (API Key or OAuth2)
|
|
493
|
+
|
|
494
|
+
**Name**: `payment_gateway` (or `adyen_payment_gateway`, `stripe_payment_gateway`)
|
|
495
|
+
|
|
496
|
+
**Type**: API Key (for Adyen) or OAuth2 (for Stripe)
|
|
497
|
+
|
|
498
|
+
**Configuration**:
|
|
499
|
+
- **Adyen**: `api_key` - Your Adyen API key
|
|
500
|
+
- **Stripe**: `secret_key` - Your Stripe secret key
|
|
501
|
+
|
|
502
|
+
**Alternative**: Store API key in activation variable `paymentGatewayApiKey` if preferred
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
## Complete Working Code
|
|
507
|
+
|
|
508
|
+
### File: `src/index.ts`
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
/**
|
|
512
|
+
* ═══════════════════════════════════════════════════════════════
|
|
513
|
+
* 🚀 VERSORI PAYMENT GATEWAY INTEGRATION - ENTRY POINT
|
|
514
|
+
* ═══════════════════════════════════════════════════════════════
|
|
515
|
+
*
|
|
516
|
+
* Entry Point - Registers all payment webhooks with Versori platform
|
|
517
|
+
*
|
|
518
|
+
* Pattern: MemoryInterpreter
|
|
519
|
+
* - Export all workflows
|
|
520
|
+
* - Clean separation of concerns
|
|
521
|
+
* - Easy to add new workflows
|
|
522
|
+
*/
|
|
523
|
+
|
|
524
|
+
import { MemoryInterpreter } from '@versori/run';
|
|
525
|
+
import { paymentCapture } from './workflows/webhook/payment-capture';
|
|
526
|
+
import { paymentRefund } from './workflows/webhook/payment-refund';
|
|
527
|
+
import { paymentAuthCancel } from './workflows/webhook/payment-auth-cancel';
|
|
528
|
+
import { paymentReauth } from './workflows/webhook/payment-reauth';
|
|
529
|
+
|
|
530
|
+
async function main(): Promise<void> {
|
|
531
|
+
const mi = await MemoryInterpreter.newInstance();
|
|
532
|
+
|
|
533
|
+
// Register all payment webhook workflows
|
|
534
|
+
mi.register(paymentCapture);
|
|
535
|
+
mi.register(paymentRefund);
|
|
536
|
+
mi.register(paymentAuthCancel);
|
|
537
|
+
mi.register(paymentReauth);
|
|
538
|
+
|
|
539
|
+
await mi.start();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
main().then().catch((err) => console.error('Failed to run main()', err));
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
### File: `src/services/payment-provider.service.ts`
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
/**
|
|
551
|
+
* Payment Provider Service Interface
|
|
552
|
+
*
|
|
553
|
+
* Abstract interface for payment gateway operations
|
|
554
|
+
* Allows easy swapping of payment providers (Adyen → Stripe → PayPal)
|
|
555
|
+
*/
|
|
556
|
+
|
|
557
|
+
import type { Logger } from '@fluentcommerce/fc-connect-sdk';
|
|
558
|
+
|
|
559
|
+
export interface PaymentAmount {
|
|
560
|
+
value: number; // Amount in minor units (e.g., 1000 = $10.00)
|
|
561
|
+
currency: string; // ISO 4217 currency code (e.g., "USD")
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export interface PaymentContext {
|
|
565
|
+
orderRef: string;
|
|
566
|
+
paymentReference: string;
|
|
567
|
+
retailerId?: string;
|
|
568
|
+
merchantReference?: string;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export interface CaptureResult {
|
|
572
|
+
success: boolean;
|
|
573
|
+
paymentReference: string;
|
|
574
|
+
amount: PaymentAmount;
|
|
575
|
+
capturedAt: string;
|
|
576
|
+
resultCode?: string;
|
|
577
|
+
error?: string;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export interface RefundResult {
|
|
581
|
+
success: boolean;
|
|
582
|
+
refundReference: string;
|
|
583
|
+
originalPaymentReference: string;
|
|
584
|
+
amount: PaymentAmount;
|
|
585
|
+
refundedAt: string;
|
|
586
|
+
refundType?: 'FULL' | 'PARTIAL';
|
|
587
|
+
error?: string;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export interface CancelResult {
|
|
591
|
+
success: boolean;
|
|
592
|
+
cancellationReference: string;
|
|
593
|
+
originalPaymentReference: string;
|
|
594
|
+
cancelledAt: string;
|
|
595
|
+
error?: string;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export interface ReauthResult {
|
|
599
|
+
success: boolean;
|
|
600
|
+
reauthReference: string;
|
|
601
|
+
originalPaymentReference: string;
|
|
602
|
+
amount: PaymentAmount;
|
|
603
|
+
reauthorizedAt: string;
|
|
604
|
+
expiryDate?: string;
|
|
605
|
+
error?: string;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Payment Provider Service Interface
|
|
610
|
+
*
|
|
611
|
+
* Implement this interface for each payment provider:
|
|
612
|
+
* - AdyenPaymentProviderService
|
|
613
|
+
* - StripePaymentProviderService
|
|
614
|
+
* - PayPalPaymentProviderService
|
|
615
|
+
*/
|
|
616
|
+
export interface PaymentProviderService {
|
|
617
|
+
/**
|
|
618
|
+
* Capture an authorized payment
|
|
619
|
+
*/
|
|
620
|
+
capturePayment(
|
|
621
|
+
paymentReference: string,
|
|
622
|
+
amount: PaymentAmount,
|
|
623
|
+
context: PaymentContext
|
|
624
|
+
): Promise<CaptureResult>;
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Refund a captured payment
|
|
628
|
+
*/
|
|
629
|
+
refundPayment(
|
|
630
|
+
paymentReference: string,
|
|
631
|
+
amount: PaymentAmount,
|
|
632
|
+
context: PaymentContext
|
|
633
|
+
): Promise<RefundResult>;
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Cancel an authorization
|
|
637
|
+
*/
|
|
638
|
+
cancelAuthorization(
|
|
639
|
+
paymentReference: string,
|
|
640
|
+
context: PaymentContext
|
|
641
|
+
): Promise<CancelResult>;
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Re-authorize a payment
|
|
645
|
+
*/
|
|
646
|
+
reauthorizePayment(
|
|
647
|
+
paymentReference: string,
|
|
648
|
+
amount: PaymentAmount,
|
|
649
|
+
context: PaymentContext
|
|
650
|
+
): Promise<ReauthResult>;
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Get provider name (for logging/debugging)
|
|
654
|
+
*/
|
|
655
|
+
getProviderName(): string;
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
---
|
|
660
|
+
|
|
661
|
+
### File: `src/services/adyen-provider.service.ts`
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
/**
|
|
665
|
+
* Adyen Payment Provider Service Implementation
|
|
666
|
+
*
|
|
667
|
+
* Implements PaymentProviderService for Adyen payment gateway
|
|
668
|
+
*/
|
|
669
|
+
|
|
670
|
+
import type { Logger } from '@fluentcommerce/fc-connect-sdk';
|
|
671
|
+
import type {
|
|
672
|
+
PaymentProviderService,
|
|
673
|
+
PaymentAmount,
|
|
674
|
+
PaymentContext,
|
|
675
|
+
CaptureResult,
|
|
676
|
+
RefundResult,
|
|
677
|
+
CancelResult,
|
|
678
|
+
ReauthResult,
|
|
679
|
+
} from './payment-provider.service';
|
|
680
|
+
|
|
681
|
+
export class AdyenPaymentProviderService implements PaymentProviderService {
|
|
682
|
+
constructor(
|
|
683
|
+
private apiKey: string,
|
|
684
|
+
private environment: 'test' | 'live',
|
|
685
|
+
private merchantAccount: string,
|
|
686
|
+
private log: Logger
|
|
687
|
+
) {}
|
|
688
|
+
|
|
689
|
+
getProviderName(): string {
|
|
690
|
+
return 'Adyen';
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Get Adyen API base URL based on environment
|
|
695
|
+
*/
|
|
696
|
+
private getApiUrl(): string {
|
|
697
|
+
return this.environment === 'live'
|
|
698
|
+
? 'https://pal-live.adyen.com/pal/servlet/Payment'
|
|
699
|
+
: 'https://pal-test.adyen.com/pal/servlet/Payment';
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Capture an authorized payment
|
|
704
|
+
*/
|
|
705
|
+
async capturePayment(
|
|
706
|
+
paymentReference: string,
|
|
707
|
+
amount: PaymentAmount,
|
|
708
|
+
context: PaymentContext
|
|
709
|
+
): Promise<CaptureResult> {
|
|
710
|
+
const url = `${this.getApiUrl()}/v68/payments/${paymentReference}/capture`;
|
|
711
|
+
|
|
712
|
+
this.log.info('🔧 [Adyen] Calling capture API', {
|
|
713
|
+
paymentReference,
|
|
714
|
+
amount: amount.value,
|
|
715
|
+
currency: amount.currency,
|
|
716
|
+
merchantAccount: this.merchantAccount,
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
try {
|
|
720
|
+
const response = await fetch(url, {
|
|
721
|
+
method: 'POST',
|
|
722
|
+
headers: {
|
|
723
|
+
'Content-Type': 'application/json',
|
|
724
|
+
'X-API-Key': this.apiKey,
|
|
725
|
+
},
|
|
726
|
+
body: JSON.stringify({
|
|
727
|
+
amount: amount,
|
|
728
|
+
merchantAccount: this.merchantAccount,
|
|
729
|
+
}),
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
const responseData = await response.json();
|
|
733
|
+
|
|
734
|
+
if (!response.ok) {
|
|
735
|
+
const error = responseData.error || {
|
|
736
|
+
errorCode: 'UNKNOWN_ERROR',
|
|
737
|
+
errorType: 'API_ERROR',
|
|
738
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
this.log.error('❌ [Adyen] Capture API error', {
|
|
742
|
+
paymentReference,
|
|
743
|
+
errorCode: error.errorCode,
|
|
744
|
+
message: error.message,
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
return {
|
|
748
|
+
success: false,
|
|
749
|
+
paymentReference: paymentReference,
|
|
750
|
+
amount: amount,
|
|
751
|
+
capturedAt: new Date().toISOString(),
|
|
752
|
+
error: `${error.errorCode}: ${error.message}`,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
this.log.info('✅ [Adyen] Capture successful', {
|
|
757
|
+
pspReference: responseData.pspReference,
|
|
758
|
+
resultCode: responseData.resultCode,
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
return {
|
|
762
|
+
success: true,
|
|
763
|
+
paymentReference: responseData.pspReference,
|
|
764
|
+
amount: responseData.amount || amount,
|
|
765
|
+
capturedAt: responseData.eventDate || new Date().toISOString(),
|
|
766
|
+
resultCode: responseData.resultCode,
|
|
767
|
+
};
|
|
768
|
+
} catch (error: any) {
|
|
769
|
+
this.log.error('❌ [Adyen] Capture API call failed', {
|
|
770
|
+
paymentReference,
|
|
771
|
+
error: error.message,
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
return {
|
|
775
|
+
success: false,
|
|
776
|
+
paymentReference: paymentReference,
|
|
777
|
+
amount: amount,
|
|
778
|
+
capturedAt: new Date().toISOString(),
|
|
779
|
+
error: error.message,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Refund a captured payment
|
|
786
|
+
*/
|
|
787
|
+
async refundPayment(
|
|
788
|
+
paymentReference: string,
|
|
789
|
+
amount: PaymentAmount,
|
|
790
|
+
context: PaymentContext
|
|
791
|
+
): Promise<RefundResult> {
|
|
792
|
+
const url = `${this.getApiUrl()}/v68/payments/${paymentReference}/refunds`;
|
|
793
|
+
|
|
794
|
+
this.log.info('🔧 [Adyen] Calling refund API', {
|
|
795
|
+
paymentReference,
|
|
796
|
+
amount: amount.value,
|
|
797
|
+
currency: amount.currency,
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
try {
|
|
801
|
+
const response = await fetch(url, {
|
|
802
|
+
method: 'POST',
|
|
803
|
+
headers: {
|
|
804
|
+
'Content-Type': 'application/json',
|
|
805
|
+
'X-API-Key': this.apiKey,
|
|
806
|
+
},
|
|
807
|
+
body: JSON.stringify({
|
|
808
|
+
amount: amount,
|
|
809
|
+
merchantAccount: this.merchantAccount,
|
|
810
|
+
reference: context.merchantReference || context.orderRef,
|
|
811
|
+
}),
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
const responseData = await response.json();
|
|
815
|
+
|
|
816
|
+
if (!response.ok) {
|
|
817
|
+
const error = responseData.error || {
|
|
818
|
+
errorCode: 'UNKNOWN_ERROR',
|
|
819
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
return {
|
|
823
|
+
success: false,
|
|
824
|
+
refundReference: '',
|
|
825
|
+
originalPaymentReference: paymentReference,
|
|
826
|
+
amount: amount,
|
|
827
|
+
refundedAt: new Date().toISOString(),
|
|
828
|
+
error: `${error.errorCode}: ${error.message}`,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Determine refund type (full vs partial)
|
|
833
|
+
const refundType = responseData.refundType || 'FULL';
|
|
834
|
+
|
|
835
|
+
return {
|
|
836
|
+
success: true,
|
|
837
|
+
refundReference: responseData.pspReference,
|
|
838
|
+
originalPaymentReference: paymentReference,
|
|
839
|
+
amount: responseData.amount || amount,
|
|
840
|
+
refundedAt: responseData.eventDate || new Date().toISOString(),
|
|
841
|
+
refundType: refundType as 'FULL' | 'PARTIAL',
|
|
842
|
+
};
|
|
843
|
+
} catch (error: any) {
|
|
844
|
+
return {
|
|
845
|
+
success: false,
|
|
846
|
+
refundReference: '',
|
|
847
|
+
originalPaymentReference: paymentReference,
|
|
848
|
+
amount: amount,
|
|
849
|
+
refundedAt: new Date().toISOString(),
|
|
850
|
+
error: error.message,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Cancel an authorization
|
|
857
|
+
*/
|
|
858
|
+
async cancelAuthorization(
|
|
859
|
+
paymentReference: string,
|
|
860
|
+
context: PaymentContext
|
|
861
|
+
): Promise<CancelResult> {
|
|
862
|
+
const url = `${this.getApiUrl()}/v68/payments/${paymentReference}/cancels`;
|
|
863
|
+
|
|
864
|
+
this.log.info('🔧 [Adyen] Calling cancel API', {
|
|
865
|
+
paymentReference,
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
try {
|
|
869
|
+
const response = await fetch(url, {
|
|
870
|
+
method: 'POST',
|
|
871
|
+
headers: {
|
|
872
|
+
'Content-Type': 'application/json',
|
|
873
|
+
'X-API-Key': this.apiKey,
|
|
874
|
+
},
|
|
875
|
+
body: JSON.stringify({
|
|
876
|
+
merchantAccount: this.merchantAccount,
|
|
877
|
+
reference: context.merchantReference || context.orderRef,
|
|
878
|
+
}),
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
const responseData = await response.json();
|
|
882
|
+
|
|
883
|
+
if (!response.ok) {
|
|
884
|
+
const error = responseData.error || {
|
|
885
|
+
errorCode: 'UNKNOWN_ERROR',
|
|
886
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
return {
|
|
890
|
+
success: false,
|
|
891
|
+
cancellationReference: '',
|
|
892
|
+
originalPaymentReference: paymentReference,
|
|
893
|
+
cancelledAt: new Date().toISOString(),
|
|
894
|
+
error: `${error.errorCode}: ${error.message}`,
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return {
|
|
899
|
+
success: true,
|
|
900
|
+
cancellationReference: responseData.pspReference,
|
|
901
|
+
originalPaymentReference: paymentReference,
|
|
902
|
+
cancelledAt: responseData.eventDate || new Date().toISOString(),
|
|
903
|
+
};
|
|
904
|
+
} catch (error: any) {
|
|
905
|
+
return {
|
|
906
|
+
success: false,
|
|
907
|
+
cancellationReference: '',
|
|
908
|
+
originalPaymentReference: paymentReference,
|
|
909
|
+
cancelledAt: new Date().toISOString(),
|
|
910
|
+
error: error.message,
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Re-authorize a payment
|
|
917
|
+
*/
|
|
918
|
+
async reauthorizePayment(
|
|
919
|
+
paymentReference: string,
|
|
920
|
+
amount: PaymentAmount,
|
|
921
|
+
context: PaymentContext
|
|
922
|
+
): Promise<ReauthResult> {
|
|
923
|
+
const url = `${this.getApiUrl()}/v68/payments/${paymentReference}/reauthorise`;
|
|
924
|
+
|
|
925
|
+
this.log.info('🔧 [Adyen] Calling reauthorize API', {
|
|
926
|
+
paymentReference,
|
|
927
|
+
amount: amount.value,
|
|
928
|
+
currency: amount.currency,
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
try {
|
|
932
|
+
const response = await fetch(url, {
|
|
933
|
+
method: 'POST',
|
|
934
|
+
headers: {
|
|
935
|
+
'Content-Type': 'application/json',
|
|
936
|
+
'X-API-Key': this.apiKey,
|
|
937
|
+
},
|
|
938
|
+
body: JSON.stringify({
|
|
939
|
+
amount: amount,
|
|
940
|
+
merchantAccount: this.merchantAccount,
|
|
941
|
+
reference: context.merchantReference || context.orderRef,
|
|
942
|
+
}),
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
const responseData = await response.json();
|
|
946
|
+
|
|
947
|
+
if (!response.ok) {
|
|
948
|
+
const error = responseData.error || {
|
|
949
|
+
errorCode: 'UNKNOWN_ERROR',
|
|
950
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
return {
|
|
954
|
+
success: false,
|
|
955
|
+
reauthReference: '',
|
|
956
|
+
originalPaymentReference: paymentReference,
|
|
957
|
+
amount: amount,
|
|
958
|
+
reauthorizedAt: new Date().toISOString(),
|
|
959
|
+
error: `${error.errorCode}: ${error.message}`,
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
return {
|
|
964
|
+
success: true,
|
|
965
|
+
reauthReference: responseData.pspReference,
|
|
966
|
+
originalPaymentReference: paymentReference,
|
|
967
|
+
amount: responseData.amount || amount,
|
|
968
|
+
reauthorizedAt: responseData.eventDate || new Date().toISOString(),
|
|
969
|
+
expiryDate: responseData.expiryDate,
|
|
970
|
+
};
|
|
971
|
+
} catch (error: any) {
|
|
972
|
+
return {
|
|
973
|
+
success: false,
|
|
974
|
+
reauthReference: '',
|
|
975
|
+
originalPaymentReference: paymentReference,
|
|
976
|
+
amount: amount,
|
|
977
|
+
reauthorizedAt: new Date().toISOString(),
|
|
978
|
+
error: error.message,
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
---
|
|
986
|
+
|
|
987
|
+
### File: `src/services/webhook-validation.service.ts`
|
|
988
|
+
|
|
989
|
+
```typescript
|
|
990
|
+
/**
|
|
991
|
+
* Webhook Validation Service
|
|
992
|
+
*
|
|
993
|
+
* Validates incoming webhooks from Fluent Workflow
|
|
994
|
+
* - Signature validation (HMAC-SHA256)
|
|
995
|
+
* - Payload structure validation
|
|
996
|
+
* - Required field checking
|
|
997
|
+
*/
|
|
998
|
+
|
|
999
|
+
import { createHmac } from 'node:crypto';
|
|
1000
|
+
import type { Logger } from '@fluentcommerce/fc-connect-sdk';
|
|
1001
|
+
|
|
1002
|
+
export interface WebhookPayload {
|
|
1003
|
+
orderRef: string;
|
|
1004
|
+
paymentReference: string;
|
|
1005
|
+
amount: {
|
|
1006
|
+
value: number;
|
|
1007
|
+
currency: string;
|
|
1008
|
+
};
|
|
1009
|
+
retailerId?: string;
|
|
1010
|
+
merchantReference?: string;
|
|
1011
|
+
triggerSource?: string; // ORDER_FULFILLMENT | PAYMENT_ENTITY
|
|
1012
|
+
orderStatus?: string; // For ORDER_FULFILLMENT triggers
|
|
1013
|
+
refundReason?: string; // For Payment Entity refunds
|
|
1014
|
+
cancelReason?: string; // For Payment Entity cancellations
|
|
1015
|
+
reauthReason?: string; // For Payment Entity reauthorizations
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
export interface ValidationResult {
|
|
1019
|
+
valid: boolean;
|
|
1020
|
+
error?: string;
|
|
1021
|
+
payload?: WebhookPayload;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
export class WebhookValidationService {
|
|
1025
|
+
constructor(private log: Logger) {}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Validate webhook signature using HMAC-SHA256
|
|
1029
|
+
*/
|
|
1030
|
+
validateSignature(
|
|
1031
|
+
payload: string,
|
|
1032
|
+
signature: string,
|
|
1033
|
+
secret: string
|
|
1034
|
+
): boolean {
|
|
1035
|
+
if (!signature || !secret) {
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const hmac = createHmac('sha256', secret);
|
|
1040
|
+
hmac.update(payload);
|
|
1041
|
+
const expectedSignature = hmac.digest('hex');
|
|
1042
|
+
|
|
1043
|
+
// Constant-time comparison to prevent timing attacks
|
|
1044
|
+
if (signature.length !== expectedSignature.length) {
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
let match = true;
|
|
1049
|
+
for (let i = 0; i < signature.length; i++) {
|
|
1050
|
+
if (signature[i] !== expectedSignature[i]) {
|
|
1051
|
+
match = false;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
return match;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Validate webhook payload structure
|
|
1060
|
+
*/
|
|
1061
|
+
validatePayload(payload: any): ValidationResult {
|
|
1062
|
+
const requiredFields = ['orderRef', 'paymentReference', 'amount'];
|
|
1063
|
+
const missingFields = requiredFields.filter((field) => !payload[field]);
|
|
1064
|
+
|
|
1065
|
+
if (missingFields.length > 0) {
|
|
1066
|
+
return {
|
|
1067
|
+
valid: false,
|
|
1068
|
+
error: `Missing required fields: ${missingFields.join(', ')}`,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (!payload.amount || typeof payload.amount.value !== 'number') {
|
|
1073
|
+
return {
|
|
1074
|
+
valid: false,
|
|
1075
|
+
error: 'Invalid amount structure: amount.value must be a number',
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (!payload.amount.currency || typeof payload.amount.currency !== 'string') {
|
|
1080
|
+
return {
|
|
1081
|
+
valid: false,
|
|
1082
|
+
error: 'Invalid amount structure: amount.currency must be a string',
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (payload.amount.value <= 0) {
|
|
1087
|
+
return {
|
|
1088
|
+
valid: false,
|
|
1089
|
+
error: 'Invalid amount: value must be positive',
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (typeof payload.paymentReference !== 'string' || payload.paymentReference.length === 0) {
|
|
1094
|
+
return {
|
|
1095
|
+
valid: false,
|
|
1096
|
+
error: 'Invalid paymentReference: must be a non-empty string',
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return {
|
|
1101
|
+
valid: true,
|
|
1102
|
+
payload: {
|
|
1103
|
+
orderRef: payload.orderRef,
|
|
1104
|
+
paymentReference: payload.paymentReference,
|
|
1105
|
+
amount: {
|
|
1106
|
+
value: payload.amount.value,
|
|
1107
|
+
currency: payload.amount.currency,
|
|
1108
|
+
},
|
|
1109
|
+
retailerId: payload.retailerId,
|
|
1110
|
+
merchantReference: payload.merchantReference,
|
|
1111
|
+
},
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/**
|
|
1116
|
+
* Extract payment context from validated payload
|
|
1117
|
+
*/
|
|
1118
|
+
extractPaymentContext(payload: WebhookPayload) {
|
|
1119
|
+
return {
|
|
1120
|
+
orderRef: payload.orderRef,
|
|
1121
|
+
paymentReference: payload.paymentReference,
|
|
1122
|
+
amount: payload.amount,
|
|
1123
|
+
retailerId: payload.retailerId,
|
|
1124
|
+
merchantReference: payload.merchantReference || payload.orderRef,
|
|
1125
|
+
triggerSource: payload.triggerSource,
|
|
1126
|
+
orderStatus: payload.orderStatus,
|
|
1127
|
+
refundReason: payload.refundReason,
|
|
1128
|
+
cancelReason: payload.cancelReason,
|
|
1129
|
+
reauthReason: payload.reauthReason,
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Validate trigger source (ensures webhook called from correct workflow)
|
|
1135
|
+
*/
|
|
1136
|
+
validateTriggerSource(
|
|
1137
|
+
payload: any,
|
|
1138
|
+
allowedSources: string[],
|
|
1139
|
+
requiredSource?: string
|
|
1140
|
+
): ValidationResult {
|
|
1141
|
+
const source = payload.triggerSource;
|
|
1142
|
+
|
|
1143
|
+
if (!source) {
|
|
1144
|
+
return {
|
|
1145
|
+
valid: false,
|
|
1146
|
+
error: 'Missing triggerSource in payload',
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (requiredSource && source !== requiredSource) {
|
|
1151
|
+
return {
|
|
1152
|
+
valid: false,
|
|
1153
|
+
error: `Invalid trigger source: expected ${requiredSource}, got ${source}`,
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
if (!allowedSources.includes(source)) {
|
|
1158
|
+
return {
|
|
1159
|
+
valid: false,
|
|
1160
|
+
error: `Trigger source not allowed: ${source}. Allowed: ${allowedSources.join(', ')}`,
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
return {
|
|
1165
|
+
valid: true,
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
---
|
|
1172
|
+
|
|
1173
|
+
### File: `src/services/idempotency.service.ts`
|
|
1174
|
+
|
|
1175
|
+
```typescript
|
|
1176
|
+
/**
|
|
1177
|
+
* Idempotency Service
|
|
1178
|
+
*
|
|
1179
|
+
* Tracks processed payments to prevent duplicate operations
|
|
1180
|
+
*/
|
|
1181
|
+
|
|
1182
|
+
import { VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
1183
|
+
import type { Logger } from '@fluentcommerce/fc-connect-sdk';
|
|
1184
|
+
|
|
1185
|
+
export interface IdempotencyRecord {
|
|
1186
|
+
operationType: 'capture' | 'refund' | 'cancel' | 'reauth';
|
|
1187
|
+
paymentReference: string;
|
|
1188
|
+
processedAt: string;
|
|
1189
|
+
resultReference: string;
|
|
1190
|
+
amount?: number;
|
|
1191
|
+
currency?: string;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
export class IdempotencyService {
|
|
1195
|
+
constructor(
|
|
1196
|
+
private kvAdapter: VersoriKVAdapter,
|
|
1197
|
+
private log: Logger
|
|
1198
|
+
) {}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* Generate idempotency key
|
|
1202
|
+
*/
|
|
1203
|
+
private getIdempotencyKey(
|
|
1204
|
+
operationType: string,
|
|
1205
|
+
paymentReference: string
|
|
1206
|
+
): string[] {
|
|
1207
|
+
return [`payment:${operationType}:${paymentReference}`];
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Check if operation was already processed
|
|
1212
|
+
*/
|
|
1213
|
+
async checkAndStore(
|
|
1214
|
+
operationType: 'capture' | 'refund' | 'cancel' | 'reauth',
|
|
1215
|
+
paymentReference: string,
|
|
1216
|
+
resultReference: string,
|
|
1217
|
+
amount?: { value: number; currency: string }
|
|
1218
|
+
): Promise<{ alreadyProcessed: boolean; existingRecord?: IdempotencyRecord }> {
|
|
1219
|
+
const key = this.getIdempotencyKey(operationType, paymentReference);
|
|
1220
|
+
const existing = await this.kvAdapter.get(key);
|
|
1221
|
+
|
|
1222
|
+
if (existing?.value) {
|
|
1223
|
+
const record = typeof existing.value === 'string'
|
|
1224
|
+
? JSON.parse(existing.value)
|
|
1225
|
+
: existing.value;
|
|
1226
|
+
|
|
1227
|
+
this.log.info('✅ [Idempotency] Operation already processed', {
|
|
1228
|
+
operationType,
|
|
1229
|
+
paymentReference,
|
|
1230
|
+
processedAt: record.processedAt,
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
return {
|
|
1234
|
+
alreadyProcessed: true,
|
|
1235
|
+
existingRecord: record,
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Store new record
|
|
1240
|
+
const record: IdempotencyRecord = {
|
|
1241
|
+
operationType,
|
|
1242
|
+
paymentReference,
|
|
1243
|
+
processedAt: new Date().toISOString(),
|
|
1244
|
+
resultReference,
|
|
1245
|
+
amount: amount?.value,
|
|
1246
|
+
currency: amount?.currency,
|
|
1247
|
+
};
|
|
1248
|
+
|
|
1249
|
+
await this.kvAdapter.set(key, JSON.stringify(record));
|
|
1250
|
+
|
|
1251
|
+
this.log.info('✅ [Idempotency] Operation recorded', {
|
|
1252
|
+
operationType,
|
|
1253
|
+
paymentReference,
|
|
1254
|
+
resultReference,
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
return {
|
|
1258
|
+
alreadyProcessed: false,
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* Get existing record (without storing)
|
|
1264
|
+
*/
|
|
1265
|
+
async getExisting(
|
|
1266
|
+
operationType: 'capture' | 'refund' | 'cancel' | 'reauth',
|
|
1267
|
+
paymentReference: string
|
|
1268
|
+
): Promise<IdempotencyRecord | null> {
|
|
1269
|
+
const key = this.getIdempotencyKey(operationType, paymentReference);
|
|
1270
|
+
const existing = await this.kvAdapter.get(key);
|
|
1271
|
+
|
|
1272
|
+
if (existing?.value) {
|
|
1273
|
+
return typeof existing.value === 'string'
|
|
1274
|
+
? JSON.parse(existing.value)
|
|
1275
|
+
: existing.value;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
return null;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
```
|
|
1282
|
+
|
|
1283
|
+
---
|
|
1284
|
+
|
|
1285
|
+
### File: `src/services/payment-event.service.ts`
|
|
1286
|
+
|
|
1287
|
+
```typescript
|
|
1288
|
+
/**
|
|
1289
|
+
* Payment Event Service
|
|
1290
|
+
*
|
|
1291
|
+
* Maps payment provider responses to Fluent event format and sends events
|
|
1292
|
+
*/
|
|
1293
|
+
|
|
1294
|
+
import type { FluentClient, Logger } from '@fluentcommerce/fc-connect-sdk';
|
|
1295
|
+
import type {
|
|
1296
|
+
CaptureResult,
|
|
1297
|
+
RefundResult,
|
|
1298
|
+
CancelResult,
|
|
1299
|
+
ReauthResult,
|
|
1300
|
+
PaymentContext,
|
|
1301
|
+
} from './payment-provider.service';
|
|
1302
|
+
|
|
1303
|
+
export class PaymentEventService {
|
|
1304
|
+
constructor(
|
|
1305
|
+
private client: FluentClient,
|
|
1306
|
+
private log: Logger
|
|
1307
|
+
) {}
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* Send payment capture event to Fluent Commerce
|
|
1311
|
+
*/
|
|
1312
|
+
async sendCaptureEvent(
|
|
1313
|
+
result: CaptureResult,
|
|
1314
|
+
context: PaymentContext
|
|
1315
|
+
): Promise<void> {
|
|
1316
|
+
this.log.info('📤 [Event] Sending payment capture event', {
|
|
1317
|
+
orderRef: context.orderRef,
|
|
1318
|
+
paymentReference: result.paymentReference,
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
const eventPayload = {
|
|
1322
|
+
name: 'PaymentCaptured',
|
|
1323
|
+
entityType: 'ORDER',
|
|
1324
|
+
entityRef: context.orderRef,
|
|
1325
|
+
data: {
|
|
1326
|
+
paymentReference: result.paymentReference,
|
|
1327
|
+
originalPaymentReference: context.paymentReference,
|
|
1328
|
+
amount: result.amount.value,
|
|
1329
|
+
currency: result.amount.currency,
|
|
1330
|
+
capturedAt: result.capturedAt,
|
|
1331
|
+
resultCode: result.resultCode,
|
|
1332
|
+
merchantReference: context.merchantReference,
|
|
1333
|
+
},
|
|
1334
|
+
};
|
|
1335
|
+
|
|
1336
|
+
await this.client.sendEvent(eventPayload);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* Send payment refund event to Fluent Commerce
|
|
1341
|
+
*/
|
|
1342
|
+
async sendRefundEvent(
|
|
1343
|
+
result: RefundResult,
|
|
1344
|
+
context: PaymentContext
|
|
1345
|
+
): Promise<void> {
|
|
1346
|
+
this.log.info('📤 [Event] Sending payment refund event', {
|
|
1347
|
+
orderRef: context.orderRef,
|
|
1348
|
+
refundReference: result.refundReference,
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
const eventPayload = {
|
|
1352
|
+
name: 'PaymentRefunded',
|
|
1353
|
+
entityType: 'ORDER',
|
|
1354
|
+
entityRef: context.orderRef,
|
|
1355
|
+
data: {
|
|
1356
|
+
refundReference: result.refundReference,
|
|
1357
|
+
originalPaymentReference: result.originalPaymentReference,
|
|
1358
|
+
amount: result.amount.value,
|
|
1359
|
+
currency: result.amount.currency,
|
|
1360
|
+
refundType: result.refundType || 'FULL',
|
|
1361
|
+
refundedAt: result.refundedAt,
|
|
1362
|
+
merchantReference: context.merchantReference,
|
|
1363
|
+
},
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
await this.client.sendEvent(eventPayload);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* Send payment cancellation event to Fluent Commerce
|
|
1371
|
+
*/
|
|
1372
|
+
async sendCancelEvent(
|
|
1373
|
+
result: CancelResult,
|
|
1374
|
+
context: PaymentContext
|
|
1375
|
+
): Promise<void> {
|
|
1376
|
+
this.log.info('📤 [Event] Sending payment cancellation event', {
|
|
1377
|
+
orderRef: context.orderRef,
|
|
1378
|
+
cancellationReference: result.cancellationReference,
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
const eventPayload = {
|
|
1382
|
+
name: 'PaymentAuthorizationCancelled',
|
|
1383
|
+
entityType: 'ORDER',
|
|
1384
|
+
entityRef: context.orderRef,
|
|
1385
|
+
data: {
|
|
1386
|
+
cancellationReference: result.cancellationReference,
|
|
1387
|
+
originalPaymentReference: result.originalPaymentReference,
|
|
1388
|
+
cancelledAt: result.cancelledAt,
|
|
1389
|
+
merchantReference: context.merchantReference,
|
|
1390
|
+
},
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1393
|
+
await this.client.sendEvent(eventPayload);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Send payment reauthorization event to Fluent Commerce
|
|
1398
|
+
*/
|
|
1399
|
+
async sendReauthEvent(
|
|
1400
|
+
result: ReauthResult,
|
|
1401
|
+
context: PaymentContext
|
|
1402
|
+
): Promise<void> {
|
|
1403
|
+
this.log.info('📤 [Event] Sending payment reauthorization event', {
|
|
1404
|
+
orderRef: context.orderRef,
|
|
1405
|
+
reauthReference: result.reauthReference,
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
const eventPayload = {
|
|
1409
|
+
name: 'PaymentReauthorized',
|
|
1410
|
+
entityType: 'ORDER',
|
|
1411
|
+
entityRef: context.orderRef,
|
|
1412
|
+
data: {
|
|
1413
|
+
reauthReference: result.reauthReference,
|
|
1414
|
+
originalPaymentReference: result.originalPaymentReference,
|
|
1415
|
+
amount: result.amount.value,
|
|
1416
|
+
currency: result.amount.currency,
|
|
1417
|
+
reauthorizedAt: result.reauthorizedAt,
|
|
1418
|
+
expiryDate: result.expiryDate,
|
|
1419
|
+
merchantReference: context.merchantReference,
|
|
1420
|
+
},
|
|
1421
|
+
};
|
|
1422
|
+
|
|
1423
|
+
await this.client.sendEvent(eventPayload);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
```
|
|
1427
|
+
|
|
1428
|
+
---
|
|
1429
|
+
|
|
1430
|
+
### File: `src/services/payment-provider-factory.service.ts`
|
|
1431
|
+
|
|
1432
|
+
```typescript
|
|
1433
|
+
/**
|
|
1434
|
+
* Payment Provider Factory
|
|
1435
|
+
*
|
|
1436
|
+
* Creates payment provider service based on configuration
|
|
1437
|
+
* Easy to swap providers: Adyen → Stripe → PayPal
|
|
1438
|
+
*/
|
|
1439
|
+
|
|
1440
|
+
import type { Logger } from '@fluentcommerce/fc-connect-sdk';
|
|
1441
|
+
import type { PaymentProviderService } from './payment-provider.service';
|
|
1442
|
+
import { AdyenPaymentProviderService } from './adyen-provider.service';
|
|
1443
|
+
// Future: import { StripePaymentProviderService } from './stripe-provider.service';
|
|
1444
|
+
|
|
1445
|
+
export class PaymentProviderFactory {
|
|
1446
|
+
/**
|
|
1447
|
+
* Create payment provider service
|
|
1448
|
+
*
|
|
1449
|
+
* @param provider - Provider name (adyen, stripe, paypal)
|
|
1450
|
+
* @param config - Provider-specific configuration
|
|
1451
|
+
* @param log - Logger instance
|
|
1452
|
+
*/
|
|
1453
|
+
static createProvider(
|
|
1454
|
+
provider: string,
|
|
1455
|
+
config: any,
|
|
1456
|
+
log: Logger
|
|
1457
|
+
): PaymentProviderService {
|
|
1458
|
+
switch (provider.toLowerCase()) {
|
|
1459
|
+
case 'adyen':
|
|
1460
|
+
return new AdyenPaymentProviderService(
|
|
1461
|
+
config.apiKey,
|
|
1462
|
+
config.environment,
|
|
1463
|
+
config.merchantAccount,
|
|
1464
|
+
log
|
|
1465
|
+
);
|
|
1466
|
+
|
|
1467
|
+
// Future: Stripe implementation
|
|
1468
|
+
// case 'stripe':
|
|
1469
|
+
// return new StripePaymentProviderService(
|
|
1470
|
+
// config.secretKey,
|
|
1471
|
+
// config.environment,
|
|
1472
|
+
// log
|
|
1473
|
+
// );
|
|
1474
|
+
|
|
1475
|
+
// Future: PayPal implementation
|
|
1476
|
+
// case 'paypal':
|
|
1477
|
+
// return new PayPalPaymentProviderService(
|
|
1478
|
+
// config.clientId,
|
|
1479
|
+
// config.clientSecret,
|
|
1480
|
+
// config.environment,
|
|
1481
|
+
// log
|
|
1482
|
+
// );
|
|
1483
|
+
|
|
1484
|
+
default:
|
|
1485
|
+
throw new Error(`Unsupported payment provider: ${provider}`);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
---
|
|
1492
|
+
|
|
1493
|
+
### File: `src/workflows/webhook/payment-capture.ts`
|
|
1494
|
+
|
|
1495
|
+
```typescript
|
|
1496
|
+
/**
|
|
1497
|
+
* ═══════════════════════════════════════════════════════════════
|
|
1498
|
+
* 🚀 PAYMENT CAPTURE WEBHOOK
|
|
1499
|
+
* ═══════════════════════════════════════════════════════════════
|
|
1500
|
+
*
|
|
1501
|
+
* Intercepts Fluent Workflow webhook for payment capture
|
|
1502
|
+
*
|
|
1503
|
+
* TRIGGER SOURCE: ORDER/FULFILLMENT Workflow (Rubix)
|
|
1504
|
+
* - Triggered when order is ready to ship
|
|
1505
|
+
* - Validates triggerSource: ORDER_FULFILLMENT
|
|
1506
|
+
* - Note: Rubix workflow ensures order/payment is in correct state before calling
|
|
1507
|
+
*
|
|
1508
|
+
* Flow:
|
|
1509
|
+
* - Validates webhook payload and signature
|
|
1510
|
+
* - Validates trigger source (must be ORDER_FULFILLMENT)
|
|
1511
|
+
* - Calls payment provider Capture API
|
|
1512
|
+
* - Sends event to Fluent Commerce Event API
|
|
1513
|
+
* - Handles idempotency
|
|
1514
|
+
*
|
|
1515
|
+
* IMPORTANT: No entity status validation needed - Rubix workflow handles business logic
|
|
1516
|
+
*/
|
|
1517
|
+
|
|
1518
|
+
import { webhook } from '@versori/run';
|
|
1519
|
+
import { createClient, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
1520
|
+
import { WebhookValidationService } from '../../services/webhook-validation.service';
|
|
1521
|
+
import { PaymentProviderFactory } from '../../services/payment-provider-factory.service';
|
|
1522
|
+
import { PaymentEventService } from '../../services/payment-event.service';
|
|
1523
|
+
import { IdempotencyService } from '../../services/idempotency.service';
|
|
1524
|
+
import type { PaymentProviderService } from '../../services/payment-provider.service';
|
|
1525
|
+
|
|
1526
|
+
/**
|
|
1527
|
+
* Background processing function for payment capture
|
|
1528
|
+
* Handles all long-running operations (payment gateway API, event sending)
|
|
1529
|
+
*/
|
|
1530
|
+
async function processPaymentCapture(ctx: any, executionStartTime: number): Promise<void> {
|
|
1531
|
+
const { log, activation, connections } = ctx;
|
|
1532
|
+
|
|
1533
|
+
try {
|
|
1534
|
+
log.info('🔄 [BACKGROUND] Starting background payment capture processing', {
|
|
1535
|
+
correlationId: activation.id,
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
// =================================================================
|
|
1539
|
+
// STEP 1: CONFIGURATION VALIDATION
|
|
1540
|
+
// =================================================================
|
|
1541
|
+
|
|
1542
|
+
if (!connections?.fluent_commerce) {
|
|
1543
|
+
log.error('❌ [BACKGROUND] Missing fluent_commerce connection');
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
const retailerId = activation.getVariable('fluentRetailerId');
|
|
1548
|
+
const paymentProvider = activation.getVariable('paymentProvider') || 'adyen';
|
|
1549
|
+
const paymentProviderEnv = (activation.getVariable('paymentProviderEnvironment') || 'test') as 'test' | 'live';
|
|
1550
|
+
const enableIdempotency = activation.getVariable('enableIdempotency') !== 'false';
|
|
1551
|
+
|
|
1552
|
+
// Get payment provider API key
|
|
1553
|
+
let apiKey: string | undefined;
|
|
1554
|
+
if (connections.payment_gateway?.credentials?.api_key) {
|
|
1555
|
+
apiKey = connections.payment_gateway.credentials.api_key;
|
|
1556
|
+
} else if (connections.adyen_payment_gateway?.credentials?.api_key) {
|
|
1557
|
+
apiKey = connections.adyen_payment_gateway.credentials.api_key;
|
|
1558
|
+
} else {
|
|
1559
|
+
apiKey = activation.getVariable('paymentGatewayApiKey') || activation.getVariable('adyenApiKey');
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
if (!apiKey) {
|
|
1563
|
+
log.error('❌ [BACKGROUND] Missing payment gateway API key');
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
const merchantAccount = activation.getVariable('adyenMerchantAccount');
|
|
1568
|
+
if (!merchantAccount && paymentProvider === 'adyen') {
|
|
1569
|
+
log.error('❌ [BACKGROUND] Missing adyenMerchantAccount');
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// =================================================================
|
|
1574
|
+
// STEP 2: PAYLOAD VALIDATION
|
|
1575
|
+
// =================================================================
|
|
1576
|
+
|
|
1577
|
+
const rawPayload = activation.body || ctx.data;
|
|
1578
|
+
const payload = typeof rawPayload === 'string' ? JSON.parse(rawPayload) : rawPayload;
|
|
1579
|
+
const validator = new WebhookValidationService(log);
|
|
1580
|
+
const validation = validator.validatePayload(payload);
|
|
1581
|
+
|
|
1582
|
+
if (!validation.valid) {
|
|
1583
|
+
log.error('❌ [BACKGROUND] Invalid payload', { error: validation.error });
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
const paymentContext = validator.extractPaymentContext(validation.payload!);
|
|
1588
|
+
const finalRetailerId = paymentContext.retailerId || retailerId;
|
|
1589
|
+
|
|
1590
|
+
if (!finalRetailerId) {
|
|
1591
|
+
log.error('❌ [BACKGROUND] Missing retailerId');
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// =================================================================
|
|
1596
|
+
// STEP 2.5: VALIDATE TRIGGER SOURCE (Capture must come from ORDER/FULFILLMENT)
|
|
1597
|
+
// =================================================================
|
|
1598
|
+
|
|
1599
|
+
const validateTriggerSource = activation.getVariable('validateTriggerSource') !== 'false';
|
|
1600
|
+
const allowedSources = (activation.getVariable('allowedTriggerSources') || 'ORDER_FULFILLMENT,PAYMENT_ENTITY')
|
|
1601
|
+
.split(',').map(s => s.trim());
|
|
1602
|
+
|
|
1603
|
+
if (validateTriggerSource) {
|
|
1604
|
+
const triggerValidation = validator.validateTriggerSource(
|
|
1605
|
+
payload,
|
|
1606
|
+
allowedSources,
|
|
1607
|
+
'ORDER_FULFILLMENT' // Capture MUST come from ORDER_FULFILLMENT
|
|
1608
|
+
);
|
|
1609
|
+
|
|
1610
|
+
if (!triggerValidation.valid) {
|
|
1611
|
+
log.error('❌ [BACKGROUND] Invalid trigger source', {
|
|
1612
|
+
triggerSource: payload.triggerSource,
|
|
1613
|
+
expected: 'ORDER_FULFILLMENT',
|
|
1614
|
+
error: triggerValidation.error,
|
|
1615
|
+
});
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
log.info('✅ [BACKGROUND] Trigger source validated', {
|
|
1620
|
+
triggerSource: payload.triggerSource,
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// =================================================================
|
|
1625
|
+
// STEP 3: INITIALIZE SERVICES
|
|
1626
|
+
// =================================================================
|
|
1627
|
+
|
|
1628
|
+
const fluentClient = await createClient(ctx, { validateConnection: true });
|
|
1629
|
+
await fluentClient.setRetailerId(finalRetailerId);
|
|
1630
|
+
|
|
1631
|
+
const kvAdapter = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
1632
|
+
const idempotencyService = new IdempotencyService(kvAdapter, log);
|
|
1633
|
+
|
|
1634
|
+
// Create payment provider service (easily swappable)
|
|
1635
|
+
const paymentProviderService: PaymentProviderService = PaymentProviderFactory.createProvider(
|
|
1636
|
+
paymentProvider,
|
|
1637
|
+
{
|
|
1638
|
+
apiKey,
|
|
1639
|
+
environment: paymentProviderEnv,
|
|
1640
|
+
merchantAccount,
|
|
1641
|
+
},
|
|
1642
|
+
log
|
|
1643
|
+
);
|
|
1644
|
+
|
|
1645
|
+
const eventService = new PaymentEventService(fluentClient, log);
|
|
1646
|
+
|
|
1647
|
+
// =================================================================
|
|
1648
|
+
// STEP 4: IDEMPOTENCY CHECK
|
|
1649
|
+
// =================================================================
|
|
1650
|
+
|
|
1651
|
+
if (enableIdempotency) {
|
|
1652
|
+
const idempotencyCheck = await idempotencyService.checkAndStore(
|
|
1653
|
+
'capture',
|
|
1654
|
+
paymentContext.paymentReference,
|
|
1655
|
+
'', // Will be updated after successful capture
|
|
1656
|
+
paymentContext.amount
|
|
1657
|
+
);
|
|
1658
|
+
|
|
1659
|
+
if (idempotencyCheck.alreadyProcessed && idempotencyCheck.existingRecord) {
|
|
1660
|
+
log.info('✅ [BACKGROUND] Payment already processed (idempotency)', {
|
|
1661
|
+
paymentReference: paymentContext.paymentReference,
|
|
1662
|
+
resultReference: idempotencyCheck.existingRecord.resultReference,
|
|
1663
|
+
});
|
|
1664
|
+
return; // Already processed, exit early
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// =================================================================
|
|
1669
|
+
// STEP 5: CALL PAYMENT PROVIDER
|
|
1670
|
+
// =================================================================
|
|
1671
|
+
|
|
1672
|
+
log.info('💳 [BACKGROUND] Calling payment provider', {
|
|
1673
|
+
provider: paymentProviderService.getProviderName(),
|
|
1674
|
+
paymentReference: paymentContext.paymentReference,
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
const captureResult = await paymentProviderService.capturePayment(
|
|
1678
|
+
paymentContext.paymentReference,
|
|
1679
|
+
paymentContext.amount,
|
|
1680
|
+
{
|
|
1681
|
+
...paymentContext,
|
|
1682
|
+
retailerId: finalRetailerId,
|
|
1683
|
+
}
|
|
1684
|
+
);
|
|
1685
|
+
|
|
1686
|
+
if (!captureResult.success) {
|
|
1687
|
+
log.error('❌ [BACKGROUND] Payment capture failed', {
|
|
1688
|
+
providerError: captureResult.error,
|
|
1689
|
+
paymentReference: paymentContext.paymentReference,
|
|
1690
|
+
});
|
|
1691
|
+
return; // Failed, exit
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// =================================================================
|
|
1695
|
+
// STEP 6: UPDATE IDEMPOTENCY KEY
|
|
1696
|
+
// =================================================================
|
|
1697
|
+
|
|
1698
|
+
if (enableIdempotency) {
|
|
1699
|
+
await idempotencyService.checkAndStore(
|
|
1700
|
+
'capture',
|
|
1701
|
+
paymentContext.paymentReference,
|
|
1702
|
+
captureResult.paymentReference,
|
|
1703
|
+
captureResult.amount
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// =================================================================
|
|
1708
|
+
// STEP 7: SEND EVENT TO FLUENT
|
|
1709
|
+
// =================================================================
|
|
1710
|
+
|
|
1711
|
+
try {
|
|
1712
|
+
await eventService.sendCaptureEvent(captureResult, {
|
|
1713
|
+
...paymentContext,
|
|
1714
|
+
retailerId: finalRetailerId,
|
|
1715
|
+
});
|
|
1716
|
+
log.info('✅ [BACKGROUND] Payment event sent to Fluent', {
|
|
1717
|
+
paymentReference: captureResult.paymentReference,
|
|
1718
|
+
});
|
|
1719
|
+
} catch (error: any) {
|
|
1720
|
+
log.error('❌ [BACKGROUND] Event send failed (non-blocking)', {
|
|
1721
|
+
error: error.message,
|
|
1722
|
+
paymentReference: captureResult.paymentReference,
|
|
1723
|
+
note: 'Payment capture succeeded, but event failed - will be retried',
|
|
1724
|
+
});
|
|
1725
|
+
// Continue - event failure doesn't block success
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// =================================================================
|
|
1729
|
+
// STEP 8: LOG SUCCESS
|
|
1730
|
+
// =================================================================
|
|
1731
|
+
|
|
1732
|
+
const duration = Date.now() - executionStartTime;
|
|
1733
|
+
log.info('✅ [BACKGROUND] Payment capture completed successfully', {
|
|
1734
|
+
paymentReference: paymentContext.paymentReference,
|
|
1735
|
+
resultReference: captureResult.paymentReference,
|
|
1736
|
+
provider: paymentProviderService.getProviderName(),
|
|
1737
|
+
duration: `${duration}ms`,
|
|
1738
|
+
});
|
|
1739
|
+
} catch (error: any) {
|
|
1740
|
+
log.error('❌ [BACKGROUND] Payment capture failed', {
|
|
1741
|
+
error: error.message,
|
|
1742
|
+
stack: error.stack,
|
|
1743
|
+
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* Payment Capture Webhook Handler
|
|
1750
|
+
* Uses sync + fire-and-forget pattern for fast response
|
|
1751
|
+
*/
|
|
1752
|
+
export const paymentCapture = webhook('payment-capture', {
|
|
1753
|
+
response: { mode: 'sync' }, // ✅ Sync mode: response sent when handler returns
|
|
1754
|
+
}, async (ctx) => {
|
|
1755
|
+
const { log, activation, connections } = ctx;
|
|
1756
|
+
const executionStartTime = Date.now();
|
|
1757
|
+
|
|
1758
|
+
log.info('🚀 [WEBHOOK] Received payment capture webhook', {
|
|
1759
|
+
correlationId: activation.id,
|
|
1760
|
+
timestamp: new Date().toISOString(),
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
// =================================================================
|
|
1764
|
+
// QUICK VALIDATION (Synchronous, ~10-50ms)
|
|
1765
|
+
// =================================================================
|
|
1766
|
+
|
|
1767
|
+
if (!connections?.fluent_commerce) {
|
|
1768
|
+
log.error('❌ [WEBHOOK] Missing fluent_commerce connection');
|
|
1769
|
+
return {
|
|
1770
|
+
status: 500,
|
|
1771
|
+
body: {
|
|
1772
|
+
success: false,
|
|
1773
|
+
error: 'Missing fluent_commerce connection',
|
|
1774
|
+
recommendation: 'Configure fluent_commerce connection in Connections section',
|
|
1775
|
+
},
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
const retailerId = activation.getVariable('fluentRetailerId');
|
|
1780
|
+
const paymentProvider = activation.getVariable('paymentProvider') || 'adyen';
|
|
1781
|
+
const webhookSecret = activation.getVariable('webhookSecret');
|
|
1782
|
+
|
|
1783
|
+
// Quick payload validation
|
|
1784
|
+
const rawPayload = activation.body || ctx.data;
|
|
1785
|
+
const payload = typeof rawPayload === 'string' ? JSON.parse(rawPayload) : rawPayload;
|
|
1786
|
+
|
|
1787
|
+
if (!payload.paymentReference || !payload.amount) {
|
|
1788
|
+
log.error('❌ [WEBHOOK] Invalid payload', {
|
|
1789
|
+
hasPaymentReference: !!payload.paymentReference,
|
|
1790
|
+
hasAmount: !!payload.amount,
|
|
1791
|
+
});
|
|
1792
|
+
return {
|
|
1793
|
+
status: 400,
|
|
1794
|
+
body: {
|
|
1795
|
+
success: false,
|
|
1796
|
+
error: 'Invalid payload: missing paymentReference or amount',
|
|
1797
|
+
},
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// Quick webhook signature validation (if configured)
|
|
1802
|
+
if (webhookSecret) {
|
|
1803
|
+
const payloadString = typeof rawPayload === 'string' ? rawPayload : JSON.stringify(rawPayload);
|
|
1804
|
+
const signature = activation.headers?.['x-webhook-signature'] ||
|
|
1805
|
+
activation.headers?.['webhook-signature'] ||
|
|
1806
|
+
activation.headers?.['signature'];
|
|
1807
|
+
|
|
1808
|
+
if (signature) {
|
|
1809
|
+
const validator = new WebhookValidationService(log);
|
|
1810
|
+
const isValid = validator.validateSignature(payloadString, signature, webhookSecret);
|
|
1811
|
+
|
|
1812
|
+
if (!isValid) {
|
|
1813
|
+
log.error('❌ [WEBHOOK] Invalid webhook signature');
|
|
1814
|
+
return {
|
|
1815
|
+
status: 401,
|
|
1816
|
+
body: {
|
|
1817
|
+
success: false,
|
|
1818
|
+
error: 'Invalid webhook signature',
|
|
1819
|
+
},
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
log.info('✅ [WEBHOOK] Validation passed, starting background processing', {
|
|
1826
|
+
paymentReference: payload.paymentReference,
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
// ✅ Fire-and-forget: Start background processing WITHOUT await
|
|
1830
|
+
// The promise continues execution after we return the response
|
|
1831
|
+
processPaymentCapture(ctx, executionStartTime)
|
|
1832
|
+
.then(() => {
|
|
1833
|
+
log.info('✅ [BACKGROUND] Payment capture processing completed successfully', {
|
|
1834
|
+
correlationId: activation.id,
|
|
1835
|
+
});
|
|
1836
|
+
})
|
|
1837
|
+
.catch((error: unknown) => {
|
|
1838
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1839
|
+
log.error('❌ [BACKGROUND] Payment capture processing failed', {
|
|
1840
|
+
correlationId: activation.id,
|
|
1841
|
+
error: errorMessage,
|
|
1842
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1843
|
+
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
1844
|
+
});
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1847
|
+
// Return immediately (response sent with this return value)
|
|
1848
|
+
return {
|
|
1849
|
+
status: 200,
|
|
1850
|
+
body: {
|
|
1851
|
+
success: true,
|
|
1852
|
+
message: 'Payment capture started in background',
|
|
1853
|
+
paymentReference: payload.paymentReference,
|
|
1854
|
+
timestamp: new Date().toISOString(),
|
|
1855
|
+
},
|
|
1856
|
+
};
|
|
1857
|
+
});
|
|
1858
|
+
```
|
|
1859
|
+
|
|
1860
|
+
---
|
|
1861
|
+
|
|
1862
|
+
### File: `src/workflows/webhook/payment-refund.ts`
|
|
1863
|
+
|
|
1864
|
+
```typescript
|
|
1865
|
+
/**
|
|
1866
|
+
* ═══════════════════════════════════════════════════════════════
|
|
1867
|
+
* 🚀 PAYMENT REFUND WEBHOOK
|
|
1868
|
+
* ═══════════════════════════════════════════════════════════════
|
|
1869
|
+
*
|
|
1870
|
+
* Intercepts Fluent Workflow webhook for payment refund
|
|
1871
|
+
*
|
|
1872
|
+
* TRIGGER SOURCE: Payment Entity Orchestration (Rubix)
|
|
1873
|
+
* - Triggered when refund is approved (after RMA processing, order cancellation)
|
|
1874
|
+
* - Validates triggerSource: PAYMENT_ENTITY
|
|
1875
|
+
* - Note: Rubix workflow ensures payment is in correct state before calling
|
|
1876
|
+
*
|
|
1877
|
+
* IMPORTANT: No entity status validation needed - Rubix workflow handles business logic
|
|
1878
|
+
*/
|
|
1879
|
+
|
|
1880
|
+
import { webhook } from '@versori/run';
|
|
1881
|
+
import { createClient, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
1882
|
+
import { WebhookValidationService } from '../../services/webhook-validation.service';
|
|
1883
|
+
import { PaymentProviderFactory } from '../../services/payment-provider-factory.service';
|
|
1884
|
+
import { PaymentEventService } from '../../services/payment-event.service';
|
|
1885
|
+
import { IdempotencyService } from '../../services/idempotency.service';
|
|
1886
|
+
import type { PaymentProviderService } from '../../services/payment-provider.service';
|
|
1887
|
+
|
|
1888
|
+
export const paymentRefund = webhook('payment-refund', async (ctx) => {
|
|
1889
|
+
const { log, activation, connections } = ctx;
|
|
1890
|
+
const executionStartTime = Date.now();
|
|
1891
|
+
|
|
1892
|
+
try {
|
|
1893
|
+
log.info('🚀 [PaymentRefund] Processing payment refund webhook', {
|
|
1894
|
+
correlationId: activation.id,
|
|
1895
|
+
});
|
|
1896
|
+
|
|
1897
|
+
// Similar structure to payment-capture.ts, but calls refundPayment()
|
|
1898
|
+
// ... (same configuration validation, webhook validation, etc.)
|
|
1899
|
+
|
|
1900
|
+
const fluentClient = await createClient(ctx, { validateConnection: true });
|
|
1901
|
+
await fluentClient.setRetailerId(finalRetailerId);
|
|
1902
|
+
|
|
1903
|
+
const kvAdapter = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
1904
|
+
const idempotencyService = new IdempotencyService(kvAdapter, log);
|
|
1905
|
+
|
|
1906
|
+
const paymentProviderService: PaymentProviderService = PaymentProviderFactory.createProvider(
|
|
1907
|
+
paymentProvider,
|
|
1908
|
+
{ apiKey, environment: paymentProviderEnv, merchantAccount },
|
|
1909
|
+
log
|
|
1910
|
+
);
|
|
1911
|
+
|
|
1912
|
+
const eventService = new PaymentEventService(fluentClient, log);
|
|
1913
|
+
|
|
1914
|
+
// Idempotency check
|
|
1915
|
+
if (enableIdempotency) {
|
|
1916
|
+
const idempotencyCheck = await idempotencyService.checkAndStore(
|
|
1917
|
+
'refund',
|
|
1918
|
+
paymentContext.paymentReference,
|
|
1919
|
+
'',
|
|
1920
|
+
paymentContext.amount
|
|
1921
|
+
);
|
|
1922
|
+
|
|
1923
|
+
if (idempotencyCheck.alreadyProcessed) {
|
|
1924
|
+
return {
|
|
1925
|
+
status: 200,
|
|
1926
|
+
body: {
|
|
1927
|
+
success: true,
|
|
1928
|
+
alreadyProcessed: true,
|
|
1929
|
+
refundReference: idempotencyCheck.existingRecord!.resultReference,
|
|
1930
|
+
},
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
// Call payment provider
|
|
1936
|
+
const refundResult = await paymentProviderService.refundPayment(
|
|
1937
|
+
paymentContext.paymentReference,
|
|
1938
|
+
paymentContext.amount,
|
|
1939
|
+
{ ...paymentContext, retailerId: finalRetailerId }
|
|
1940
|
+
);
|
|
1941
|
+
|
|
1942
|
+
if (!refundResult.success) {
|
|
1943
|
+
return {
|
|
1944
|
+
status: 502,
|
|
1945
|
+
body: {
|
|
1946
|
+
success: false,
|
|
1947
|
+
error: 'Payment refund failed',
|
|
1948
|
+
providerError: refundResult.error,
|
|
1949
|
+
},
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// Update idempotency
|
|
1954
|
+
if (enableIdempotency) {
|
|
1955
|
+
await idempotencyService.checkAndStore(
|
|
1956
|
+
'refund',
|
|
1957
|
+
paymentContext.paymentReference,
|
|
1958
|
+
refundResult.refundReference,
|
|
1959
|
+
refundResult.amount
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// Send event
|
|
1964
|
+
await eventService.sendRefundEvent(refundResult, {
|
|
1965
|
+
...paymentContext,
|
|
1966
|
+
retailerId: finalRetailerId,
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1969
|
+
const duration = Date.now() - executionStartTime;
|
|
1970
|
+
|
|
1971
|
+
return {
|
|
1972
|
+
status: 200,
|
|
1973
|
+
body: {
|
|
1974
|
+
success: true,
|
|
1975
|
+
refundReference: refundResult.refundReference,
|
|
1976
|
+
originalPaymentReference: refundResult.originalPaymentReference,
|
|
1977
|
+
amount: refundResult.amount.value,
|
|
1978
|
+
currency: refundResult.amount.currency,
|
|
1979
|
+
refundType: refundResult.refundType,
|
|
1980
|
+
refundedAt: refundResult.refundedAt,
|
|
1981
|
+
provider: paymentProviderService.getProviderName(),
|
|
1982
|
+
duration: `${duration}ms`,
|
|
1983
|
+
},
|
|
1984
|
+
};
|
|
1985
|
+
} catch (error: any) {
|
|
1986
|
+
log.error('❌ [PaymentRefund] Unexpected error', { error: error.message });
|
|
1987
|
+
return {
|
|
1988
|
+
status: 500,
|
|
1989
|
+
body: {
|
|
1990
|
+
success: false,
|
|
1991
|
+
error: 'Internal server error',
|
|
1992
|
+
message: error.message,
|
|
1993
|
+
},
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
});
|
|
1997
|
+
```
|
|
1998
|
+
|
|
1999
|
+
---
|
|
2000
|
+
|
|
2001
|
+
### File: `src/workflows/webhook/payment-auth-cancel.ts`
|
|
2002
|
+
|
|
2003
|
+
```typescript
|
|
2004
|
+
/**
|
|
2005
|
+
* ═══════════════════════════════════════════════════════════════
|
|
2006
|
+
* 🚀 PAYMENT AUTH CANCEL WEBHOOK
|
|
2007
|
+
* ═══════════════════════════════════════════════════════════════
|
|
2008
|
+
*
|
|
2009
|
+
* Intercepts Fluent Workflow webhook for authorization cancellation
|
|
2010
|
+
*
|
|
2011
|
+
* TRIGGER SOURCE: Payment Entity Orchestration (Rubix)
|
|
2012
|
+
* - Triggered when order is cancelled before capture
|
|
2013
|
+
* - Validates triggerSource: PAYMENT_ENTITY
|
|
2014
|
+
* - Note: Rubix workflow ensures payment is in correct state before calling
|
|
2015
|
+
*
|
|
2016
|
+
* IMPORTANT: No entity status validation needed - Rubix workflow handles business logic
|
|
2017
|
+
*/
|
|
2018
|
+
|
|
2019
|
+
import { webhook } from '@versori/run';
|
|
2020
|
+
import { createClient, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
2021
|
+
import { WebhookValidationService } from '../../services/webhook-validation.service';
|
|
2022
|
+
import { PaymentProviderFactory } from '../../services/payment-provider-factory.service';
|
|
2023
|
+
import { PaymentEventService } from '../../services/payment-event.service';
|
|
2024
|
+
import { IdempotencyService } from '../../services/idempotency.service';
|
|
2025
|
+
import type { PaymentProviderService } from '../../services/payment-provider.service';
|
|
2026
|
+
|
|
2027
|
+
export const paymentAuthCancel = webhook('payment-auth-cancel', async (ctx) => {
|
|
2028
|
+
const { log, activation, connections } = ctx;
|
|
2029
|
+
const executionStartTime = Date.now();
|
|
2030
|
+
|
|
2031
|
+
try {
|
|
2032
|
+
log.info('🚀 [PaymentAuthCancel] Processing authorization cancellation webhook', {
|
|
2033
|
+
correlationId: activation.id,
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
// Similar structure, but:
|
|
2037
|
+
// - No amount validation (cancellation doesn't need amount)
|
|
2038
|
+
// - Calls cancelAuthorization()
|
|
2039
|
+
// - Sends PaymentAuthorizationCancelled event
|
|
2040
|
+
|
|
2041
|
+
// ... (configuration validation, webhook validation similar to capture)
|
|
2042
|
+
|
|
2043
|
+
const paymentProviderService: PaymentProviderService = PaymentProviderFactory.createProvider(
|
|
2044
|
+
paymentProvider,
|
|
2045
|
+
{ apiKey, environment: paymentProviderEnv, merchantAccount },
|
|
2046
|
+
log
|
|
2047
|
+
);
|
|
2048
|
+
|
|
2049
|
+
const cancelResult = await paymentProviderService.cancelAuthorization(
|
|
2050
|
+
paymentContext.paymentReference,
|
|
2051
|
+
{ ...paymentContext, retailerId: finalRetailerId }
|
|
2052
|
+
);
|
|
2053
|
+
|
|
2054
|
+
if (!cancelResult.success) {
|
|
2055
|
+
return {
|
|
2056
|
+
status: 502,
|
|
2057
|
+
body: {
|
|
2058
|
+
success: false,
|
|
2059
|
+
error: 'Authorization cancellation failed',
|
|
2060
|
+
providerError: cancelResult.error,
|
|
2061
|
+
},
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
await eventService.sendCancelEvent(cancelResult, {
|
|
2066
|
+
...paymentContext,
|
|
2067
|
+
retailerId: finalRetailerId,
|
|
2068
|
+
});
|
|
2069
|
+
|
|
2070
|
+
return {
|
|
2071
|
+
status: 200,
|
|
2072
|
+
body: {
|
|
2073
|
+
success: true,
|
|
2074
|
+
cancellationReference: cancelResult.cancellationReference,
|
|
2075
|
+
originalPaymentReference: cancelResult.originalPaymentReference,
|
|
2076
|
+
cancelledAt: cancelResult.cancelledAt,
|
|
2077
|
+
provider: paymentProviderService.getProviderName(),
|
|
2078
|
+
},
|
|
2079
|
+
};
|
|
2080
|
+
} catch (error: any) {
|
|
2081
|
+
log.error('❌ [PaymentAuthCancel] Unexpected error', { error: error.message });
|
|
2082
|
+
return {
|
|
2083
|
+
status: 500,
|
|
2084
|
+
body: {
|
|
2085
|
+
success: false,
|
|
2086
|
+
error: 'Internal server error',
|
|
2087
|
+
message: error.message,
|
|
2088
|
+
},
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
});
|
|
2092
|
+
```
|
|
2093
|
+
|
|
2094
|
+
---
|
|
2095
|
+
|
|
2096
|
+
### File: `src/workflows/webhook/payment-reauth.ts`
|
|
2097
|
+
|
|
2098
|
+
```typescript
|
|
2099
|
+
/**
|
|
2100
|
+
* ═══════════════════════════════════════════════════════════════
|
|
2101
|
+
* 🚀 PAYMENT REAUTH WEBHOOK
|
|
2102
|
+
* ═══════════════════════════════════════════════════════════════
|
|
2103
|
+
*
|
|
2104
|
+
* Intercepts Fluent Workflow webhook for payment reauthorization
|
|
2105
|
+
*
|
|
2106
|
+
* TRIGGER SOURCE: Payment Entity Orchestration (Rubix)
|
|
2107
|
+
* - Triggered when authorization expires or needs extension
|
|
2108
|
+
* - Validates triggerSource: PAYMENT_ENTITY
|
|
2109
|
+
* - Note: Rubix workflow ensures authorization is in correct state before calling
|
|
2110
|
+
*
|
|
2111
|
+
* IMPORTANT: No entity status validation needed - Rubix workflow handles business logic
|
|
2112
|
+
*/
|
|
2113
|
+
|
|
2114
|
+
import { webhook } from '@versori/run';
|
|
2115
|
+
import { createClient, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
2116
|
+
import { WebhookValidationService } from '../../services/webhook-validation.service';
|
|
2117
|
+
import { PaymentProviderFactory } from '../../services/payment-provider-factory.service';
|
|
2118
|
+
import { PaymentEventService } from '../../services/payment-event.service';
|
|
2119
|
+
import { IdempotencyService } from '../../services/idempotency.service';
|
|
2120
|
+
import type { PaymentProviderService } from '../../services/payment-provider.service';
|
|
2121
|
+
|
|
2122
|
+
export const paymentReauth = webhook('payment-reauth', async (ctx) => {
|
|
2123
|
+
const { log, activation, connections } = ctx;
|
|
2124
|
+
const executionStartTime = Date.now();
|
|
2125
|
+
|
|
2126
|
+
try {
|
|
2127
|
+
log.info('🚀 [PaymentReauth] Processing payment reauthorization webhook', {
|
|
2128
|
+
correlationId: activation.id,
|
|
2129
|
+
});
|
|
2130
|
+
|
|
2131
|
+
// Similar structure to capture, but:
|
|
2132
|
+
// - Calls reauthorizePayment()
|
|
2133
|
+
// - Sends PaymentReauthorized event
|
|
2134
|
+
|
|
2135
|
+
// ... (configuration validation, webhook validation similar to capture)
|
|
2136
|
+
|
|
2137
|
+
const paymentProviderService: PaymentProviderService = PaymentProviderFactory.createProvider(
|
|
2138
|
+
paymentProvider,
|
|
2139
|
+
{ apiKey, environment: paymentProviderEnv, merchantAccount },
|
|
2140
|
+
log
|
|
2141
|
+
);
|
|
2142
|
+
|
|
2143
|
+
const reauthResult = await paymentProviderService.reauthorizePayment(
|
|
2144
|
+
paymentContext.paymentReference,
|
|
2145
|
+
paymentContext.amount,
|
|
2146
|
+
{ ...paymentContext, retailerId: finalRetailerId }
|
|
2147
|
+
);
|
|
2148
|
+
|
|
2149
|
+
if (!reauthResult.success) {
|
|
2150
|
+
return {
|
|
2151
|
+
status: 502,
|
|
2152
|
+
body: {
|
|
2153
|
+
success: false,
|
|
2154
|
+
error: 'Payment reauthorization failed',
|
|
2155
|
+
providerError: reauthResult.error,
|
|
2156
|
+
},
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
await eventService.sendReauthEvent(reauthResult, {
|
|
2161
|
+
...paymentContext,
|
|
2162
|
+
retailerId: finalRetailerId,
|
|
2163
|
+
});
|
|
2164
|
+
|
|
2165
|
+
return {
|
|
2166
|
+
status: 200,
|
|
2167
|
+
body: {
|
|
2168
|
+
success: true,
|
|
2169
|
+
reauthReference: reauthResult.reauthReference,
|
|
2170
|
+
originalPaymentReference: reauthResult.originalPaymentReference,
|
|
2171
|
+
amount: reauthResult.amount.value,
|
|
2172
|
+
currency: reauthResult.amount.currency,
|
|
2173
|
+
reauthorizedAt: reauthResult.reauthorizedAt,
|
|
2174
|
+
expiryDate: reauthResult.expiryDate,
|
|
2175
|
+
provider: paymentProviderService.getProviderName(),
|
|
2176
|
+
},
|
|
2177
|
+
};
|
|
2178
|
+
} catch (error: any) {
|
|
2179
|
+
log.error('❌ [PaymentReauth] Unexpected error', { error: error.message });
|
|
2180
|
+
return {
|
|
2181
|
+
status: 500,
|
|
2182
|
+
body: {
|
|
2183
|
+
success: false,
|
|
2184
|
+
error: 'Internal server error',
|
|
2185
|
+
message: error.message,
|
|
2186
|
+
},
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
});
|
|
2190
|
+
```
|
|
2191
|
+
|
|
2192
|
+
---
|
|
2193
|
+
|
|
2194
|
+
## How to Swap Payment Providers
|
|
2195
|
+
|
|
2196
|
+
### Current: Adyen → Stripe
|
|
2197
|
+
|
|
2198
|
+
**Step 1**: Create `src/services/stripe-provider.service.ts`
|
|
2199
|
+
|
|
2200
|
+
```typescript
|
|
2201
|
+
import type { PaymentProviderService } from './payment-provider.service';
|
|
2202
|
+
// Implement StripePaymentProviderService with same interface
|
|
2203
|
+
```
|
|
2204
|
+
|
|
2205
|
+
**Step 2**: Update `PaymentProviderFactory`
|
|
2206
|
+
|
|
2207
|
+
```typescript
|
|
2208
|
+
case 'stripe':
|
|
2209
|
+
return new StripePaymentProviderService(
|
|
2210
|
+
config.secretKey,
|
|
2211
|
+
config.environment,
|
|
2212
|
+
log
|
|
2213
|
+
);
|
|
2214
|
+
```
|
|
2215
|
+
|
|
2216
|
+
**Step 3**: Update Activation Variables
|
|
2217
|
+
|
|
2218
|
+
```bash
|
|
2219
|
+
paymentProvider=stripe # Change from adyen to stripe
|
|
2220
|
+
stripeSecretKey=sk_test_... # Add Stripe config
|
|
2221
|
+
```
|
|
2222
|
+
|
|
2223
|
+
**That's it!** All 4 webhooks automatically use Stripe. No changes needed to webhook code.
|
|
2224
|
+
|
|
2225
|
+
---
|
|
2226
|
+
|
|
2227
|
+
## Expected Webhook Payloads
|
|
2228
|
+
|
|
2229
|
+
### Capture Payload (from ORDER/FULFILLMENT Workflow)
|
|
2230
|
+
|
|
2231
|
+
```json
|
|
2232
|
+
{
|
|
2233
|
+
"orderRef": "ORDER-12345",
|
|
2234
|
+
"paymentReference": "8826162495174985",
|
|
2235
|
+
"amount": {
|
|
2236
|
+
"value": 10000,
|
|
2237
|
+
"currency": "USD"
|
|
2238
|
+
},
|
|
2239
|
+
"retailerId": "2",
|
|
2240
|
+
"merchantReference": "ORDER-12345",
|
|
2241
|
+
"triggerSource": "ORDER_FULFILLMENT",
|
|
2242
|
+
"orderStatus": "READY_TO_SHIP"
|
|
2243
|
+
}
|
|
2244
|
+
```
|
|
2245
|
+
|
|
2246
|
+
### Refund Payload (from Payment Entity Orchestration)
|
|
2247
|
+
|
|
2248
|
+
```json
|
|
2249
|
+
{
|
|
2250
|
+
"orderRef": "ORDER-12345",
|
|
2251
|
+
"paymentReference": "8826162495174985",
|
|
2252
|
+
"amount": {
|
|
2253
|
+
"value": 10000,
|
|
2254
|
+
"currency": "USD"
|
|
2255
|
+
},
|
|
2256
|
+
"retailerId": "2",
|
|
2257
|
+
"merchantReference": "ORDER-12345",
|
|
2258
|
+
"triggerSource": "PAYMENT_ENTITY",
|
|
2259
|
+
"refundReason": "RETURN_APPROVED",
|
|
2260
|
+
"rmaRef": "RMA-67890",
|
|
2261
|
+
"refundType": "FULL"
|
|
2262
|
+
}
|
|
2263
|
+
```
|
|
2264
|
+
|
|
2265
|
+
### Auth Cancel Payload (from Payment Entity Orchestration)
|
|
2266
|
+
|
|
2267
|
+
```json
|
|
2268
|
+
{
|
|
2269
|
+
"orderRef": "ORDER-12345",
|
|
2270
|
+
"paymentReference": "8826162495174985",
|
|
2271
|
+
"retailerId": "2",
|
|
2272
|
+
"merchantReference": "ORDER-12345",
|
|
2273
|
+
"triggerSource": "PAYMENT_ENTITY",
|
|
2274
|
+
"cancelReason": "ORDER_CANCELLED",
|
|
2275
|
+
"orderStatus": "CANCELLED"
|
|
2276
|
+
}
|
|
2277
|
+
```
|
|
2278
|
+
|
|
2279
|
+
### ReAuth Payload (from Payment Entity Orchestration)
|
|
2280
|
+
|
|
2281
|
+
```json
|
|
2282
|
+
{
|
|
2283
|
+
"orderRef": "ORDER-12345",
|
|
2284
|
+
"paymentReference": "8826162495174985",
|
|
2285
|
+
"amount": {
|
|
2286
|
+
"value": 10000,
|
|
2287
|
+
"currency": "USD"
|
|
2288
|
+
},
|
|
2289
|
+
"retailerId": "2",
|
|
2290
|
+
"merchantReference": "ORDER-12345",
|
|
2291
|
+
"triggerSource": "PAYMENT_ENTITY",
|
|
2292
|
+
"reauthReason": "AUTHORIZATION_EXPIRING"
|
|
2293
|
+
}
|
|
2294
|
+
```
|
|
2295
|
+
|
|
2296
|
+
---
|
|
2297
|
+
|
|
2298
|
+
## Expected Events Sent to Fluent Commerce
|
|
2299
|
+
|
|
2300
|
+
### PaymentCaptured
|
|
2301
|
+
|
|
2302
|
+
```json
|
|
2303
|
+
{
|
|
2304
|
+
"name": "PaymentCaptured",
|
|
2305
|
+
"entityType": "ORDER",
|
|
2306
|
+
"entityRef": "ORDER-12345",
|
|
2307
|
+
"data": {
|
|
2308
|
+
"paymentReference": "8826162495174985",
|
|
2309
|
+
"originalPaymentReference": "8826162495174985",
|
|
2310
|
+
"amount": 10000,
|
|
2311
|
+
"currency": "USD",
|
|
2312
|
+
"capturedAt": "2025-01-22T12:34:56.789Z",
|
|
2313
|
+
"resultCode": "Received",
|
|
2314
|
+
"merchantReference": "ORDER-12345"
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
```
|
|
2318
|
+
|
|
2319
|
+
### PaymentRefunded
|
|
2320
|
+
|
|
2321
|
+
```json
|
|
2322
|
+
{
|
|
2323
|
+
"name": "PaymentRefunded",
|
|
2324
|
+
"entityType": "ORDER",
|
|
2325
|
+
"entityRef": "ORDER-12345",
|
|
2326
|
+
"data": {
|
|
2327
|
+
"refundReference": "8826162495174986",
|
|
2328
|
+
"originalPaymentReference": "8826162495174985",
|
|
2329
|
+
"amount": 10000,
|
|
2330
|
+
"currency": "USD",
|
|
2331
|
+
"refundType": "FULL",
|
|
2332
|
+
"refundedAt": "2025-01-22T12:34:56.789Z",
|
|
2333
|
+
"merchantReference": "ORDER-12345"
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
```
|
|
2337
|
+
|
|
2338
|
+
### PaymentAuthorizationCancelled
|
|
2339
|
+
|
|
2340
|
+
```json
|
|
2341
|
+
{
|
|
2342
|
+
"name": "PaymentAuthorizationCancelled",
|
|
2343
|
+
"entityType": "ORDER",
|
|
2344
|
+
"entityRef": "ORDER-12345",
|
|
2345
|
+
"data": {
|
|
2346
|
+
"cancellationReference": "8826162495174987",
|
|
2347
|
+
"originalPaymentReference": "8826162495174985",
|
|
2348
|
+
"cancelledAt": "2025-01-22T12:34:56.789Z",
|
|
2349
|
+
"merchantReference": "ORDER-12345"
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
```
|
|
2353
|
+
|
|
2354
|
+
### PaymentReauthorized
|
|
2355
|
+
|
|
2356
|
+
```json
|
|
2357
|
+
{
|
|
2358
|
+
"name": "PaymentReauthorized",
|
|
2359
|
+
"entityType": "ORDER",
|
|
2360
|
+
"entityRef": "ORDER-12345",
|
|
2361
|
+
"data": {
|
|
2362
|
+
"reauthReference": "8826162495174988",
|
|
2363
|
+
"originalPaymentReference": "8826162495174985",
|
|
2364
|
+
"amount": 10000,
|
|
2365
|
+
"currency": "USD",
|
|
2366
|
+
"reauthorizedAt": "2025-01-22T12:34:56.789Z",
|
|
2367
|
+
"expiryDate": "2025-02-22T12:34:56.789Z",
|
|
2368
|
+
"merchantReference": "ORDER-12345"
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
```
|
|
2372
|
+
|
|
2373
|
+
---
|
|
2374
|
+
|
|
2375
|
+
## Testing Checklist
|
|
2376
|
+
|
|
2377
|
+
### 1. Configuration Testing
|
|
2378
|
+
- [ ] Fluent Commerce connection configured
|
|
2379
|
+
- [ ] Payment gateway connection configured (Adyen)
|
|
2380
|
+
- [ ] All activation variables set
|
|
2381
|
+
- [ ] Provider can be swapped (test Stripe if implemented)
|
|
2382
|
+
|
|
2383
|
+
### 2. Webhook Testing (All 4)
|
|
2384
|
+
- [ ] Capture webhook works
|
|
2385
|
+
- [ ] Refund webhook works
|
|
2386
|
+
- [ ] Auth Cancel webhook works
|
|
2387
|
+
- [ ] ReAuth webhook works
|
|
2388
|
+
|
|
2389
|
+
### 3. Idempotency Testing
|
|
2390
|
+
- [ ] Duplicate capture calls return same result
|
|
2391
|
+
- [ ] Duplicate refund calls return same result
|
|
2392
|
+
- [ ] Duplicate cancel calls return same result
|
|
2393
|
+
- [ ] Duplicate reauth calls return same result
|
|
2394
|
+
|
|
2395
|
+
### 4. Event Testing
|
|
2396
|
+
- [ ] Capture event sent to Fluent
|
|
2397
|
+
- [ ] Refund event sent to Fluent
|
|
2398
|
+
- [ ] Cancel event sent to Fluent
|
|
2399
|
+
- [ ] ReAuth event sent to Fluent
|
|
2400
|
+
|
|
2401
|
+
### 5. Provider Swap Testing (Future)
|
|
2402
|
+
- [ ] Switch to Stripe provider
|
|
2403
|
+
- [ ] Verify all 4 flows work with Stripe
|
|
2404
|
+
- [ ] Verify events still sent correctly
|
|
2405
|
+
|
|
2406
|
+
---
|
|
2407
|
+
|
|
2408
|
+
## Deployment Steps
|
|
2409
|
+
|
|
2410
|
+
1. **Set up Connections**:
|
|
2411
|
+
- Configure `fluent_commerce` connection (OAuth2)
|
|
2412
|
+
- Configure `payment_gateway` or `adyen_payment_gateway` connection (API key)
|
|
2413
|
+
|
|
2414
|
+
2. **Set Activation Variables**:
|
|
2415
|
+
- `fluentRetailerId`
|
|
2416
|
+
- `paymentProvider` (default: `adyen`)
|
|
2417
|
+
- `paymentProviderEnvironment` (test/live)
|
|
2418
|
+
- `adyenMerchantAccount` (if using Adyen)
|
|
2419
|
+
- `webhookSecret` (optional)
|
|
2420
|
+
|
|
2421
|
+
3. **Deploy Workflow**:
|
|
2422
|
+
```bash
|
|
2423
|
+
cd versori-payment-gateway-integration
|
|
2424
|
+
npm install
|
|
2425
|
+
versori deploy
|
|
2426
|
+
```
|
|
2427
|
+
|
|
2428
|
+
4. **Configure Fluent Workflows**:
|
|
2429
|
+
- Point webhook URLs to Versori endpoints:
|
|
2430
|
+
- `/payment-capture`
|
|
2431
|
+
- `/payment-refund`
|
|
2432
|
+
- `/payment-auth-cancel`
|
|
2433
|
+
- `/payment-reauth`
|
|
2434
|
+
|
|
2435
|
+
5. **Test End-to-End**:
|
|
2436
|
+
- Test each webhook from Fluent Workflow
|
|
2437
|
+
- Verify payment provider API calls
|
|
2438
|
+
- Verify events sent to Fluent Commerce
|
|
2439
|
+
- Check Versori logs for errors
|
|
2440
|
+
|
|
2441
|
+
---
|
|
2442
|
+
|
|
2443
|
+
## Key Benefits of This Architecture
|
|
2444
|
+
|
|
2445
|
+
✅ **Single Connector** - All 4 flows in one document/workflow
|
|
2446
|
+
✅ **Provider Agnostic** - Easy to swap Adyen → Stripe (change one service)
|
|
2447
|
+
✅ **DRY Principle** - Shared services used by all webhooks
|
|
2448
|
+
✅ **Maintainable** - Clear separation of concerns
|
|
2449
|
+
✅ **Testable** - Each layer can be tested independently
|
|
2450
|
+
✅ **Scalable** - Easy to add new payment providers or operations
|
|
2451
|
+
|
|
2452
|
+
---
|
|
2453
|
+
|
|
2454
|
+
## Future Enhancements
|
|
2455
|
+
|
|
2456
|
+
### Adding Stripe Support
|
|
2457
|
+
|
|
2458
|
+
1. Create `StripePaymentProviderService` implementing `PaymentProviderService`
|
|
2459
|
+
2. Add case to `PaymentProviderFactory`
|
|
2460
|
+
3. Update activation variables
|
|
2461
|
+
4. **Done!** All 4 webhooks automatically use Stripe
|
|
2462
|
+
|
|
2463
|
+
### Adding PayPal Support
|
|
2464
|
+
|
|
2465
|
+
Same pattern - implement interface, add to factory, configure variables.
|
|
2466
|
+
|
|
2467
|
+
### Adding New Operations
|
|
2468
|
+
|
|
2469
|
+
1. Add method to `PaymentProviderService` interface
|
|
2470
|
+
2. Implement in all provider services
|
|
2471
|
+
3. Add webhook workflow
|
|
2472
|
+
4. Add event service method
|
|
2473
|
+
5. **Done!**
|
|
2474
|
+
|
|
2475
|
+
---
|
|
2476
|
+
|
|
2477
|
+
This template provides a complete, modular, provider-agnostic payment gateway integration that's easy to maintain and extend!
|
|
2478
|
+
|