@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +11 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/utils/pagination-helpers.js +38 -2
- package/dist/esm/versori/fluent-versori-client.js +11 -5
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
- package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
- package/docs/00-START-HERE/cli-documentation-index.md +202 -202
- package/docs/00-START-HERE/cli-quick-reference.md +252 -252
- package/docs/00-START-HERE/decision-tree.md +552 -552
- package/docs/00-START-HERE/getting-started.md +1070 -1070
- package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
- package/docs/00-START-HERE/readme.md +237 -237
- package/docs/00-START-HERE/retailerid-configuration.md +404 -404
- package/docs/00-START-HERE/sdk-philosophy.md +794 -794
- package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
- package/docs/01-TEMPLATES/faq.md +686 -686
- package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
- package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
- package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
- package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
- package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
- package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
- package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
- package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
- package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
- package/docs/01-TEMPLATES/readme.md +957 -957
- package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
- package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
- package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
- package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
- package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
- package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
- package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
- package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
- package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
- package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
- package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
- package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
- package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
- package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
- package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
- package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
- package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
- package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
- package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -520
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
- package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
- package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
- package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
- package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
- package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
- package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
- package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
- package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
- package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
- package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
- package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
- package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
- package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
- package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
- package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
- package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
- package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
- package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
- package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
- package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
- package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
- package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
- package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
- package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
- package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
- package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
- package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
- package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
- package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
- package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
- package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
- package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
- package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
- package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
- package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
- package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
- package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
- package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
- package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
- package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
- package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
- package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
- package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
- package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
- package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
- package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
- package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
- package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
- package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
- package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
- package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
- package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
- package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
- package/docs/02-CORE-GUIDES/readme.md +194 -194
- package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
- package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
- package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
- package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
- package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
- package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
- package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
- package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
- package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
- package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
- package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
- package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
- package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
- package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
- package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
- package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
- package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
- package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
- package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
- package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
- package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
- package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
- package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
- package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
- package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
- package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
- package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/readme.md +159 -159
- package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
- package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
- package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
- package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
- package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
- package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
- package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
- package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
- package/docs/04-REFERENCE/architecture/readme.md +279 -279
- package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
- package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
- package/docs/04-REFERENCE/platforms/readme.md +135 -135
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
- package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
- package/docs/04-REFERENCE/readme.md +148 -148
- package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
- package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
- package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
- package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
- package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
- package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
- package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
- package/docs/04-REFERENCE/schema/readme.md +141 -141
- package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
- package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
- package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
- package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
- package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
- package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
- package/docs/04-REFERENCE/testing/readme.md +86 -86
- package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
- package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
- package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
- package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
- package/docs/template-loading-matrix.md +242 -242
- package/package.json +5 -3
- package/docs/02-CORE-GUIDES/api-reference/cli-profile-integration.md +0 -377
|
@@ -1,2029 +1,2029 @@
|
|
|
1
|
-
---
|
|
2
|
-
template_id: tpl-webhook-xml-order-ingestion-sfcc
|
|
3
|
-
canonical_filename: template-webhook-xml-order-ingestion.md
|
|
4
|
-
sdk_version: ^0.1.41
|
|
5
|
-
runtime: versori
|
|
6
|
-
direction: ingestion
|
|
7
|
-
source: webhook-xml-sfcc
|
|
8
|
-
destination: fluent-graphql
|
|
9
|
-
entity: order
|
|
10
|
-
format: xml
|
|
11
|
-
logging: versori
|
|
12
|
-
status: production-ready
|
|
13
|
-
last_updated: 2025-11-13
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
# Template: Webhook - SFCC XML Order Ingestion
|
|
17
|
-
|
|
18
|
-
**FC Connect SDK Use Case Guide**
|
|
19
|
-
|
|
20
|
-
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
21
|
-
> **Version**: Use latest - `npm install @fluentcommerce/fc-connect-sdk@latest`
|
|
22
|
-
|
|
23
|
-
**Context**: Receive SFCC XML orders via Versori HTTP webhook and create orders in Fluent Commerce using GraphQL mutations.
|
|
24
|
-
|
|
25
|
-
**Complexity**: Medium
|
|
26
|
-
|
|
27
|
-
**Runtime**: Versori Platform
|
|
28
|
-
|
|
29
|
-
**Estimated Lines**: ~500 lines (modular structure)
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## STEP 1: Understand This Template
|
|
34
|
-
|
|
35
|
-
**What This Template Does:**
|
|
36
|
-
|
|
37
|
-
- Versori HTTP webhook endpoint receiving XML order data (SFCC native format)
|
|
38
|
-
- XML parsing with automatic Versori XML-to-object conversion (incoming XML)
|
|
39
|
-
- GraphQL mutation mapping from SFCC native XML structure to Fluent schema
|
|
40
|
-
- Uses ONLY SFCC native fields (orders.order.*) - no Radial XML dependency
|
|
41
|
-
- Custom resolvers for data transformation, name parsing, and order type derivation
|
|
42
|
-
- Error handling and structured response formatting
|
|
43
|
-
- **Response Options**: JSON (default) or XML (with custom Response handler)
|
|
44
|
-
- **Sync + Fire-and-Forget Pattern**: Fast webhook response, background processing
|
|
45
|
-
|
|
46
|
-
**Key SDK Components:**
|
|
47
|
-
|
|
48
|
-
- `createClient()` - Universal client factory (auto-detects Versori context)
|
|
49
|
-
- `GraphQLMutationMapper` - XML/JSON → GraphQL mutation mapping
|
|
50
|
-
- Native Versori `log` - Use `log` from context
|
|
51
|
-
- No XML parsing needed (Versori handles automatically)
|
|
52
|
-
|
|
53
|
-
**Entity Type:**
|
|
54
|
-
|
|
55
|
-
- **Order** - Fluent entity for order creation
|
|
56
|
-
- **GraphQL Mutation** - Uses `createOrder` mutation (not Event API)
|
|
57
|
-
|
|
58
|
-
**Critical Patterns:**
|
|
59
|
-
|
|
60
|
-
- **XML Response Pattern**: Production-ready XML response handling with proper Content-Type headers
|
|
61
|
-
- **Logger Adapter**: Type-safe logging with SDK Logger interface
|
|
62
|
-
- **Sync + Fire-and-Forget**: Webhook validates quickly, returns immediately, processes in background
|
|
63
|
-
- **External JSON Config**: Mapping configuration in separate JSON file (`config/sfcc-to-fluent-order-mapping.json`)
|
|
64
|
-
- **Modular Architecture**: Separate services, workflows, config, types folders
|
|
65
|
-
- **Integration Variables Validation**: Fail-fast validation of required configuration
|
|
66
|
-
- **Initial Deployment Monitoring**: Temporary full payload logging for troubleshooting
|
|
67
|
-
- **SFCC Native Only**: No Radial XML dependency - simpler and more maintainable
|
|
68
|
-
|
|
69
|
-
**When to Use This Template:**
|
|
70
|
-
|
|
71
|
-
- ✅ SFCC native XML order ingestion (orders.order.* structure)
|
|
72
|
-
- ✅ Need fast webhook response (don't wait for order creation)
|
|
73
|
-
- ✅ Single order per webhook call
|
|
74
|
-
- ✅ BOPIS (Buy Online Pick Up In Store) and standard shipping orders
|
|
75
|
-
- ✅ Custom resolvers for complex transformations (name parsing, address normalization)
|
|
76
|
-
- ✅ Want simpler template without Radial XML complexity
|
|
77
|
-
|
|
78
|
-
**When NOT to Use:**
|
|
79
|
-
|
|
80
|
-
- ❌ Bulk order processing (use Batch API or scheduled workflows)
|
|
81
|
-
- ❌ Order updates (use GraphQL `updateOrder` mutation)
|
|
82
|
-
- ❌ CSV/JSON formats (use appropriate format template)
|
|
83
|
-
- ❌ Need synchronous order creation (wait for result before responding)
|
|
84
|
-
- ❌ Orders requiring Radial-specific fields (use Radial template instead)
|
|
85
|
-
|
|
86
|
-
---
|
|
87
|
-
|
|
88
|
-
## STEP 2: Implementation Prompt for Claude Code
|
|
89
|
-
|
|
90
|
-
**Copy this prompt and send to Claude Code to generate the complete implementation:**
|
|
91
|
-
|
|
92
|
-
```
|
|
93
|
-
Create a Versori webhook workflow for SFCC XML order ingestion to Fluent Commerce GraphQL.
|
|
94
|
-
|
|
95
|
-
REQUIREMENTS:
|
|
96
|
-
1. Runtime: Versori Platform (HTTP webhook)
|
|
97
|
-
2. Source: SFCC XML order data via HTTP POST webhook
|
|
98
|
-
3. Destination: Fluent Commerce GraphQL API (createOrder mutation)
|
|
99
|
-
4. Format: XML (Versori auto-parses to object)
|
|
100
|
-
5. Entity: Order (GraphQL mutation)
|
|
101
|
-
|
|
102
|
-
KEY FEATURES:
|
|
103
|
-
- Sync + fire-and-forget pattern (fast webhook response, background processing)
|
|
104
|
-
- External JSON mapping configuration (config/sfcc-to-fluent-order-mapping.json)
|
|
105
|
-
- Modular architecture (workflows/, services/, config/, types/)
|
|
106
|
-
- SFCC native fields ONLY (no Radial XML)
|
|
107
|
-
- GraphQLMutationMapper with standard map() method (no nodes)
|
|
108
|
-
- Custom resolvers for data transformation
|
|
109
|
-
- Audit trail (save input/output files)
|
|
110
|
-
- Comprehensive error handling with structured logging
|
|
111
|
-
|
|
112
|
-
CRITICAL REQUIREMENTS:
|
|
113
|
-
1. Webhook Mode: response: { mode: 'sync' } with XML onSuccess/onError handlers
|
|
114
|
-
2. Response Format: XML with proper Content-Type headers (production standard for SFCC)
|
|
115
|
-
3. Logger Adapter: Create typed Logger adapter from Versori log
|
|
116
|
-
4. Background Processing: Fire-and-forget pattern (no await on long operations)
|
|
117
|
-
5. Mapping Config: External JSON file (config/sfcc-to-fluent-order-mapping.json)
|
|
118
|
-
6. Custom Resolvers: USE mapWithNodes() when you have custom resolvers (REQUIRED)
|
|
119
|
-
7. Integration Variables: Fail-fast validation of required config (fluentRetailerId)
|
|
120
|
-
8. Modular Structure: Separate services/, config/, types/ folders
|
|
121
|
-
9. Step Documentation: Use visual separator blocks for each workflow step
|
|
122
|
-
10. Error Handling: Structured error responses with recommendations
|
|
123
|
-
|
|
124
|
-
SDK METHODS TO USE (v0.1.41+):
|
|
125
|
-
- createClient({ ...ctx, log }) - Pass full Versori context
|
|
126
|
-
- Logger adapter pattern - Type-safe logging wrapper
|
|
127
|
-
- new GraphQLMutationMapper(mappingConfig, logger, { customResolvers, fluentClient: client }) - Initialize mapper with resolvers (RECOMMENDED)
|
|
128
|
-
- mapper.mapWithNodes(xmlData, undefined, context) - Map with constructor resolvers (auto-returns query)
|
|
129
|
-
- result.query and result.variables - Auto-generated from mapWithNodes()
|
|
130
|
-
- client.graphql({ query: result.query, variables: result.variables }) - Execute mutation
|
|
131
|
-
|
|
132
|
-
FORBIDDEN PATTERNS:
|
|
133
|
-
- ❌ Inline mapping config (use external JSON)
|
|
134
|
-
- ❌ await on background processing (use fire-and-forget)
|
|
135
|
-
- ❌ Passing log directly to SDK (use Logger adapter)
|
|
136
|
-
- ❌ All code in one file (use modular structure)
|
|
137
|
-
- ❌ async mode webhook (use sync + fire-and-forget)
|
|
138
|
-
- ❌ Radial XML references (use SFCC native only)
|
|
139
|
-
- ❌ buildMutation() calls (mapWithNodes() auto-generates query in v0.1.41+)
|
|
140
|
-
- ❌ Silent config fallbacks (fail-fast on missing required variables)
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
---
|
|
144
|
-
|
|
145
|
-
## STEP 3: Detailed Flow Documentation
|
|
146
|
-
|
|
147
|
-
### Complete Processing Flow
|
|
148
|
-
|
|
149
|
-
```
|
|
150
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
151
|
-
│ 1. WEBHOOK RECEIVED │
|
|
152
|
-
│ POST https://{workspace}.versori.run/sfcc-order-create │
|
|
153
|
-
│ Content-Type: application/xml │
|
|
154
|
-
│ Body: <orders xmlns="..."><order order-no="..."> │
|
|
155
|
-
│ <original-order-no>...</original-order-no> │
|
|
156
|
-
│ <customer>...</customer> │
|
|
157
|
-
│ <product-lineitems>...</product-lineitems> │
|
|
158
|
-
│ <shipments>...</shipments> │
|
|
159
|
-
│ <totals>...</totals> │
|
|
160
|
-
│ <payments>...</payments> │
|
|
161
|
-
│ </order></orders> │
|
|
162
|
-
└────────────────────┬────────────────────────────────────────┘
|
|
163
|
-
│
|
|
164
|
-
▼
|
|
165
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
166
|
-
│ 2. QUICK VALIDATION (Synchronous, ~10-50ms) │
|
|
167
|
-
│ - Check fluent_commerce connection exists │
|
|
168
|
-
│ - Validate SFCC XML payload present │
|
|
169
|
-
│ - Validate required fields (original-order-no, customer) │
|
|
170
|
-
│ - Return HTTP 200 OK immediately │
|
|
171
|
-
└────────────────────┬────────────────────────────────────────┘
|
|
172
|
-
│
|
|
173
|
-
▼
|
|
174
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
175
|
-
│ 3. BACKGROUND PROCESSING (Fire-and-Forget) │
|
|
176
|
-
│ ┌─────────────────────────────────────────────────────┐ │
|
|
177
|
-
│ │ 3a. Initialize Fluent Client │ │
|
|
178
|
-
│ │ - createClient({ ...ctx, log }) │ │
|
|
179
|
-
│ └─────────────────────────────────────────────────────┘ │
|
|
180
|
-
│ ┌─────────────────────────────────────────────────────┐ │
|
|
181
|
-
│ │ 3b. Validate SFCC Structure │ │
|
|
182
|
-
│ │ - Check required SFCC native fields │ │
|
|
183
|
-
│ │ - Validate order structure │ │
|
|
184
|
-
│ └─────────────────────────────────────────────────────┘ │
|
|
185
|
-
│ ┌─────────────────────────────────────────────────────┐ │
|
|
186
|
-
│ │ 3c. Map SFCC Native XML to GraphQL Variables │ │
|
|
187
|
-
│ │ - Load mapping config from JSON │ │
|
|
188
|
-
│ │ - Use SFCC native fields ONLY (orders.order.*) │ │
|
|
189
|
-
│ │ - GraphQLMutationMapper.mapWithNodes() (REQUIRED for custom resolvers)│ │
|
|
190
|
-
│ │ - Apply custom resolvers │ │
|
|
191
|
-
│ │ - Returns { query, variables } (GraphQLPayload) │ │
|
|
192
|
-
│ └─────────────────────────────────────────────────────┘ │
|
|
193
|
-
│ ┌─────────────────────────────────────────────────────┐ │
|
|
194
|
-
│ │ 3d. Execute GraphQL Mutation │ │
|
|
195
|
-
│ │ - client.graphql(payload) - one simple step! │ │
|
|
196
|
-
│ └─────────────────────────────────────────────────────┘ │
|
|
197
|
-
└─────────────────────────────────────────────────────────────┘
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### Response Timing
|
|
201
|
-
|
|
202
|
-
| Stage | Timing | Blocking |
|
|
203
|
-
|-------|--------|----------|
|
|
204
|
-
| **Webhook Validation** | ~10-50ms | ✅ Yes (blocks response) |
|
|
205
|
-
| **Background Processing** | ~1000-2000ms | ❌ No (fire-and-forget) |
|
|
206
|
-
| **Total Response Time** | ~10-50ms | ✅ Fast response |
|
|
207
|
-
|
|
208
|
-
**Key Benefit**: Webhook caller receives immediate acknowledgment (~50ms) while order creation happens in background (~1-2s).
|
|
209
|
-
|
|
210
|
-
---
|
|
211
|
-
|
|
212
|
-
## STEP 4: Production Modular Structure
|
|
213
|
-
|
|
214
|
-
> **✅ This section shows the COMPLETE production-ready modular structure.**
|
|
215
|
-
> All files are shown with proper imports/exports and folder organization.
|
|
216
|
-
|
|
217
|
-
### Complete Project Structure
|
|
218
|
-
|
|
219
|
-
```
|
|
220
|
-
sfcc-xml-order-ingestion/
|
|
221
|
-
├── package.json # Dependencies and Versori config
|
|
222
|
-
├── index.ts # Entry point - exports all workflows
|
|
223
|
-
└── src/
|
|
224
|
-
├── workflows/
|
|
225
|
-
│ └── webhook/
|
|
226
|
-
│ └── order-ingestion.ts # Webhook: Receive SFCC XML orders
|
|
227
|
-
│
|
|
228
|
-
├── services/
|
|
229
|
-
│ └── order-processing.service.ts # Shared orchestration logic (reusable)
|
|
230
|
-
│
|
|
231
|
-
├── resolvers/
|
|
232
|
-
│ ├── index.ts # Export all resolvers
|
|
233
|
-
│ ├── types.ts # TypeScript interfaces
|
|
234
|
-
│ ├── order-resolvers.ts # Order-level resolvers
|
|
235
|
-
│ ├── fulfillment-resolvers.ts # Shipment/fulfillment resolvers
|
|
236
|
-
│ ├── item-resolvers.ts # Product line item resolvers
|
|
237
|
-
│ └── payment-resolvers.ts # Payment resolvers
|
|
238
|
-
│
|
|
239
|
-
├── config/
|
|
240
|
-
│ └── sfcc-to-fluent-order-mapping.json # Mapping configuration (external JSON)
|
|
241
|
-
│
|
|
242
|
-
└── types/
|
|
243
|
-
└── order.types.ts # TypeScript interfaces
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
**Why This Structure?**
|
|
247
|
-
|
|
248
|
-
- ✅ **Clear separation**: Webhook handlers vs business logic
|
|
249
|
-
- ✅ **Reusable services**: Order processing logic can be reused
|
|
250
|
-
- ✅ **External config**: Mapping changes don't require code changes
|
|
251
|
-
- ✅ **Custom resolvers**: Separate file for complex transformations
|
|
252
|
-
- ✅ **Type safety**: TypeScript interfaces for better IDE support
|
|
253
|
-
- ✅ **Scalable**: Easy to add new webhooks or services
|
|
254
|
-
|
|
255
|
-
---
|
|
256
|
-
|
|
257
|
-
---
|
|
258
|
-
|
|
259
|
-
## CRITICAL: XML Response Architecture (Production Pattern)
|
|
260
|
-
|
|
261
|
-
### Overview
|
|
262
|
-
|
|
263
|
-
This template uses **XML responses** as the PRIMARY pattern for SFCC integration. This is the production-proven approach used in real SFCC → Fluent integrations.
|
|
264
|
-
|
|
265
|
-
### How XML Response Handling Works
|
|
266
|
-
|
|
267
|
-
```
|
|
268
|
-
┌─────────────────────────────────────────────────────────────────┐
|
|
269
|
-
│ ARCHITECTURE FLOW │
|
|
270
|
-
├─────────────────────────────────────────────────────────────────┤
|
|
271
|
-
│ │
|
|
272
|
-
│ 1. Workflow Steps Return XML Strings │
|
|
273
|
-
│ ↓ ctx.data = "<OrderProcessingResponse>...</>" │
|
|
274
|
-
│ │
|
|
275
|
-
│ 2. onSuccess Handler Receives Context │
|
|
276
|
-
│ ↓ ctx = { data: xmlString, executionId: "..." } │
|
|
277
|
-
│ │
|
|
278
|
-
│ 3. Handler Wraps in Response Object │
|
|
279
|
-
│ ↓ new Response(ctx.data, { headers: {...} }) │
|
|
280
|
-
│ │
|
|
281
|
-
│ 4. Framework Streams Response Directly │
|
|
282
|
-
│ ↓ No JSON encoding - pure XML stream │
|
|
283
|
-
│ │
|
|
284
|
-
└─────────────────────────────────────────────────────────────────┘
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
### Response Format Examples
|
|
288
|
-
|
|
289
|
-
**Success Response:**
|
|
290
|
-
```xml
|
|
291
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
292
|
-
<OrderProcessingResponse>
|
|
293
|
-
<Status>success</Status>
|
|
294
|
-
<Message>Order successfully processed and created in Fluent Commerce</Message>
|
|
295
|
-
<FluentOrderId>6370</FluentOrderId>
|
|
296
|
-
<FluentOrderRef>0017326966182_G_postman_test_1760703173226</FluentOrderRef>
|
|
297
|
-
<SFCCOrderRef>0017326966182_G_postman_test_1760703173226</SFCCOrderRef>
|
|
298
|
-
<Timestamp>2025-11-13T12:12:54.229Z</Timestamp>
|
|
299
|
-
</OrderProcessingResponse>
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
**Error Response:**
|
|
303
|
-
```xml
|
|
304
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
305
|
-
<OrderProcessingResponse>
|
|
306
|
-
<Status>error</Status>
|
|
307
|
-
<Message>Failed to process order</Message>
|
|
308
|
-
<Error>Missing required field: original-order-no</Error>
|
|
309
|
-
<Timestamp>2025-11-13T12:12:54.229Z</Timestamp>
|
|
310
|
-
</OrderProcessingResponse>
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
### Key Benefits
|
|
314
|
-
|
|
315
|
-
- ✅ **SFCC Native**: SFCC systems expect XML responses
|
|
316
|
-
- ✅ **Type Safety**: Structured XML schema
|
|
317
|
-
- ✅ **Execution Tracking**: X-Execution-Id header for troubleshooting
|
|
318
|
-
- ✅ **Proper Content-Type**: application/xml; charset=utf-8
|
|
319
|
-
- ✅ **Framework Streaming**: Direct XML stream (no JSON wrapper)
|
|
320
|
-
|
|
321
|
-
---
|
|
322
|
-
|
|
323
|
-
## XML Handling Patterns
|
|
324
|
-
|
|
325
|
-
### Incoming XML (Versori Auto-Parsing)
|
|
326
|
-
|
|
327
|
-
**CRITICAL**: Versori automatically parses XML when `Content-Type: application/xml` is set.
|
|
328
|
-
|
|
329
|
-
```typescript
|
|
330
|
-
// ✅ CORRECT - Versori auto-parses XML into object
|
|
331
|
-
// When SFCC sends: POST /webhook with Content-Type: application/xml
|
|
332
|
-
// Versori automatically converts XML string → parsed object
|
|
333
|
-
// ctx.data is already a parsed object, NOT a string
|
|
334
|
-
|
|
335
|
-
export const webhook = webhook('order-ingestion', { response: { mode: 'sync' } })
|
|
336
|
-
.then(http('process', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
337
|
-
// ctx.data is already parsed! No manual parsing needed
|
|
338
|
-
const sfccData = ctx.data; // Already an object: { orders: { order: {...} } }
|
|
339
|
-
|
|
340
|
-
// Validate structure
|
|
341
|
-
if (!sfccData.orders?.order) {
|
|
342
|
-
throw new Error('Invalid XML structure: missing orders.order');
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Use mapWithNodes() because we have custom resolvers
|
|
346
|
-
// ✅ Resolvers passed in constructor, so pass undefined here (or override if needed)
|
|
347
|
-
const result = await mapper.mapWithNodes(sfccData, undefined, context);
|
|
348
|
-
}));
|
|
349
|
-
|
|
350
|
-
// ❌ WRONG - Don't try to parse manually
|
|
351
|
-
const xmlString = ctx.data; // This is already parsed!
|
|
352
|
-
const parsed = parseXML(xmlString); // Unnecessary!
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
**Why This Matters:**
|
|
356
|
-
- Versori handles XML parsing automatically
|
|
357
|
-
- `ctx.data` is always a parsed object when Content-Type is `application/xml`
|
|
358
|
-
- No need for `XMLParserService` or manual parsing in Versori workflows
|
|
359
|
-
- Simply validate the structure and use directly
|
|
360
|
-
|
|
361
|
-
### Outgoing XML (Production-Ready Pattern)
|
|
362
|
-
|
|
363
|
-
**RECOMMENDED FOR SFCC**: Use XML responses with custom handlers for production SFCC integrations.
|
|
364
|
-
|
|
365
|
-
**XML Response Pattern (RECOMMENDED)**
|
|
366
|
-
```typescript
|
|
367
|
-
import { XMLBuilder } from '@fluentcommerce/fc-connect-sdk';
|
|
368
|
-
|
|
369
|
-
export const webhook = webhook('order-ingestion', {
|
|
370
|
-
response: {
|
|
371
|
-
mode: 'sync',
|
|
372
|
-
/**
|
|
373
|
-
* onSuccess Handler for XML Response
|
|
374
|
-
*
|
|
375
|
-
* This handler wraps the XML response string with proper headers.
|
|
376
|
-
* The workflow steps return XML strings via ctx.data, and this
|
|
377
|
-
* handler wraps them in Response objects for proper streaming.
|
|
378
|
-
*
|
|
379
|
-
* @param ctx - Context containing:
|
|
380
|
-
* - ctx.data: The XML response string from the final workflow step
|
|
381
|
-
* - ctx.executionId: Unique identifier for this execution
|
|
382
|
-
* @returns Response object with XML body and proper headers
|
|
383
|
-
*/
|
|
384
|
-
onSuccess: (ctx) => new Response(ctx.data, {
|
|
385
|
-
status: 200,
|
|
386
|
-
headers: {
|
|
387
|
-
'Content-Type': 'application/xml; charset=utf-8',
|
|
388
|
-
'X-Execution-Id': ctx.executionId
|
|
389
|
-
}
|
|
390
|
-
}),
|
|
391
|
-
/**
|
|
392
|
-
* onError Handler for XML Error Response
|
|
393
|
-
*
|
|
394
|
-
* This handler wraps error responses with proper status and headers.
|
|
395
|
-
* Returns XML format for consistent error responses.
|
|
396
|
-
*
|
|
397
|
-
* @param ctx - Context containing:
|
|
398
|
-
* - ctx.data: The error XML string from the .catch() step
|
|
399
|
-
* - ctx.executionId: Unique identifier for tracking failed executions
|
|
400
|
-
* @returns Response object with error XML and 500 status
|
|
401
|
-
*/
|
|
402
|
-
onError: (ctx) => new Response(ctx.data, {
|
|
403
|
-
status: 500,
|
|
404
|
-
headers: {
|
|
405
|
-
'Content-Type': 'application/xml; charset=utf-8',
|
|
406
|
-
'X-Execution-Id': ctx.executionId
|
|
407
|
-
}
|
|
408
|
-
})
|
|
409
|
-
}
|
|
410
|
-
})
|
|
411
|
-
.then(http('process', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
412
|
-
// Process order...
|
|
413
|
-
const orderId = '12345';
|
|
414
|
-
const orderRef = 'ORD-001';
|
|
415
|
-
|
|
416
|
-
// Build XML response string
|
|
417
|
-
const builder = new XMLBuilder({
|
|
418
|
-
xmlDeclaration: true, // Adds <?xml version="1.0" encoding="UTF-8"?>
|
|
419
|
-
prettyPrint: true, // Format with indentation
|
|
420
|
-
encoding: 'UTF-8'
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
const responseData = {
|
|
424
|
-
Status: 'success',
|
|
425
|
-
FluentOrderId: orderId,
|
|
426
|
-
FluentOrderRef: orderRef,
|
|
427
|
-
Timestamp: new Date().toISOString()
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
// Return XML string (onSuccess handler wraps it in Response)
|
|
431
|
-
return builder.build(responseData, 'OrderProcessingResponse');
|
|
432
|
-
}))
|
|
433
|
-
.catch(({ data, log }) => {
|
|
434
|
-
// Return error XML string (onError handler wraps it)
|
|
435
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
436
|
-
<OrderProcessingResponse>
|
|
437
|
-
<Status>error</Status>
|
|
438
|
-
<Error>${data instanceof Error ? data.message : String(data)}</Error>
|
|
439
|
-
</OrderProcessingResponse>`;
|
|
440
|
-
});
|
|
441
|
-
// Response: <?xml version="1.0"?><OrderProcessingResponse>...
|
|
442
|
-
// Content-Type: application/xml; charset=utf-8
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
**Alternative: JSON Response (Simple Testing)**
|
|
446
|
-
|
|
447
|
-
For testing or non-SFCC systems, you can use JSON responses:
|
|
448
|
-
|
|
449
|
-
```typescript
|
|
450
|
-
export const webhook = webhook('order-ingestion', {
|
|
451
|
-
response: { mode: 'sync' }
|
|
452
|
-
})
|
|
453
|
-
.then(http('process', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
454
|
-
// Returns JSON object - Versori auto-encodes
|
|
455
|
-
return {
|
|
456
|
-
success: true,
|
|
457
|
-
orderId: '12345',
|
|
458
|
-
orderRef: 'ORD-001'
|
|
459
|
-
};
|
|
460
|
-
}));
|
|
461
|
-
// Response: {"success":true,"orderId":"12345","orderRef":"ORD-001"}
|
|
462
|
-
// Content-Type: application/json
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
**Key Decision Points:**
|
|
466
|
-
|
|
467
|
-
| Pattern | Use When | Response Type |
|
|
468
|
-
|---------|----------|---------------|
|
|
469
|
-
| **XML Response** | Production SFCC integration | XML with proper headers |
|
|
470
|
-
| **JSON Response** | Testing, non-SFCC systems | JSON auto-encoded |
|
|
471
|
-
|
|
472
|
-
**Key Points:**
|
|
473
|
-
- ✅ **XML (Recommended)**: Requires custom `onSuccess`/`onError` handlers with `Response` objects
|
|
474
|
-
- ✅ **JSON (Testing)**: Default behavior, no custom handler needed
|
|
475
|
-
- ✅ Workflow steps return raw strings (XML) or objects (JSON)
|
|
476
|
-
- ✅ Response handlers wrap in `Response` objects with proper Content-Type
|
|
477
|
-
|
|
478
|
-
---
|
|
479
|
-
|
|
480
|
-
## SDK Methods Used
|
|
481
|
-
|
|
482
|
-
```typescript
|
|
483
|
-
// Core SDK imports
|
|
484
|
-
// FC Connect SDK v0.1.41+
|
|
485
|
-
// Install: npm install @fluentcommerce/fc-connect-sdk@^0.1.41
|
|
486
|
-
// Docs: https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk/
|
|
487
|
-
// GitHub: https://github.com/fluentcommerce/fc-connect-sdk
|
|
488
|
-
|
|
489
|
-
import { createClient, GraphQLMutationMapper, XMLBuilder } from '@fluentcommerce/fc-connect-sdk';
|
|
490
|
-
import type { Logger } from '@fluentcommerce/fc-connect-sdk';
|
|
491
|
-
|
|
492
|
-
// ═══════════════════════════════════════════════════════════════
|
|
493
|
-
// LOGGER ADAPTER PATTERN (Production-Proven)
|
|
494
|
-
// ═══════════════════════════════════════════════════════════════
|
|
495
|
-
// Create typed Logger adapter from Versori log
|
|
496
|
-
// Benefits:
|
|
497
|
-
// - Type safety with SDK Logger interface
|
|
498
|
-
// - Centralized logging (easy to modify all calls)
|
|
499
|
-
// - Clear separation between Versori log and SDK Logger
|
|
500
|
-
const logger: Logger = {
|
|
501
|
-
info: (msg: string, meta?: any) => log.info(msg, meta),
|
|
502
|
-
error: (msg: string, error?: Error, meta?: any) => log.error(msg, meta),
|
|
503
|
-
warn: (msg: string, meta?: any) => log.info(msg, meta),
|
|
504
|
-
debug: (msg: string, meta?: any) => log.info(msg, meta)
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
// Key methods
|
|
508
|
-
createClient(ctx); // Auto-detects Versori context
|
|
509
|
-
// ✅ RECOMMENDED: Pass resolvers in constructor
|
|
510
|
-
new GraphQLMutationMapper(config, logger, { customResolvers, fluentClient: client });
|
|
511
|
-
// ✅ Alternative: Pass resolvers to mapWithNodes (constructor resolvers merged)
|
|
512
|
-
mapper.mapWithNodes(data, resolvers, context);
|
|
513
|
-
// ✅ v0.1.41+: mapWithNodes() auto-returns query and variables
|
|
514
|
-
const result = await mapper.mapWithNodes(data, undefined, context); // Use constructor resolvers
|
|
515
|
-
// result.query - Auto-generated GraphQL mutation
|
|
516
|
-
// result.variables - Auto-wrapped variables (input object)
|
|
517
|
-
client.graphql({ query: result.query, variables: result.variables }); // Execute
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
### Why GraphQLMutationMapper (Not UniversalMapper)?
|
|
521
|
-
|
|
522
|
-
**GraphQLMutationMapper** is the right choice for this template because:
|
|
523
|
-
|
|
524
|
-
✅ **Automatic Mutation Building**: Automatically generates GraphQL mutation query from mapping config
|
|
525
|
-
✅ **Input Wrapping**: Automatically wraps mapped data in `input` object (Fluent API requirement)
|
|
526
|
-
✅ **GraphQL-Specific**: Designed specifically for GraphQL mutations with schema awareness
|
|
527
|
-
|
|
528
|
-
**UniversalMapper** would require:
|
|
529
|
-
- ❌ Manual GraphQL mutation query building
|
|
530
|
-
- ❌ Manual `input` wrapping
|
|
531
|
-
- ❌ More boilerplate code
|
|
532
|
-
|
|
533
|
-
**Use UniversalMapper when**: You need general-purpose data transformation (CSV → JSON, GraphQL → Parquet, etc.)
|
|
534
|
-
**Use GraphQLMutationMapper when**: You need XML/JSON → GraphQL mutations (this template's use case)
|
|
535
|
-
|
|
536
|
-
---
|
|
537
|
-
|
|
538
|
-
## Complete Working Code
|
|
539
|
-
|
|
540
|
-
### 1. Entry Point (index.ts)
|
|
541
|
-
|
|
542
|
-
```typescript
|
|
543
|
-
/**
|
|
544
|
-
* Entry point - Export all workflows for Versori platform
|
|
545
|
-
*/
|
|
546
|
-
|
|
547
|
-
// Webhook workflows
|
|
548
|
-
export { sfccOrderCreate } from './workflows/webhook/sfcc-order-create';
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
### 2. Workflow Entry Point (workflows/webhook/sfcc-order-create.ts)
|
|
552
|
-
|
|
553
|
-
```typescript
|
|
554
|
-
/**
|
|
555
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
556
|
-
* SFCC → FLUENT ORDER CREATE WEBHOOK (Production Pattern)
|
|
557
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
558
|
-
*
|
|
559
|
-
* PURPOSE:
|
|
560
|
-
* This webhook receives order data from Salesforce Commerce Cloud (SFCC) as XML
|
|
561
|
-
* in the request body. It validates, transforms, and creates orders in Fluent
|
|
562
|
-
* Commerce via GraphQL mutations.
|
|
563
|
-
*
|
|
564
|
-
* WEBHOOK CONFIGURATION:
|
|
565
|
-
* - Response mode: synchronous (caller waits for response)
|
|
566
|
-
* - Response format: XML (OrderProcessingResponse)
|
|
567
|
-
* - CORS: enabled (allows cross-origin requests)
|
|
568
|
-
* - Expected payload: XML in request body (Content-Type: application/xml)
|
|
569
|
-
*
|
|
570
|
-
* ARCHITECTURE:
|
|
571
|
-
* Uses production-proven patterns from real SFCC integrations:
|
|
572
|
-
* - XML response handling with proper Content-Type headers
|
|
573
|
-
* - Logger adapter for type-safe logging
|
|
574
|
-
* - Fire-and-forget background processing
|
|
575
|
-
* - Fail-fast validation of required configuration
|
|
576
|
-
* - Visual step documentation blocks
|
|
577
|
-
*
|
|
578
|
-
* WORKFLOW STEPS:
|
|
579
|
-
* 1. Validate XML structure and required fields
|
|
580
|
-
* 2. Apply mapping with custom resolvers
|
|
581
|
-
* 3. Execute GraphQL mutation to create order
|
|
582
|
-
* 4. Build XML response with order creation results
|
|
583
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
584
|
-
*/
|
|
585
|
-
|
|
586
|
-
import { webhook, http, fn } from '@versori/run';
|
|
587
|
-
import type { Context } from '@versori/run';
|
|
588
|
-
import { createClient, GraphQLMutationMapper } from '@fluentcommerce/fc-connect-sdk';
|
|
589
|
-
import type { Logger } from '@fluentcommerce/fc-connect-sdk';
|
|
590
|
-
import { allResolvers } from '../../resolvers';
|
|
591
|
-
import mappingConfig from '../../config/sfcc-to-fluent-order-mapping.json' with { type: 'json' };
|
|
592
|
-
import { XMLBuilder } from '@fluentcommerce/fc-connect-sdk';
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
596
|
-
* XML RESPONSE ARCHITECTURE (Production Pattern)
|
|
597
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
598
|
-
*
|
|
599
|
-
* This webhook uses XML responses as the PRIMARY pattern for SFCC integration.
|
|
600
|
-
* The onSuccess and onError handlers wrap XML strings in Response objects with
|
|
601
|
-
* proper Content-Type headers.
|
|
602
|
-
*
|
|
603
|
-
* RESPONSE FORMAT:
|
|
604
|
-
* Success:
|
|
605
|
-
* <?xml version="1.0" encoding="UTF-8"?>
|
|
606
|
-
* <OrderProcessingResponse>
|
|
607
|
-
* <Status>success</Status>
|
|
608
|
-
* <Message>Order successfully processed and created in Fluent Commerce</Message>
|
|
609
|
-
* <FluentOrderId>6370</FluentOrderId>
|
|
610
|
-
* <FluentOrderRef>0017326966182_G_postman_test_1760703173226</FluentOrderRef>
|
|
611
|
-
* <SFCCOrderRef>0017326966182_G_postman_test_1760703173226</SFCCOrderRef>
|
|
612
|
-
* <Timestamp>2025-11-13T12:12:54.229Z</Timestamp>
|
|
613
|
-
* </OrderProcessingResponse>
|
|
614
|
-
*
|
|
615
|
-
* Error:
|
|
616
|
-
* <?xml version="1.0" encoding="UTF-8"?>
|
|
617
|
-
* <OrderProcessingResponse>
|
|
618
|
-
* <Status>error</Status>
|
|
619
|
-
* <Message>Failed to process order</Message>
|
|
620
|
-
* <Error>Error details here</Error>
|
|
621
|
-
* <Timestamp>2025-11-13T12:12:54.229Z</Timestamp>
|
|
622
|
-
* </OrderProcessingResponse>
|
|
623
|
-
*
|
|
624
|
-
* HOW IT WORKS:
|
|
625
|
-
* 1. Workflow steps return XML strings via ctx.data
|
|
626
|
-
* 2. onSuccess handler receives ctx with the XML string in ctx.data
|
|
627
|
-
* 3. Handler creates Response with Content-Type: application/xml
|
|
628
|
-
* 4. Framework streams the Response directly (no JSON encoding)
|
|
629
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
630
|
-
*/
|
|
631
|
-
export const sfccOrderCreate = webhook('sfcc-order-create', {
|
|
632
|
-
response: {
|
|
633
|
-
mode: 'sync',
|
|
634
|
-
/**
|
|
635
|
-
* onSuccess Handler for XML Response
|
|
636
|
-
*
|
|
637
|
-
* This handler wraps the XML response string with proper headers.
|
|
638
|
-
* Similar to production SFCC workflows, this ensures the XML is
|
|
639
|
-
* streamed directly without JSON encoding.
|
|
640
|
-
*
|
|
641
|
-
* @param ctx - Context containing:
|
|
642
|
-
* - ctx.data: The XML response string from the final workflow step
|
|
643
|
-
* - ctx.executionId: Unique identifier for this execution
|
|
644
|
-
* @returns Response object with XML body and proper headers
|
|
645
|
-
*/
|
|
646
|
-
onSuccess: (ctx) => new Response(ctx.data, {
|
|
647
|
-
status: 200,
|
|
648
|
-
headers: {
|
|
649
|
-
'Content-Type': 'application/xml; charset=utf-8',
|
|
650
|
-
'X-Execution-Id': ctx.executionId
|
|
651
|
-
}
|
|
652
|
-
}),
|
|
653
|
-
/**
|
|
654
|
-
* onError Handler for XML Error Response
|
|
655
|
-
*
|
|
656
|
-
* This handler wraps error responses with proper status and headers.
|
|
657
|
-
* Returns XML format for consistent error responses.
|
|
658
|
-
*
|
|
659
|
-
* @param ctx - Context containing:
|
|
660
|
-
* - ctx.data: The error XML string from the .catch() step
|
|
661
|
-
* - ctx.executionId: Unique identifier for tracking failed executions
|
|
662
|
-
* @returns Response object with error XML and 500 status
|
|
663
|
-
*/
|
|
664
|
-
onError: (ctx) => new Response(ctx.data, {
|
|
665
|
-
status: 500,
|
|
666
|
-
headers: {
|
|
667
|
-
'Content-Type': 'application/xml; charset=utf-8',
|
|
668
|
-
'X-Execution-Id': ctx.executionId
|
|
669
|
-
}
|
|
670
|
-
})
|
|
671
|
-
},
|
|
672
|
-
cors: true
|
|
673
|
-
}).then(
|
|
674
|
-
/**
|
|
675
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
676
|
-
* STEP 1: VALIDATE XML STRUCTURE
|
|
677
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
678
|
-
*
|
|
679
|
-
* This step validates that the incoming data is properly parsed XML from Versori.
|
|
680
|
-
* Uses fail-fast validation pattern to catch configuration issues early.
|
|
681
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
682
|
-
*/
|
|
683
|
-
fn('validate-xml-structure', async ({ data, log, activation }) => {
|
|
684
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
685
|
-
// LOGGER ADAPTER PATTERN (Production-Proven)
|
|
686
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
687
|
-
// Create Logger Adapter for type-safe SDK logging
|
|
688
|
-
// Benefits:
|
|
689
|
-
// - Explicit interface contract (TypeScript Logger type)
|
|
690
|
-
// - Centralized logging adapter (easy to modify all log calls)
|
|
691
|
-
// - Type safety enforced
|
|
692
|
-
// - Clear separation between Versori log and SDK Logger
|
|
693
|
-
const logger: Logger = {
|
|
694
|
-
info: (msg: string, meta?: any) => log.info(msg, meta),
|
|
695
|
-
error: (msg: string, error?: Error, meta?: any) => log.error(msg, meta),
|
|
696
|
-
warn: (msg: string, meta?: any) => log.info(msg, meta),
|
|
697
|
-
debug: (msg: string, meta?: any) => log.info(msg, meta)
|
|
698
|
-
};
|
|
699
|
-
|
|
700
|
-
logger.info('Starting SFCC order webhook processing');
|
|
701
|
-
|
|
702
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
703
|
-
// INITIAL DEPLOYMENT MONITORING PATTERN
|
|
704
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
705
|
-
// TEMPORARY: Complete XML log for short-term monitoring (first 2-4 weeks)
|
|
706
|
-
// TODO: Remove this log once integration is stable and monitoring is complete
|
|
707
|
-
// Purpose: Helps catch XML structure issues and field mapping problems early
|
|
708
|
-
// Impact: Increases log volume - remove after stabilization
|
|
709
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
710
|
-
logger.info('Complete incoming XML data', { completeData: data });
|
|
711
|
-
|
|
712
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
713
|
-
// Validate Pre-Parsed XML Object from Versori
|
|
714
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
715
|
-
if (typeof data !== 'object' || data === null || !('orders' in data)) {
|
|
716
|
-
throw new Error(
|
|
717
|
-
`Invalid data format. Expected XML (as pre-parsed object with 'orders' root). ` +
|
|
718
|
-
`Received: ${typeof data}. ` +
|
|
719
|
-
`Please ensure you are sending XML with Content-Type: application/xml.`
|
|
720
|
-
);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
logger.info('XML structure validated successfully');
|
|
724
|
-
|
|
725
|
-
return {
|
|
726
|
-
parsedXml: data,
|
|
727
|
-
logger
|
|
728
|
-
};
|
|
729
|
-
})
|
|
730
|
-
)
|
|
731
|
-
|
|
732
|
-
/**
|
|
733
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
734
|
-
* STEP 2: MAP WITH RESOLVERS USING SDK NODES PATTERN
|
|
735
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
736
|
-
*
|
|
737
|
-
* This step applies field mappings and executes custom resolvers using the SDK's
|
|
738
|
-
* mapWithNodes() method. This method is REQUIRED when using custom resolvers.
|
|
739
|
-
*
|
|
740
|
-
* v0.1.41+ Improvement: mapWithNodes() auto-returns query and variables
|
|
741
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
742
|
-
*/
|
|
743
|
-
.then(
|
|
744
|
-
http('map-with-resolvers', {
|
|
745
|
-
connection: 'fluent_commerce'
|
|
746
|
-
}, async (ctx) => {
|
|
747
|
-
const { data, log, activation } = ctx;
|
|
748
|
-
const { parsedXml, logger } = data;
|
|
749
|
-
|
|
750
|
-
logger.info('Mapping SFCC order to Fluent Commerce with custom resolvers');
|
|
751
|
-
|
|
752
|
-
try {
|
|
753
|
-
const fluentClient = await createClient(ctx);
|
|
754
|
-
|
|
755
|
-
// ═════════════════════════════════════════════════════════════════════
|
|
756
|
-
// INTEGRATION VARIABLES VALIDATION (Fail-Fast Pattern)
|
|
757
|
-
// ═════════════════════════════════════════════════════════════════════
|
|
758
|
-
// Production pattern: Validate required configuration early
|
|
759
|
-
// Benefits:
|
|
760
|
-
// - Fails immediately with explicit error message
|
|
761
|
-
// - Tells user EXACTLY what's wrong and where to fix it
|
|
762
|
-
// - Better than silent fallback to default (hides config issues)
|
|
763
|
-
// - Provides audit trail of successful retrieval
|
|
764
|
-
const fluentRetailerId = activation.getVariable('fluentRetailerId') as string;
|
|
765
|
-
|
|
766
|
-
if (!fluentRetailerId) {
|
|
767
|
-
throw new Error(
|
|
768
|
-
'fluentRetailerId integration variable is required but not set. ' +
|
|
769
|
-
'Please set this variable in Versori Activations > Variables section.'
|
|
770
|
-
);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
logger.info('Using retailer ID from integration variables', { retailerId: fluentRetailerId });
|
|
774
|
-
|
|
775
|
-
// ═════════════════════════════════════════════════════════════════════
|
|
776
|
-
// INITIALIZE MAPPER WITH RESOLVERS IN CONSTRUCTOR (Recommended Pattern)
|
|
777
|
-
// ═════════════════════════════════════════════════════════════════════
|
|
778
|
-
// ✅ CORRECT: Pass resolvers in constructor options
|
|
779
|
-
// This is consistent with UniversalMapper and allows resolvers to be reused
|
|
780
|
-
const mapper = new GraphQLMutationMapper(
|
|
781
|
-
mappingConfig as any,
|
|
782
|
-
logger,
|
|
783
|
-
{
|
|
784
|
-
customResolvers: allResolvers, // ✅ Resolvers in constructor
|
|
785
|
-
fluentClient: fluentClient as any,
|
|
786
|
-
}
|
|
787
|
-
);
|
|
788
|
-
|
|
789
|
-
// ═════════════════════════════════════════════════════════════════════
|
|
790
|
-
// CREATE RESOLVER CONTEXT
|
|
791
|
-
// ═════════════════════════════════════════════════════════════════════
|
|
792
|
-
// Pass configuration to custom resolvers
|
|
793
|
-
// SDK automatically provides helpers (get, ensureArray, logger, etc.)
|
|
794
|
-
// You only need to pass custom config and fluentClient
|
|
795
|
-
const resolverContext = {
|
|
796
|
-
fluentClient: fluentClient as any,
|
|
797
|
-
config: {
|
|
798
|
-
retailerId: fluentRetailerId,
|
|
799
|
-
defaultCountry: 'US',
|
|
800
|
-
companyName: 'YourCompany' // Customize per deployment
|
|
801
|
-
},
|
|
802
|
-
// ✅ SDK automatically provides helpers.get, helpers.ensureArray, helpers.logger
|
|
803
|
-
// No need to manually create them - SDK handles XML attributes via path resolver
|
|
804
|
-
// ✅ Resolvers can access config via helpers.context.config.retailerId
|
|
805
|
-
// OR via the config parameter (3rd param): config.retailerId
|
|
806
|
-
};
|
|
807
|
-
|
|
808
|
-
// Execute mapping WITH custom resolvers
|
|
809
|
-
// ✅ v0.1.41+: mapWithNodes() auto-generates query and variables
|
|
810
|
-
// ✅ Resolvers from constructor are automatically used (can override with 2nd param)
|
|
811
|
-
const mappingResult = await mapper.mapWithNodes(
|
|
812
|
-
parsedXml,
|
|
813
|
-
undefined, // Use constructor resolvers (or pass override resolvers here)
|
|
814
|
-
resolverContext
|
|
815
|
-
);
|
|
816
|
-
|
|
817
|
-
if (!mappingResult.success) {
|
|
818
|
-
throw new Error(`Mapping failed: ${mappingResult.errors?.join(', ')}`);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
const orderRef = mappingResult.data?.input?.ref || 'unknown';
|
|
822
|
-
|
|
823
|
-
logger.info('Successfully mapped order data with custom resolvers', { orderRef });
|
|
824
|
-
|
|
825
|
-
return {
|
|
826
|
-
mappedData: mappingResult.data,
|
|
827
|
-
mappingResult, // ✅ Contains auto-generated query and variables
|
|
828
|
-
orderRef,
|
|
829
|
-
fluentClient,
|
|
830
|
-
logger
|
|
831
|
-
};
|
|
832
|
-
} catch (error) {
|
|
833
|
-
log.error('Error during mapping with resolvers', {
|
|
834
|
-
error: error instanceof Error ? error.message : String(error),
|
|
835
|
-
stack: error instanceof Error ? error.stack : undefined
|
|
836
|
-
});
|
|
837
|
-
throw error;
|
|
838
|
-
}
|
|
839
|
-
})
|
|
840
|
-
)
|
|
841
|
-
|
|
842
|
-
/**
|
|
843
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
844
|
-
* STEP 3: EXECUTE GRAPHQL MUTATION
|
|
845
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
846
|
-
*
|
|
847
|
-
* Execute the GraphQL mutation using auto-generated query from mapWithNodes().
|
|
848
|
-
* v0.1.41+ automatically wraps variables and generates query.
|
|
849
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
850
|
-
*/
|
|
851
|
-
.then(
|
|
852
|
-
http('execute-graphql-mutation', {
|
|
853
|
-
connection: 'fluent_commerce'
|
|
854
|
-
}, async (ctx) => {
|
|
855
|
-
const { data, log } = ctx;
|
|
856
|
-
const { mappingResult, orderRef, fluentClient, logger } = data;
|
|
857
|
-
|
|
858
|
-
logger.info('Executing createOrder GraphQL mutation', { orderRef });
|
|
859
|
-
|
|
860
|
-
try {
|
|
861
|
-
// ✅ v0.1.41+: Use auto-generated query and variables from mapWithNodes()
|
|
862
|
-
// No need to call buildMutation() - query is already generated
|
|
863
|
-
const mutationResult = await (fluentClient as any).graphql({
|
|
864
|
-
query: mappingResult.query, // ✅ Auto-generated mutation query
|
|
865
|
-
variables: mappingResult.variables // ✅ Auto-wrapped variables
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
if (mutationResult.errors && mutationResult.errors.length > 0) {
|
|
869
|
-
logger.error('GraphQL mutation returned errors', {
|
|
870
|
-
errors: mutationResult.errors,
|
|
871
|
-
orderRef
|
|
872
|
-
});
|
|
873
|
-
throw new Error(`GraphQL errors: ${JSON.stringify(mutationResult.errors)}`);
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
const createdOrder = (mutationResult.data as any)?.createOrder;
|
|
877
|
-
|
|
878
|
-
if (!createdOrder) {
|
|
879
|
-
throw new Error('No order data returned from createOrder mutation');
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
// Access fields specified in returnFields (id, ref, status, totalPrice)
|
|
883
|
-
logger.info('Successfully created order in Fluent Commerce', {
|
|
884
|
-
fluentOrderId: createdOrder.id, // From returnFields
|
|
885
|
-
fluentOrderRef: createdOrder.ref, // From returnFields
|
|
886
|
-
status: createdOrder.status, // From returnFields
|
|
887
|
-
totalPrice: createdOrder.totalPrice, // From returnFields
|
|
888
|
-
sfccOrderRef: orderRef
|
|
889
|
-
});
|
|
890
|
-
|
|
891
|
-
return {
|
|
892
|
-
success: true,
|
|
893
|
-
fluentOrderId: createdOrder.id,
|
|
894
|
-
fluentOrderRef: createdOrder.ref,
|
|
895
|
-
sfccOrderRef: orderRef,
|
|
896
|
-
orderData: createdOrder
|
|
897
|
-
};
|
|
898
|
-
} catch (error) {
|
|
899
|
-
log.error('Error executing GraphQL mutation', {
|
|
900
|
-
error: error instanceof Error ? error.message : String(error),
|
|
901
|
-
orderRef
|
|
902
|
-
});
|
|
903
|
-
throw error;
|
|
904
|
-
}
|
|
905
|
-
})
|
|
906
|
-
)
|
|
907
|
-
|
|
908
|
-
/**
|
|
909
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
910
|
-
* STEP 4: BUILD XML RESPONSE
|
|
911
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
912
|
-
*
|
|
913
|
-
* IMPORTANT: Return the XML string here.
|
|
914
|
-
* The onSuccess handler will wrap it in a Response with:
|
|
915
|
-
* - Content-Type: application/xml
|
|
916
|
-
* - Status: 200
|
|
917
|
-
* - X-Execution-Id header
|
|
918
|
-
*
|
|
919
|
-
* This keeps the workflow logic focused on data transformation while
|
|
920
|
-
* the framework handles HTTP response formatting.
|
|
921
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
922
|
-
*/
|
|
923
|
-
.then(
|
|
924
|
-
fn('build-xml-response', ({ data, log }) => {
|
|
925
|
-
log.info('Building XML response for SFCC order processing', {
|
|
926
|
-
fluentOrderId: data.fluentOrderId,
|
|
927
|
-
fluentOrderRef: data.fluentOrderRef,
|
|
928
|
-
sfccOrderRef: data.sfccOrderRef
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
// Initialize XML Builder (SDK-provided)
|
|
932
|
-
const builder = new XMLBuilder({
|
|
933
|
-
xmlDeclaration: true, // Adds <?xml version="1.0" encoding="UTF-8"?>
|
|
934
|
-
prettyPrint: true, // Format with indentation (default: true)
|
|
935
|
-
indent: ' ', // Two-space indentation (default)
|
|
936
|
-
encoding: 'UTF-8' // XML encoding (default)
|
|
937
|
-
});
|
|
938
|
-
|
|
939
|
-
// Build XML response data
|
|
940
|
-
const responseData = {
|
|
941
|
-
Status: 'success',
|
|
942
|
-
Message: 'Order successfully processed and created in Fluent Commerce',
|
|
943
|
-
FluentOrderId: data.fluentOrderId,
|
|
944
|
-
FluentOrderRef: data.fluentOrderRef,
|
|
945
|
-
SFCCOrderRef: data.sfccOrderRef,
|
|
946
|
-
Timestamp: new Date().toISOString()
|
|
947
|
-
};
|
|
948
|
-
|
|
949
|
-
// Build XML with root element
|
|
950
|
-
const xmlString = builder.build(responseData, 'OrderProcessingResponse');
|
|
951
|
-
|
|
952
|
-
log.info('Successfully built XML response', {
|
|
953
|
-
orderRef: data.sfccOrderRef,
|
|
954
|
-
xmlLength: xmlString.length
|
|
955
|
-
});
|
|
956
|
-
|
|
957
|
-
/**
|
|
958
|
-
* Return just the XML string here.
|
|
959
|
-
* The onSuccess handler will wrap it in a Response object with:
|
|
960
|
-
* - Content-Type: application/xml
|
|
961
|
-
* - Status: 200
|
|
962
|
-
* - X-Execution-Id header
|
|
963
|
-
*/
|
|
964
|
-
return xmlString;
|
|
965
|
-
})
|
|
966
|
-
)
|
|
967
|
-
|
|
968
|
-
/**
|
|
969
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
970
|
-
* ERROR HANDLING
|
|
971
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
972
|
-
*
|
|
973
|
-
* Return error XML string that will be wrapped by onError handler with status 500.
|
|
974
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
975
|
-
*/
|
|
976
|
-
.catch(({ data, log }) => {
|
|
977
|
-
log.error('Order processing failed', {
|
|
978
|
-
error: data instanceof Error ? data.message : String(data)
|
|
979
|
-
});
|
|
980
|
-
|
|
981
|
-
/**
|
|
982
|
-
* Build error XML that will be wrapped by onError handler.
|
|
983
|
-
* The onError handler will receive this string in ctx.data and wrap it
|
|
984
|
-
* in a Response object with proper status and headers.
|
|
985
|
-
*/
|
|
986
|
-
const errorXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
987
|
-
<OrderProcessingResponse>
|
|
988
|
-
<Status>error</Status>
|
|
989
|
-
<Message>Failed to process order</Message>
|
|
990
|
-
<Error>${data instanceof Error ? data.message : String(data)}</Error>
|
|
991
|
-
<Timestamp>${new Date().toISOString()}</Timestamp>
|
|
992
|
-
</OrderProcessingResponse>`;
|
|
993
|
-
|
|
994
|
-
/**
|
|
995
|
-
* Return the error XML string.
|
|
996
|
-
* The onError handler will wrap it in a Response with status 500.
|
|
997
|
-
*/
|
|
998
|
-
return errorXml;
|
|
999
|
-
});
|
|
1000
|
-
```
|
|
1001
|
-
|
|
1002
|
-
### 3. Service Implementation (services/order-ingestion.service.ts)
|
|
1003
|
-
|
|
1004
|
-
```typescript
|
|
1005
|
-
/**
|
|
1006
|
-
* SFCC Order Ingestion Service
|
|
1007
|
-
*
|
|
1008
|
-
* Orchestrates the complete order ingestion process:
|
|
1009
|
-
* 1. Validate webhook payload
|
|
1010
|
-
* 2. Validate SFCC structure
|
|
1011
|
-
* 3. Apply field mapping with custom resolvers
|
|
1012
|
-
* 4. Create order via GraphQL API
|
|
1013
|
-
* 5. Audit trail (save input/output files)
|
|
1014
|
-
*/
|
|
1015
|
-
|
|
1016
|
-
import { createClient, GraphQLMutationMapper } from '@fluentcommerce/fc-connect-sdk';
|
|
1017
|
-
import { allResolvers } from '../resolvers';
|
|
1018
|
-
import mappingConfig from '../config/sfcc-to-fluent-order-mapping.json' with { type: 'json' };
|
|
1019
|
-
|
|
1020
|
-
/**
|
|
1021
|
-
* Process SFCC order ingestion
|
|
1022
|
-
*
|
|
1023
|
-
* @param ctx - Versori context object containing fetch, connections, log, activation, data
|
|
1024
|
-
* @param executionStartTime - Workflow start time for duration tracking
|
|
1025
|
-
*/
|
|
1026
|
-
export async function processOrderIngestion(ctx: any, executionStartTime: number) {
|
|
1027
|
-
const { log, data, activation } = ctx;
|
|
1028
|
-
|
|
1029
|
-
log.info('Starting SFCC order processing');
|
|
1030
|
-
|
|
1031
|
-
try {
|
|
1032
|
-
const sfccData = data;
|
|
1033
|
-
|
|
1034
|
-
if (!sfccData) {
|
|
1035
|
-
log.error('No order data received');
|
|
1036
|
-
return {
|
|
1037
|
-
success: false,
|
|
1038
|
-
error: 'No order data received',
|
|
1039
|
-
timestamp: new Date().toISOString(),
|
|
1040
|
-
duration: Date.now() - executionStartTime,
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
if (!sfccData.orders?.order) {
|
|
1045
|
-
log.error('Invalid order structure: missing orders.order');
|
|
1046
|
-
return {
|
|
1047
|
-
success: false,
|
|
1048
|
-
error: 'Invalid order structure: missing orders.order',
|
|
1049
|
-
timestamp: new Date().toISOString(),
|
|
1050
|
-
duration: Date.now() - executionStartTime,
|
|
1051
|
-
};
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
const order = sfccData.orders.order;
|
|
1055
|
-
if (!order['original-order-no']) {
|
|
1056
|
-
log.error('Missing required field: original-order-no');
|
|
1057
|
-
return {
|
|
1058
|
-
success: false,
|
|
1059
|
-
error: 'Missing required field: original-order-no',
|
|
1060
|
-
timestamp: new Date().toISOString(),
|
|
1061
|
-
duration: Date.now() - executionStartTime,
|
|
1062
|
-
};
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
if (!order.customer?.['customer-no']) {
|
|
1066
|
-
log.error('Missing required field: customer.customer-no');
|
|
1067
|
-
return {
|
|
1068
|
-
success: false,
|
|
1069
|
-
error: 'Missing required field: customer.customer-no',
|
|
1070
|
-
timestamp: new Date().toISOString(),
|
|
1071
|
-
duration: Date.now() - executionStartTime,
|
|
1072
|
-
};
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
const fluentClient = await createClient(ctx);
|
|
1076
|
-
log.info('Fluent client initialized');
|
|
1077
|
-
|
|
1078
|
-
log.info('Applying mapping');
|
|
1079
|
-
|
|
1080
|
-
const retailerId = activation?.getVariable('fluentRetailerId') as string;
|
|
1081
|
-
|
|
1082
|
-
if (!retailerId) {
|
|
1083
|
-
log.error('Missing required variable: fluentRetailerId');
|
|
1084
|
-
return {
|
|
1085
|
-
success: false,
|
|
1086
|
-
error: 'Missing required configuration: fluentRetailerId',
|
|
1087
|
-
timestamp: new Date().toISOString(),
|
|
1088
|
-
duration: Date.now() - executionStartTime,
|
|
1089
|
-
};
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
const productCatalogueRef = activation?.getVariable('productCatalogueRef') as string || 'PC:MASTER:2';
|
|
1093
|
-
|
|
1094
|
-
const mapper = new GraphQLMutationMapper(
|
|
1095
|
-
mappingConfig as any,
|
|
1096
|
-
log,
|
|
1097
|
-
{
|
|
1098
|
-
customResolvers: allResolvers,
|
|
1099
|
-
fluentClient: fluentClient as any,
|
|
1100
|
-
}
|
|
1101
|
-
);
|
|
1102
|
-
|
|
1103
|
-
const resolverContext = {
|
|
1104
|
-
fluentClient: fluentClient as any,
|
|
1105
|
-
config: {
|
|
1106
|
-
retailerId: retailerId,
|
|
1107
|
-
defaultOrderType: 'HD',
|
|
1108
|
-
defaultCountry: 'US',
|
|
1109
|
-
productCatalogueRef: productCatalogueRef,
|
|
1110
|
-
},
|
|
1111
|
-
};
|
|
1112
|
-
|
|
1113
|
-
const result = await mapper.mapWithNodes(sfccData, undefined, resolverContext);
|
|
1114
|
-
|
|
1115
|
-
if (!result.success) {
|
|
1116
|
-
log.error('Mapping failed', { errors: result.errors });
|
|
1117
|
-
return {
|
|
1118
|
-
success: false,
|
|
1119
|
-
error: `Mapping failed: ${result.errors?.join(', ')}`,
|
|
1120
|
-
timestamp: new Date().toISOString(),
|
|
1121
|
-
duration: Date.now() - executionStartTime,
|
|
1122
|
-
};
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
const orderRef = sfccData.orders?.order?.['original-order-no'] || 'unknown';
|
|
1126
|
-
|
|
1127
|
-
log.info('Mapping completed', { orderRef });
|
|
1128
|
-
|
|
1129
|
-
// Save audit trail to KV storage (Versori-compatible - file system is read-only)
|
|
1130
|
-
try {
|
|
1131
|
-
const kv = openKv(':project:');
|
|
1132
|
-
const timestamp = new Date().toISOString();
|
|
1133
|
-
const auditKey = ['orders', 'audit', orderRef, timestamp];
|
|
1134
|
-
|
|
1135
|
-
await kv.set([...auditKey, 'sfcc-input'], sfccData);
|
|
1136
|
-
await kv.set([...auditKey, 'fluent-mapped'], result.data);
|
|
1137
|
-
|
|
1138
|
-
log.info('Audit trail saved to KV storage', { orderRef, timestamp });
|
|
1139
|
-
} catch (kvError: any) {
|
|
1140
|
-
log.warn('Failed to save audit trail to KV', { error: kvError.message });
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
log.info('Creating order in Fluent Commerce', { orderRef });
|
|
1144
|
-
|
|
1145
|
-
const mutationRes = await (fluentClient as any).graphql({
|
|
1146
|
-
query: result.query,
|
|
1147
|
-
variables: result.variables
|
|
1148
|
-
});
|
|
1149
|
-
|
|
1150
|
-
if (mutationRes.errors && mutationRes.errors.length > 0) {
|
|
1151
|
-
log.error('GraphQL mutation failed', { errors: mutationRes.errors });
|
|
1152
|
-
|
|
1153
|
-
// Save error to KV storage
|
|
1154
|
-
try {
|
|
1155
|
-
const kv = openKv(':project:');
|
|
1156
|
-
const timestamp = new Date().toISOString();
|
|
1157
|
-
const auditKey = ['orders', 'audit', orderRef, timestamp, 'fluent-error'];
|
|
1158
|
-
await kv.set(auditKey, mutationRes);
|
|
1159
|
-
} catch (kvError) {
|
|
1160
|
-
// Ignore KV save errors
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
return {
|
|
1164
|
-
success: false,
|
|
1165
|
-
error: `GraphQL mutation failed: ${mutationRes.errors.map((e: any) => e.message).join(', ')}`,
|
|
1166
|
-
timestamp: new Date().toISOString(),
|
|
1167
|
-
duration: Date.now() - executionStartTime,
|
|
1168
|
-
};
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
const createOrderData = (mutationRes as any)?.data?.createOrder;
|
|
1172
|
-
|
|
1173
|
-
if (!createOrderData) {
|
|
1174
|
-
log.error('No order data returned from API');
|
|
1175
|
-
return {
|
|
1176
|
-
success: false,
|
|
1177
|
-
error: 'No order data returned from GraphQL mutation',
|
|
1178
|
-
timestamp: new Date().toISOString(),
|
|
1179
|
-
duration: Date.now() - executionStartTime,
|
|
1180
|
-
};
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// Save successful response to KV storage
|
|
1184
|
-
try {
|
|
1185
|
-
const kv = openKv(':project:');
|
|
1186
|
-
const timestamp = new Date().toISOString();
|
|
1187
|
-
const auditKey = ['orders', 'audit', orderRef, timestamp, 'fluent-response'];
|
|
1188
|
-
await kv.set(auditKey, mutationRes);
|
|
1189
|
-
} catch (kvError) {
|
|
1190
|
-
// Ignore KV save errors
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
log.info('Order created in Fluent Commerce', {
|
|
1194
|
-
orderId: createOrderData?.id,
|
|
1195
|
-
orderRef: createOrderData?.ref,
|
|
1196
|
-
});
|
|
1197
|
-
|
|
1198
|
-
return {
|
|
1199
|
-
success: true,
|
|
1200
|
-
data: {
|
|
1201
|
-
sfccOrderRef: orderRef,
|
|
1202
|
-
fluentOrderId: createOrderData?.id,
|
|
1203
|
-
fluentOrderRef: createOrderData?.ref,
|
|
1204
|
-
timestamp: new Date().toISOString(),
|
|
1205
|
-
},
|
|
1206
|
-
duration: Date.now() - executionStartTime,
|
|
1207
|
-
};
|
|
1208
|
-
} catch (error: any) {
|
|
1209
|
-
log.error('Fatal error', {
|
|
1210
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1211
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1212
|
-
});
|
|
1213
|
-
|
|
1214
|
-
return {
|
|
1215
|
-
success: false,
|
|
1216
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1217
|
-
timestamp: new Date().toISOString(),
|
|
1218
|
-
duration: Date.now() - executionStartTime,
|
|
1219
|
-
};
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
```
|
|
1223
|
-
|
|
1224
|
-
### 4. Mapping Configuration (config/sfcc-to-fluent-order-mapping.json)
|
|
1225
|
-
|
|
1226
|
-
```json
|
|
1227
|
-
{
|
|
1228
|
-
"direction": "ingest",
|
|
1229
|
-
"sourceFormat": "xml",
|
|
1230
|
-
"mutation": "createOrder",
|
|
1231
|
-
"returnFields": ["id", "ref", "status", "totalPrice"],
|
|
1232
|
-
"fields": {
|
|
1233
|
-
"ref": {
|
|
1234
|
-
"source": "orders.order.original-order-no",
|
|
1235
|
-
"resolver": "sdk.toString",
|
|
1236
|
-
"comment": "Order reference from SFCC native field (REQUIRED)"
|
|
1237
|
-
},
|
|
1238
|
-
"type": {
|
|
1239
|
-
"resolver": "custom.deriveOrderType",
|
|
1240
|
-
"comment": "Derive from shipment count: 1 shipment = HD, multiple = MULTI (REQUIRED)"
|
|
1241
|
-
},
|
|
1242
|
-
"retailer.id": {
|
|
1243
|
-
"value": "${RETAILER_ID}",
|
|
1244
|
-
"comment": "From environment variable or configuration (REQUIRED)"
|
|
1245
|
-
},
|
|
1246
|
-
"totalPrice": {
|
|
1247
|
-
"source": "orders.order.totals.order-total.net-price",
|
|
1248
|
-
"resolver": "sdk.parseFloat",
|
|
1249
|
-
"comment": "Order total net price from SFCC totals (REQUIRED)"
|
|
1250
|
-
},
|
|
1251
|
-
"totalTaxPrice": {
|
|
1252
|
-
"source": "orders.order.totals.order-total.tax",
|
|
1253
|
-
"resolver": "sdk.parseFloat",
|
|
1254
|
-
"comment": "Order total tax from SFCC totals (REQUIRED)"
|
|
1255
|
-
},
|
|
1256
|
-
"attributes": {
|
|
1257
|
-
"resolver": "custom.buildOrderAttributes",
|
|
1258
|
-
"comment": "Order-level attributes: order-date, created-by, invoice-no, totals JSON"
|
|
1259
|
-
},
|
|
1260
|
-
"customer": {
|
|
1261
|
-
"fields": {
|
|
1262
|
-
"ref": {
|
|
1263
|
-
"source": "orders.order.customer.customer-no",
|
|
1264
|
-
"resolver": "sdk.toString",
|
|
1265
|
-
"comment": "Customer reference from SFCC (REQUIRED)"
|
|
1266
|
-
},
|
|
1267
|
-
"firstName": {
|
|
1268
|
-
"source": "orders.order.customer.customer-name",
|
|
1269
|
-
"resolver": "custom.extractFirstName",
|
|
1270
|
-
"comment": "Extract first name from customer-name"
|
|
1271
|
-
},
|
|
1272
|
-
"lastName": {
|
|
1273
|
-
"source": "orders.order.customer.customer-name",
|
|
1274
|
-
"resolver": "custom.extractLastName",
|
|
1275
|
-
"comment": "Extract last name from customer-name"
|
|
1276
|
-
},
|
|
1277
|
-
"primaryEmail": {
|
|
1278
|
-
"source": "orders.order.customer.customer-email",
|
|
1279
|
-
"resolver": "sdk.toString",
|
|
1280
|
-
"comment": "Customer email from SFCC (REQUIRED)"
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
},
|
|
1284
|
-
"billingAddress": {
|
|
1285
|
-
"fields": {
|
|
1286
|
-
"name": {
|
|
1287
|
-
"source": "orders.order.customer.billing-address.first-name",
|
|
1288
|
-
"resolver": "custom.combineBillingName",
|
|
1289
|
-
"comment": "Combine first-name + last-name"
|
|
1290
|
-
},
|
|
1291
|
-
"street": {
|
|
1292
|
-
"source": "orders.order.customer.billing-address.address1",
|
|
1293
|
-
"resolver": "sdk.toString",
|
|
1294
|
-
"comment": "Billing address line 1 (REQUIRED)"
|
|
1295
|
-
},
|
|
1296
|
-
"street2": {
|
|
1297
|
-
"source": "orders.order.customer.billing-address.address2",
|
|
1298
|
-
"resolver": "sdk.toString",
|
|
1299
|
-
"comment": "Billing address line 2 (optional)"
|
|
1300
|
-
},
|
|
1301
|
-
"city": {
|
|
1302
|
-
"source": "orders.order.customer.billing-address.city",
|
|
1303
|
-
"resolver": "sdk.toString",
|
|
1304
|
-
"comment": "Billing city (REQUIRED)"
|
|
1305
|
-
},
|
|
1306
|
-
"state": {
|
|
1307
|
-
"source": "orders.order.customer.billing-address.state-code",
|
|
1308
|
-
"resolver": "sdk.toString",
|
|
1309
|
-
"comment": "Billing state code (REQUIRED)"
|
|
1310
|
-
},
|
|
1311
|
-
"postcode": {
|
|
1312
|
-
"source": "orders.order.customer.billing-address.postal-code",
|
|
1313
|
-
"resolver": "sdk.toString",
|
|
1314
|
-
"comment": "Billing postal code (REQUIRED)"
|
|
1315
|
-
},
|
|
1316
|
-
"country": {
|
|
1317
|
-
"source": "orders.order.customer.billing-address.country-code",
|
|
1318
|
-
"resolver": "custom.normalizeCountryCode",
|
|
1319
|
-
"comment": "Normalize country code (us → US)"
|
|
1320
|
-
}
|
|
1321
|
-
},
|
|
1322
|
-
"attributes": {
|
|
1323
|
-
"resolver": "custom.buildBillingAddressAttributes",
|
|
1324
|
-
"comment": "Billing address phone and custom attributes"
|
|
1325
|
-
}
|
|
1326
|
-
},
|
|
1327
|
-
"items": {
|
|
1328
|
-
"source": "orders.order.product-lineitems.product-lineitem",
|
|
1329
|
-
"isArray": true,
|
|
1330
|
-
"fields": {
|
|
1331
|
-
"ref": {
|
|
1332
|
-
"source": "$.position",
|
|
1333
|
-
"resolver": "custom.createItemRef",
|
|
1334
|
-
"comment": "Item reference from position (REQUIRED)"
|
|
1335
|
-
},
|
|
1336
|
-
"productRef": {
|
|
1337
|
-
"source": "$.product-id",
|
|
1338
|
-
"resolver": "sdk.toString",
|
|
1339
|
-
"comment": "Product reference from SFCC product-id (REQUIRED)"
|
|
1340
|
-
},
|
|
1341
|
-
"productCatalogueRef": {
|
|
1342
|
-
"resolver": "custom.getProductCatalogueRef",
|
|
1343
|
-
"comment": "Default product catalogue"
|
|
1344
|
-
},
|
|
1345
|
-
"quantity": {
|
|
1346
|
-
"source": "$.quantity",
|
|
1347
|
-
"resolver": "sdk.parseFloat",
|
|
1348
|
-
"comment": "Quantity from SFCC (REQUIRED)"
|
|
1349
|
-
},
|
|
1350
|
-
"price": {
|
|
1351
|
-
"source": "$.base-price",
|
|
1352
|
-
"resolver": "sdk.parseFloat",
|
|
1353
|
-
"comment": "Base price per item from SFCC (REQUIRED)"
|
|
1354
|
-
},
|
|
1355
|
-
"totalPrice": {
|
|
1356
|
-
"source": "$.net-price",
|
|
1357
|
-
"resolver": "sdk.parseFloat",
|
|
1358
|
-
"comment": "Total price (net-price) from SFCC (REQUIRED)"
|
|
1359
|
-
},
|
|
1360
|
-
"taxPrice": {
|
|
1361
|
-
"source": "$.tax",
|
|
1362
|
-
"resolver": "sdk.parseFloat",
|
|
1363
|
-
"comment": "Tax amount from SFCC (REQUIRED)"
|
|
1364
|
-
},
|
|
1365
|
-
"currency": {
|
|
1366
|
-
"source": "orders.order.currency",
|
|
1367
|
-
"resolver": "sdk.toString",
|
|
1368
|
-
"comment": "Currency from order level (REQUIRED)"
|
|
1369
|
-
},
|
|
1370
|
-
"attributes": {
|
|
1371
|
-
"resolver": "custom.buildItemAttributes",
|
|
1372
|
-
"comment": "Item attributes: position, tax-basis, tax-rate, shipment-id, gift, custom-attributes"
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
},
|
|
1376
|
-
"fulfilmentChoices": {
|
|
1377
|
-
"source": "orders.order.shipments.shipment",
|
|
1378
|
-
"isArray": true,
|
|
1379
|
-
"fields": {
|
|
1380
|
-
"type": {
|
|
1381
|
-
"source": "$.shipping-method",
|
|
1382
|
-
"resolver": "custom.mapShippingMethodToFulfilmentType",
|
|
1383
|
-
"comment": "Map ISPU → BOPIS, STANDARD_SHIPPING → SHIP_TO_HOME (REQUIRED)"
|
|
1384
|
-
},
|
|
1385
|
-
"pickupLocationRef": {
|
|
1386
|
-
"source": "$.custom-attributes.custom-attribute[attribute-id=fromStoreId]",
|
|
1387
|
-
"resolver": "sdk.toString",
|
|
1388
|
-
"comment": "Store ID for BOPIS orders"
|
|
1389
|
-
},
|
|
1390
|
-
"deliveryType": {
|
|
1391
|
-
"source": "orders.order.shipping-lineitems.shipping-lineitem.item-id",
|
|
1392
|
-
"resolver": "sdk.toString",
|
|
1393
|
-
"comment": "Delivery type (STANDARD_SHIPPING, etc.)"
|
|
1394
|
-
},
|
|
1395
|
-
"deliveryAddress": {
|
|
1396
|
-
"fields": {
|
|
1397
|
-
"name": {
|
|
1398
|
-
"source": "$.shipping-address.first-name",
|
|
1399
|
-
"resolver": "custom.combineShippingName",
|
|
1400
|
-
"comment": "Combine first-name + last-name"
|
|
1401
|
-
},
|
|
1402
|
-
"street": {
|
|
1403
|
-
"source": "$.shipping-address.address1",
|
|
1404
|
-
"resolver": "sdk.toString",
|
|
1405
|
-
"comment": "Shipping address line 1 (REQUIRED)"
|
|
1406
|
-
},
|
|
1407
|
-
"street2": {
|
|
1408
|
-
"source": "$.shipping-address.address2",
|
|
1409
|
-
"resolver": "sdk.toString",
|
|
1410
|
-
"comment": "Shipping address line 2 (optional)"
|
|
1411
|
-
},
|
|
1412
|
-
"city": {
|
|
1413
|
-
"source": "$.shipping-address.city",
|
|
1414
|
-
"resolver": "sdk.toString",
|
|
1415
|
-
"comment": "Shipping city (REQUIRED)"
|
|
1416
|
-
},
|
|
1417
|
-
"state": {
|
|
1418
|
-
"source": "$.shipping-address.state-code",
|
|
1419
|
-
"resolver": "sdk.toString",
|
|
1420
|
-
"comment": "Shipping state code (REQUIRED)"
|
|
1421
|
-
},
|
|
1422
|
-
"postcode": {
|
|
1423
|
-
"source": "$.shipping-address.postal-code",
|
|
1424
|
-
"resolver": "sdk.toString",
|
|
1425
|
-
"comment": "Shipping postal code (REQUIRED)"
|
|
1426
|
-
},
|
|
1427
|
-
"country": {
|
|
1428
|
-
"source": "$.shipping-address.country-code",
|
|
1429
|
-
"resolver": "custom.normalizeCountryCode",
|
|
1430
|
-
"comment": "Normalize country code (US → US)"
|
|
1431
|
-
}
|
|
1432
|
-
},
|
|
1433
|
-
"attributes": {
|
|
1434
|
-
"resolver": "custom.buildFulfilmentAddressAttributes",
|
|
1435
|
-
"comment": "Shipping address phone and custom attributes"
|
|
1436
|
-
}
|
|
1437
|
-
},
|
|
1438
|
-
"attributes": {
|
|
1439
|
-
"resolver": "custom.buildFulfilmentChoicesAttributes",
|
|
1440
|
-
"comment": "Fulfilment attributes: gift, totals JSON (merchandize-total, shipping-total, shipment-total), custom-attributes"
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
},
|
|
1444
|
-
"financialTransactions": {
|
|
1445
|
-
"source": "orders.order.payments.payment",
|
|
1446
|
-
"isArray": true,
|
|
1447
|
-
"fields": {
|
|
1448
|
-
"ref": {
|
|
1449
|
-
"source": "$.transaction-id",
|
|
1450
|
-
"resolver": "sdk.toString",
|
|
1451
|
-
"comment": "Transaction ID from SFCC (REQUIRED)"
|
|
1452
|
-
},
|
|
1453
|
-
"type": {
|
|
1454
|
-
"value": "AUTHORIZATION",
|
|
1455
|
-
"comment": "Payment type (REQUIRED)"
|
|
1456
|
-
},
|
|
1457
|
-
"amount": {
|
|
1458
|
-
"source": "$.amount",
|
|
1459
|
-
"resolver": "sdk.parseFloat",
|
|
1460
|
-
"comment": "Payment amount from SFCC (REQUIRED)"
|
|
1461
|
-
},
|
|
1462
|
-
"currency": {
|
|
1463
|
-
"source": "orders.order.currency",
|
|
1464
|
-
"resolver": "sdk.toString",
|
|
1465
|
-
"comment": "Currency from order level (REQUIRED)"
|
|
1466
|
-
},
|
|
1467
|
-
"paymentMethod": {
|
|
1468
|
-
"source": "$.custom-method.method-name",
|
|
1469
|
-
"resolver": "custom.mapPaymentMethod",
|
|
1470
|
-
"comment": "Payment method (KLARNA, CreditCard, etc.) (REQUIRED)"
|
|
1471
|
-
},
|
|
1472
|
-
"externalTransactionCode": {
|
|
1473
|
-
"source": "$.custom-attributes.custom-attribute[attribute-id=klarnaAuthorizationCode]",
|
|
1474
|
-
"resolver": "sdk.toString",
|
|
1475
|
-
"comment": "Authorization code (Klarna, etc.)"
|
|
1476
|
-
},
|
|
1477
|
-
"externalTransactionId": {
|
|
1478
|
-
"source": "$.custom-method.custom-attributes.custom-attribute[attribute-id=klarnaClientToken]",
|
|
1479
|
-
"resolver": "sdk.toString",
|
|
1480
|
-
"comment": "External transaction ID (Klarna client token, etc.)"
|
|
1481
|
-
},
|
|
1482
|
-
"attributes": {
|
|
1483
|
-
"resolver": "custom.buildPaymentAttributes",
|
|
1484
|
-
"comment": "Payment attributes: processor-id, custom-attributes"
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
},
|
|
1489
|
-
"returnFields": ["id", "ref", "status", "totalPrice"]
|
|
1490
|
-
}
|
|
1491
|
-
```
|
|
1492
|
-
|
|
1493
|
-
**Note:** The `returnFields` array specifies which fields are returned in the GraphQL mutation response. These fields are available in `mutationRes.data.createOrder` after executing the mutation. If omitted, defaults to `["id", "ref"]`.
|
|
1494
|
-
|
|
1495
|
-
### 5. Custom Resolvers (src/resolvers/order-resolvers.ts)
|
|
1496
|
-
|
|
1497
|
-
```typescript
|
|
1498
|
-
/**
|
|
1499
|
-
* Order Resolvers for SFCC Native → Fluent
|
|
1500
|
-
*
|
|
1501
|
-
* Maps from SFCC native structure to Fluent order fields
|
|
1502
|
-
*/
|
|
1503
|
-
|
|
1504
|
-
import type { ResolverMap } from './types';
|
|
1505
|
-
|
|
1506
|
-
export const orderResolvers: ResolverMap = {
|
|
1507
|
-
/**
|
|
1508
|
-
* Derive order type from SFCC order structure
|
|
1509
|
-
* Example: 1 shipment → "HD", multiple shipments → "MULTI"
|
|
1510
|
-
*/
|
|
1511
|
-
'custom.deriveOrderType': (value: any, data: any, config: any, helpers: any): string => {
|
|
1512
|
-
const sfccData = data.root || data || {};
|
|
1513
|
-
const shipments = helpers.ensureArray(helpers.get(sfccData, 'orders.order.shipments.shipment'));
|
|
1514
|
-
|
|
1515
|
-
if (shipments.length > 1) {
|
|
1516
|
-
return 'MULTI';
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
return config.defaultOrderType || 'HD';
|
|
1520
|
-
},
|
|
1521
|
-
|
|
1522
|
-
/**
|
|
1523
|
-
* Get product catalogue reference
|
|
1524
|
-
* Returns default catalogue ref for all items
|
|
1525
|
-
*/
|
|
1526
|
-
'custom.getProductCatalogueRef': (value: any, data: any, config: any, helpers: any): string => {
|
|
1527
|
-
return process.env.PRODUCT_CATALOGUE_REF || 'PC:MASTER:2';
|
|
1528
|
-
},
|
|
1529
|
-
|
|
1530
|
-
/**
|
|
1531
|
-
* Build order-level attributes from SFCC fields
|
|
1532
|
-
* Extracts order metadata (order-date, created-by, invoice-no, totals)
|
|
1533
|
-
*/
|
|
1534
|
-
'custom.buildOrderAttributes': (
|
|
1535
|
-
value: any,
|
|
1536
|
-
data: any,
|
|
1537
|
-
config: any,
|
|
1538
|
-
helpers: any
|
|
1539
|
-
): Array<{ name: string; type: string; value: any }> => {
|
|
1540
|
-
const sfccData = data.root || data || {};
|
|
1541
|
-
const order = helpers.get(sfccData, 'orders.order');
|
|
1542
|
-
const attributes: Array<{ name: string; type: string; value: any }> = [];
|
|
1543
|
-
|
|
1544
|
-
// Order date
|
|
1545
|
-
const orderDate = helpers.get(order, 'order-date');
|
|
1546
|
-
if (orderDate) {
|
|
1547
|
-
attributes.push({
|
|
1548
|
-
name: 'order-date',
|
|
1549
|
-
type: 'String',
|
|
1550
|
-
value: orderDate,
|
|
1551
|
-
});
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
// Created by
|
|
1555
|
-
const createdBy = helpers.get(order, 'created-by');
|
|
1556
|
-
if (createdBy) {
|
|
1557
|
-
attributes.push({
|
|
1558
|
-
name: 'created-by',
|
|
1559
|
-
type: 'String',
|
|
1560
|
-
value: createdBy,
|
|
1561
|
-
});
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
// Invoice number
|
|
1565
|
-
const invoiceNo = helpers.get(order, 'invoice-no');
|
|
1566
|
-
if (invoiceNo) {
|
|
1567
|
-
attributes.push({
|
|
1568
|
-
name: 'invoice-no',
|
|
1569
|
-
type: 'String',
|
|
1570
|
-
value: invoiceNo,
|
|
1571
|
-
});
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
// Order totals as JSON
|
|
1575
|
-
const totals = helpers.get(order, 'totals');
|
|
1576
|
-
if (totals) {
|
|
1577
|
-
attributes.push({
|
|
1578
|
-
name: 'totals',
|
|
1579
|
-
type: 'JSON',
|
|
1580
|
-
value: JSON.stringify(totals),
|
|
1581
|
-
});
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
return attributes;
|
|
1585
|
-
},
|
|
1586
|
-
};
|
|
1587
|
-
```
|
|
1588
|
-
|
|
1589
|
-
### 6. Additional Resolvers (src/resolvers/fulfillment-resolvers.ts, item-resolvers.ts, payment-resolvers.ts)
|
|
1590
|
-
|
|
1591
|
-
```typescript
|
|
1592
|
-
/**
|
|
1593
|
-
* Fulfillment Resolvers
|
|
1594
|
-
* Handle SFCC shipment data → Fluent fulfilmentChoices
|
|
1595
|
-
*/
|
|
1596
|
-
|
|
1597
|
-
export const fulfillmentResolvers: ResolverMap = {
|
|
1598
|
-
/**
|
|
1599
|
-
* Extract first name from SFCC customer-name field
|
|
1600
|
-
* Example: "John Smith" → "John"
|
|
1601
|
-
*/
|
|
1602
|
-
'custom.extractFirstName': (value: any, data: any, config: any, helpers: any): string => {
|
|
1603
|
-
if (!value) return '';
|
|
1604
|
-
const parts = String(value).trim().split(/\s+/);
|
|
1605
|
-
return parts[0] || '';
|
|
1606
|
-
},
|
|
1607
|
-
|
|
1608
|
-
/**
|
|
1609
|
-
* Extract last name from SFCC customer-name field
|
|
1610
|
-
* Example: "John Smith" → "Smith"
|
|
1611
|
-
*/
|
|
1612
|
-
'custom.extractLastName': (value: any, data: any, config: any, helpers: any): string => {
|
|
1613
|
-
if (!value) return '';
|
|
1614
|
-
const parts = String(value).trim().split(/\s+/);
|
|
1615
|
-
return parts.length > 1 ? parts.slice(1).join(' ') : '';
|
|
1616
|
-
},
|
|
1617
|
-
|
|
1618
|
-
/**
|
|
1619
|
-
* Combine billing address first-name and last-name
|
|
1620
|
-
* Example: "John" + "Smith" → "John Smith"
|
|
1621
|
-
*/
|
|
1622
|
-
'custom.combineBillingName': (value: any, data: any, config: any, helpers: any): string => {
|
|
1623
|
-
const sfccData = data.root || data || {};
|
|
1624
|
-
const firstName = value || helpers.get(sfccData, 'orders.order.customer.billing-address.first-name') || '';
|
|
1625
|
-
const lastName = helpers.get(sfccData, 'orders.order.customer.billing-address.last-name') || '';
|
|
1626
|
-
return `${firstName} ${lastName}`.trim();
|
|
1627
|
-
},
|
|
1628
|
-
|
|
1629
|
-
/**
|
|
1630
|
-
* Combine shipping address first-name and last-name
|
|
1631
|
-
* Example: "Acme" + "Store" → "Acme Store"
|
|
1632
|
-
*/
|
|
1633
|
-
'custom.combineShippingName': (value: any, data: any, config: any, helpers: any): string => {
|
|
1634
|
-
const sfccData = data.root || data || {};
|
|
1635
|
-
const firstName = value || helpers.get(sfccData, 'orders.order.shipments.shipment.shipping-address.first-name') || '';
|
|
1636
|
-
const lastName = helpers.get(sfccData, 'orders.order.shipments.shipment.shipping-address.last-name') || '';
|
|
1637
|
-
return `${firstName} ${lastName}`.trim();
|
|
1638
|
-
},
|
|
1639
|
-
|
|
1640
|
-
/**
|
|
1641
|
-
* Normalize country code to uppercase
|
|
1642
|
-
* Example: "us" → "US", "US" → "US"
|
|
1643
|
-
*/
|
|
1644
|
-
'custom.normalizeCountryCode': (value: any, data: any, config: any, helpers: any): string => {
|
|
1645
|
-
if (!value) return config.defaultCountry || 'US';
|
|
1646
|
-
return String(value).toUpperCase();
|
|
1647
|
-
},
|
|
1648
|
-
|
|
1649
|
-
/**
|
|
1650
|
-
* Map SFCC shipping-method to Fluent fulfilment type
|
|
1651
|
-
* Example: "ISPU" → "BOPIS", "STANDARD_SHIPPING" → "SHIP_TO_HOME"
|
|
1652
|
-
*/
|
|
1653
|
-
'custom.mapShippingMethodToFulfilmentType': (value: any, data: any, config: any, helpers: any): string => {
|
|
1654
|
-
const shippingMethod = String(value || '').toUpperCase();
|
|
1655
|
-
const mapping: Record<string, string> = {
|
|
1656
|
-
ISPU: 'BOPIS',
|
|
1657
|
-
STANDARD_SHIPPING: 'SHIP_TO_HOME',
|
|
1658
|
-
EXPRESS_SHIPPING: 'SHIP_TO_HOME',
|
|
1659
|
-
GROUND_SHIPPING: 'SHIP_TO_HOME',
|
|
1660
|
-
};
|
|
1661
|
-
return mapping[shippingMethod] || 'SHIP_TO_HOME';
|
|
1662
|
-
},
|
|
1663
|
-
|
|
1664
|
-
/**
|
|
1665
|
-
* Build billing address attributes (phone)
|
|
1666
|
-
*/
|
|
1667
|
-
'custom.buildBillingAddressAttributes': (
|
|
1668
|
-
value: any,
|
|
1669
|
-
data: any,
|
|
1670
|
-
config: any,
|
|
1671
|
-
helpers: any
|
|
1672
|
-
): Array<{ name: string; type: string; value: any }> => {
|
|
1673
|
-
const sfccData = data.root || data || {};
|
|
1674
|
-
const attributes: Array<{ name: string; type: string; value: any }> = [];
|
|
1675
|
-
|
|
1676
|
-
const phone = helpers.get(sfccData, 'orders.order.customer.billing-address.phone');
|
|
1677
|
-
if (phone) {
|
|
1678
|
-
attributes.push({ name: 'phone', type: 'String', value: String(phone) });
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
return attributes;
|
|
1682
|
-
},
|
|
1683
|
-
|
|
1684
|
-
/**
|
|
1685
|
-
* Build fulfilment address attributes (phone)
|
|
1686
|
-
*/
|
|
1687
|
-
'custom.buildFulfilmentAddressAttributes': (
|
|
1688
|
-
value: any,
|
|
1689
|
-
data: any,
|
|
1690
|
-
config: any,
|
|
1691
|
-
helpers: any
|
|
1692
|
-
): Array<{ name: string; type: string; value: any }> => {
|
|
1693
|
-
const sfccData = data.root || data || {};
|
|
1694
|
-
const attributes: Array<{ name: string; type: string; value: any }> = [];
|
|
1695
|
-
|
|
1696
|
-
const phone = helpers.get(sfccData, 'orders.order.shipments.shipment.shipping-address.phone');
|
|
1697
|
-
if (phone) {
|
|
1698
|
-
attributes.push({ name: 'phone', type: 'String', value: String(phone) });
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
return attributes;
|
|
1702
|
-
},
|
|
1703
|
-
|
|
1704
|
-
/**
|
|
1705
|
-
* Build fulfilment choices attributes
|
|
1706
|
-
* Includes gift flag, totals JSON, and custom attributes
|
|
1707
|
-
*/
|
|
1708
|
-
'custom.buildFulfilmentChoicesAttributes': (
|
|
1709
|
-
value: any,
|
|
1710
|
-
data: any,
|
|
1711
|
-
config: any,
|
|
1712
|
-
helpers: any
|
|
1713
|
-
): Array<{ name: string; type: string; value: any }> => {
|
|
1714
|
-
const sfccData = data.root || data || {};
|
|
1715
|
-
const attributes: Array<{ name: string; type: string; value: any }> = [];
|
|
1716
|
-
|
|
1717
|
-
// Gift flag
|
|
1718
|
-
const gift = helpers.get(sfccData, 'orders.order.shipments.shipment.gift');
|
|
1719
|
-
if (gift !== undefined) {
|
|
1720
|
-
attributes.push({ name: 'gift', type: 'Boolean', value: gift === 'true' || gift === true });
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
// Shipment totals as JSON
|
|
1724
|
-
const totals = helpers.get(sfccData, 'orders.order.shipments.shipment.totals');
|
|
1725
|
-
if (totals) {
|
|
1726
|
-
attributes.push({
|
|
1727
|
-
name: 'totals',
|
|
1728
|
-
type: 'JSON',
|
|
1729
|
-
value: JSON.stringify(totals),
|
|
1730
|
-
});
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
// Custom attributes (fromStoreId, shipmentType, etc.)
|
|
1734
|
-
const customAttrs = helpers.ensureArray(
|
|
1735
|
-
helpers.get(sfccData, 'orders.order.shipments.shipment.custom-attributes.custom-attribute')
|
|
1736
|
-
);
|
|
1737
|
-
for (const attr of customAttrs) {
|
|
1738
|
-
const attrId = attr['@attribute-id'];
|
|
1739
|
-
const attrValue = attr._ || attr['#text'] || attr;
|
|
1740
|
-
if (attrId && attrValue !== undefined) {
|
|
1741
|
-
attributes.push({
|
|
1742
|
-
name: attrId,
|
|
1743
|
-
type: 'String',
|
|
1744
|
-
value: String(attrValue),
|
|
1745
|
-
});
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
return attributes;
|
|
1750
|
-
},
|
|
1751
|
-
};
|
|
1752
|
-
|
|
1753
|
-
/**
|
|
1754
|
-
* Item Resolvers
|
|
1755
|
-
* Handle SFCC product-lineitem data → Fluent order items
|
|
1756
|
-
*/
|
|
1757
|
-
|
|
1758
|
-
export const itemResolvers: ResolverMap = {
|
|
1759
|
-
/**
|
|
1760
|
-
* Create item reference from position
|
|
1761
|
-
* Example: position "1" → "item_1"
|
|
1762
|
-
*/
|
|
1763
|
-
'custom.createItemRef': (value: any, data: any, config: any, helpers: any): string => {
|
|
1764
|
-
const position = String(value || '1');
|
|
1765
|
-
return `item_${position}`;
|
|
1766
|
-
},
|
|
1767
|
-
|
|
1768
|
-
/**
|
|
1769
|
-
* Build item attributes
|
|
1770
|
-
* Includes position, tax-basis, tax-rate, shipment-id, gift, custom-attributes
|
|
1771
|
-
*/
|
|
1772
|
-
'custom.buildItemAttributes': (
|
|
1773
|
-
value: any,
|
|
1774
|
-
data: any,
|
|
1775
|
-
config: any,
|
|
1776
|
-
helpers: any
|
|
1777
|
-
): Array<{ name: string; type: string; value: any }> => {
|
|
1778
|
-
const itemData = data || {};
|
|
1779
|
-
const sfccData = data.root || data || {};
|
|
1780
|
-
const attributes: Array<{ name: string; type: string; value: any }> = [];
|
|
1781
|
-
|
|
1782
|
-
// Position
|
|
1783
|
-
const position = itemData.position || helpers.get(sfccData, 'orders.order.product-lineitems.product-lineitem.position');
|
|
1784
|
-
if (position) {
|
|
1785
|
-
attributes.push({ name: 'position', type: 'String', value: String(position) });
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
// Tax basis
|
|
1789
|
-
const taxBasis = itemData['tax-basis'] || helpers.get(sfccData, 'orders.order.product-lineitems.product-lineitem.tax-basis');
|
|
1790
|
-
if (taxBasis) {
|
|
1791
|
-
attributes.push({ name: 'tax-basis', type: 'String', value: String(taxBasis) });
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
// Tax rate
|
|
1795
|
-
const taxRate = itemData['tax-rate'] || helpers.get(sfccData, 'orders.order.product-lineitems.product-lineitem.tax-rate');
|
|
1796
|
-
if (taxRate) {
|
|
1797
|
-
attributes.push({ name: 'tax-rate', type: 'String', value: String(taxRate) });
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
// Shipment ID
|
|
1801
|
-
const shipmentId = itemData['shipment-id'] || helpers.get(sfccData, 'orders.order.product-lineitems.product-lineitem.shipment-id');
|
|
1802
|
-
if (shipmentId) {
|
|
1803
|
-
attributes.push({ name: 'shipment-id', type: 'String', value: String(shipmentId) });
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
// Gift flag
|
|
1807
|
-
const gift = itemData.gift || helpers.get(sfccData, 'orders.order.product-lineitems.product-lineitem.gift');
|
|
1808
|
-
if (gift !== undefined) {
|
|
1809
|
-
attributes.push({ name: 'gift', type: 'Boolean', value: gift === 'true' || gift === true });
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
// Custom attributes (fromStoreId, storeQty, etc.)
|
|
1813
|
-
const customAttrs = helpers.ensureArray(
|
|
1814
|
-
itemData['custom-attributes']?.['custom-attribute'] ||
|
|
1815
|
-
helpers.get(sfccData, 'orders.order.product-lineitems.product-lineitem.custom-attributes.custom-attribute')
|
|
1816
|
-
);
|
|
1817
|
-
for (const attr of customAttrs) {
|
|
1818
|
-
const attrId = attr['@attribute-id'];
|
|
1819
|
-
const attrValue = attr._ || attr['#text'] || attr;
|
|
1820
|
-
if (attrId && attrValue !== undefined) {
|
|
1821
|
-
attributes.push({
|
|
1822
|
-
name: attrId,
|
|
1823
|
-
type: 'String',
|
|
1824
|
-
value: String(attrValue),
|
|
1825
|
-
});
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
return attributes;
|
|
1830
|
-
},
|
|
1831
|
-
};
|
|
1832
|
-
|
|
1833
|
-
/**
|
|
1834
|
-
* Payment Resolvers
|
|
1835
|
-
* Handle SFCC payment data → Fluent financialTransactions
|
|
1836
|
-
*/
|
|
1837
|
-
|
|
1838
|
-
export const paymentResolvers: ResolverMap = {
|
|
1839
|
-
/**
|
|
1840
|
-
* Map SFCC payment method to Fluent payment method
|
|
1841
|
-
* Example: "KLARNA" → "KLARNA", "CreditCard" → "CreditCard"
|
|
1842
|
-
*/
|
|
1843
|
-
'custom.mapPaymentMethod': (value: any, data: any, config: any, helpers: any): string => {
|
|
1844
|
-
const methodName = String(value || '').toUpperCase();
|
|
1845
|
-
const mapping: Record<string, string> = {
|
|
1846
|
-
KLARNA: 'KLARNA',
|
|
1847
|
-
CREDITCARD: 'CreditCard',
|
|
1848
|
-
PAYPAL: 'PayPal',
|
|
1849
|
-
APPLEPAY: 'ApplePay',
|
|
1850
|
-
GOOGLEPAY: 'GooglePay',
|
|
1851
|
-
};
|
|
1852
|
-
return mapping[methodName] || methodName;
|
|
1853
|
-
},
|
|
1854
|
-
|
|
1855
|
-
/**
|
|
1856
|
-
* Build payment attributes
|
|
1857
|
-
* Includes processor-id, custom-attributes (klarnaAuthorizationCode, etc.)
|
|
1858
|
-
*/
|
|
1859
|
-
'custom.buildPaymentAttributes': (
|
|
1860
|
-
value: any,
|
|
1861
|
-
data: any,
|
|
1862
|
-
config: any,
|
|
1863
|
-
helpers: any
|
|
1864
|
-
): Array<{ name: string; type: string; value: any }> => {
|
|
1865
|
-
const paymentData = data || {};
|
|
1866
|
-
const sfccData = data.root || data || {};
|
|
1867
|
-
const attributes: Array<{ name: string; type: string; value: any }> = [];
|
|
1868
|
-
|
|
1869
|
-
// Processor ID
|
|
1870
|
-
const processorId = paymentData['processor-id'] || helpers.get(sfccData, 'orders.order.payments.payment.processor-id');
|
|
1871
|
-
if (processorId) {
|
|
1872
|
-
attributes.push({ name: 'processor-id', type: 'String', value: String(processorId) });
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
// Custom attributes
|
|
1876
|
-
const customAttrs = helpers.ensureArray(
|
|
1877
|
-
paymentData['custom-attributes']?.['custom-attribute'] ||
|
|
1878
|
-
helpers.get(sfccData, 'orders.order.payments.payment.custom-attributes.custom-attribute')
|
|
1879
|
-
);
|
|
1880
|
-
for (const attr of customAttrs) {
|
|
1881
|
-
const attrId = attr['@attribute-id'];
|
|
1882
|
-
const attrValue = attr._ || attr['#text'] || attr;
|
|
1883
|
-
if (attrId && attrValue !== undefined) {
|
|
1884
|
-
attributes.push({
|
|
1885
|
-
name: attrId,
|
|
1886
|
-
type: 'String',
|
|
1887
|
-
value: String(attrValue),
|
|
1888
|
-
});
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
return attributes;
|
|
1893
|
-
},
|
|
1894
|
-
};
|
|
1895
|
-
```
|
|
1896
|
-
|
|
1897
|
-
### 7. Resolver Types (src/resolvers/types.ts)
|
|
1898
|
-
|
|
1899
|
-
```typescript
|
|
1900
|
-
/**
|
|
1901
|
-
* Type definitions for custom resolvers
|
|
1902
|
-
*/
|
|
1903
|
-
|
|
1904
|
-
export interface ResolverHelpers {
|
|
1905
|
-
logger?: any;
|
|
1906
|
-
fluentClient?: any;
|
|
1907
|
-
get: (obj: any, path: string) => any;
|
|
1908
|
-
ensureArray: (val: any) => any[];
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
export type ResolverFunction = (
|
|
1912
|
-
value: any,
|
|
1913
|
-
data: any,
|
|
1914
|
-
config: any,
|
|
1915
|
-
helpers: ResolverHelpers
|
|
1916
|
-
) => any | Promise<any>;
|
|
1917
|
-
|
|
1918
|
-
export type ResolverMap = Record<string, ResolverFunction>;
|
|
1919
|
-
```
|
|
1920
|
-
|
|
1921
|
-
### 8. Export All Resolvers (src/resolvers/index.ts)
|
|
1922
|
-
|
|
1923
|
-
```typescript
|
|
1924
|
-
/**
|
|
1925
|
-
* All custom resolvers for SFCC native mapping
|
|
1926
|
-
*/
|
|
1927
|
-
|
|
1928
|
-
import { orderResolvers } from './order-resolvers';
|
|
1929
|
-
import { fulfillmentResolvers } from './fulfillment-resolvers';
|
|
1930
|
-
import { itemResolvers } from './item-resolvers';
|
|
1931
|
-
import { paymentResolvers } from './payment-resolvers';
|
|
1932
|
-
|
|
1933
|
-
export const allResolvers = {
|
|
1934
|
-
...orderResolvers,
|
|
1935
|
-
...fulfillmentResolvers,
|
|
1936
|
-
...itemResolvers,
|
|
1937
|
-
...paymentResolvers,
|
|
1938
|
-
};
|
|
1939
|
-
|
|
1940
|
-
// Export individual resolver maps for testing
|
|
1941
|
-
export { orderResolvers, fulfillmentResolvers, itemResolvers, paymentResolvers };
|
|
1942
|
-
```
|
|
1943
|
-
|
|
1944
|
-
### 9. Versori Deployment Files
|
|
1945
|
-
|
|
1946
|
-
#### package.json
|
|
1947
|
-
|
|
1948
|
-
```json
|
|
1949
|
-
{
|
|
1950
|
-
"name": "sfcc-order-webhook",
|
|
1951
|
-
"version": "1.0.0",
|
|
1952
|
-
"type": "module",
|
|
1953
|
-
"dependencies": {
|
|
1954
|
-
"@fluentcommerce/fc-connect-sdk": "^0.1.41",
|
|
1955
|
-
"@versori/run": "latest"
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
```
|
|
1959
|
-
|
|
1960
|
-
#### import_map.json
|
|
1961
|
-
|
|
1962
|
-
```json
|
|
1963
|
-
{
|
|
1964
|
-
"imports": {
|
|
1965
|
-
"@fluentcommerce/fc-connect-sdk": "https://esm.sh/@fluentcommerce/fc-connect-sdk@0.1.41",
|
|
1966
|
-
"@versori/run": "https://esm.sh/@versori/run@latest",
|
|
1967
|
-
"node:buffer": "https://deno.land/std@0.224.0/node/buffer.ts",
|
|
1968
|
-
"node:fs": "https://deno.land/std@0.224.0/node/fs.ts",
|
|
1969
|
-
"node:path": "https://deno.land/std@0.224.0/node/path.ts"
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
```
|
|
1973
|
-
|
|
1974
|
-
---
|
|
1975
|
-
|
|
1976
|
-
## Common Issues
|
|
1977
|
-
|
|
1978
|
-
### Issue 1: "Missing required field: orders.order"
|
|
1979
|
-
|
|
1980
|
-
**Cause**: XML structure doesn't match expected SFCC format
|
|
1981
|
-
|
|
1982
|
-
**Solution**: Validate XML structure
|
|
1983
|
-
|
|
1984
|
-
```typescript
|
|
1985
|
-
// Check structure
|
|
1986
|
-
if (!sfccData.orders?.order) {
|
|
1987
|
-
throw new Error('Invalid XML structure: missing orders.order');
|
|
1988
|
-
}
|
|
1989
|
-
```
|
|
1990
|
-
|
|
1991
|
-
### Issue 2: "Order type is undefined"
|
|
1992
|
-
|
|
1993
|
-
**Cause**: Resolver not deriving order type correctly
|
|
1994
|
-
|
|
1995
|
-
**Solution**: Check shipments array
|
|
1996
|
-
|
|
1997
|
-
```typescript
|
|
1998
|
-
const shipments = helpers.ensureArray(helpers.get(sfccData, 'orders.order.shipments.shipment'));
|
|
1999
|
-
return shipments.length > 1 ? 'MULTI' : 'HD';
|
|
2000
|
-
```
|
|
2001
|
-
|
|
2002
|
-
### Issue 3: "Expected array at path but got object"
|
|
2003
|
-
|
|
2004
|
-
**Cause**: XML parser doesn't create arrays for single items
|
|
2005
|
-
|
|
2006
|
-
**Solution**: Use `helpers.ensureArray()` in resolvers
|
|
2007
|
-
|
|
2008
|
-
```typescript
|
|
2009
|
-
// ✅ CORRECT - wraps single items
|
|
2010
|
-
const items = helpers.ensureArray(helpers.get(sfccData, 'orders.order.product-lineitems.product-lineitem'));
|
|
2011
|
-
```
|
|
2012
|
-
|
|
2013
|
-
---
|
|
2014
|
-
|
|
2015
|
-
## Production Checklist
|
|
2016
|
-
|
|
2017
|
-
Before deploying to production:
|
|
2018
|
-
|
|
2019
|
-
- [ ] Set proper environment variables in Versori (RETAILER_ID, PRODUCT_CATALOGUE_REF)
|
|
2020
|
-
- [ ] Configure error alerting/monitoring
|
|
2021
|
-
- [ ] Test with real SFCC webhook payload
|
|
2022
|
-
- [ ] Test all resolver edge cases (missing data, null values)
|
|
2023
|
-
- [ ] Set up log aggregation
|
|
2024
|
-
- [ ] Configure connection retry logic for transient failures
|
|
2025
|
-
- [ ] Document any customer-specific field mappings
|
|
2026
|
-
|
|
2027
|
-
---
|
|
2028
|
-
|
|
2029
|
-
**Next Steps**: For more advanced SFCC integration patterns, explore the Radial XML version of this template for orders requiring Radial-specific fields.
|
|
1
|
+
---
|
|
2
|
+
template_id: tpl-webhook-xml-order-ingestion-sfcc
|
|
3
|
+
canonical_filename: template-webhook-xml-order-ingestion.md
|
|
4
|
+
sdk_version: ^0.1.41
|
|
5
|
+
runtime: versori
|
|
6
|
+
direction: ingestion
|
|
7
|
+
source: webhook-xml-sfcc
|
|
8
|
+
destination: fluent-graphql
|
|
9
|
+
entity: order
|
|
10
|
+
format: xml
|
|
11
|
+
logging: versori
|
|
12
|
+
status: production-ready
|
|
13
|
+
last_updated: 2025-11-13
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Template: Webhook - SFCC XML Order Ingestion
|
|
17
|
+
|
|
18
|
+
**FC Connect SDK Use Case Guide**
|
|
19
|
+
|
|
20
|
+
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
21
|
+
> **Version**: Use latest - `npm install @fluentcommerce/fc-connect-sdk@latest`
|
|
22
|
+
|
|
23
|
+
**Context**: Receive SFCC XML orders via Versori HTTP webhook and create orders in Fluent Commerce using GraphQL mutations.
|
|
24
|
+
|
|
25
|
+
**Complexity**: Medium
|
|
26
|
+
|
|
27
|
+
**Runtime**: Versori Platform
|
|
28
|
+
|
|
29
|
+
**Estimated Lines**: ~500 lines (modular structure)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## STEP 1: Understand This Template
|
|
34
|
+
|
|
35
|
+
**What This Template Does:**
|
|
36
|
+
|
|
37
|
+
- Versori HTTP webhook endpoint receiving XML order data (SFCC native format)
|
|
38
|
+
- XML parsing with automatic Versori XML-to-object conversion (incoming XML)
|
|
39
|
+
- GraphQL mutation mapping from SFCC native XML structure to Fluent schema
|
|
40
|
+
- Uses ONLY SFCC native fields (orders.order.*) - no Radial XML dependency
|
|
41
|
+
- Custom resolvers for data transformation, name parsing, and order type derivation
|
|
42
|
+
- Error handling and structured response formatting
|
|
43
|
+
- **Response Options**: JSON (default) or XML (with custom Response handler)
|
|
44
|
+
- **Sync + Fire-and-Forget Pattern**: Fast webhook response, background processing
|
|
45
|
+
|
|
46
|
+
**Key SDK Components:**
|
|
47
|
+
|
|
48
|
+
- `createClient()` - Universal client factory (auto-detects Versori context)
|
|
49
|
+
- `GraphQLMutationMapper` - XML/JSON → GraphQL mutation mapping
|
|
50
|
+
- Native Versori `log` - Use `log` from context
|
|
51
|
+
- No XML parsing needed (Versori handles automatically)
|
|
52
|
+
|
|
53
|
+
**Entity Type:**
|
|
54
|
+
|
|
55
|
+
- **Order** - Fluent entity for order creation
|
|
56
|
+
- **GraphQL Mutation** - Uses `createOrder` mutation (not Event API)
|
|
57
|
+
|
|
58
|
+
**Critical Patterns:**
|
|
59
|
+
|
|
60
|
+
- **XML Response Pattern**: Production-ready XML response handling with proper Content-Type headers
|
|
61
|
+
- **Logger Adapter**: Type-safe logging with SDK Logger interface
|
|
62
|
+
- **Sync + Fire-and-Forget**: Webhook validates quickly, returns immediately, processes in background
|
|
63
|
+
- **External JSON Config**: Mapping configuration in separate JSON file (`config/sfcc-to-fluent-order-mapping.json`)
|
|
64
|
+
- **Modular Architecture**: Separate services, workflows, config, types folders
|
|
65
|
+
- **Integration Variables Validation**: Fail-fast validation of required configuration
|
|
66
|
+
- **Initial Deployment Monitoring**: Temporary full payload logging for troubleshooting
|
|
67
|
+
- **SFCC Native Only**: No Radial XML dependency - simpler and more maintainable
|
|
68
|
+
|
|
69
|
+
**When to Use This Template:**
|
|
70
|
+
|
|
71
|
+
- ✅ SFCC native XML order ingestion (orders.order.* structure)
|
|
72
|
+
- ✅ Need fast webhook response (don't wait for order creation)
|
|
73
|
+
- ✅ Single order per webhook call
|
|
74
|
+
- ✅ BOPIS (Buy Online Pick Up In Store) and standard shipping orders
|
|
75
|
+
- ✅ Custom resolvers for complex transformations (name parsing, address normalization)
|
|
76
|
+
- ✅ Want simpler template without Radial XML complexity
|
|
77
|
+
|
|
78
|
+
**When NOT to Use:**
|
|
79
|
+
|
|
80
|
+
- ❌ Bulk order processing (use Batch API or scheduled workflows)
|
|
81
|
+
- ❌ Order updates (use GraphQL `updateOrder` mutation)
|
|
82
|
+
- ❌ CSV/JSON formats (use appropriate format template)
|
|
83
|
+
- ❌ Need synchronous order creation (wait for result before responding)
|
|
84
|
+
- ❌ Orders requiring Radial-specific fields (use Radial template instead)
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## STEP 2: Implementation Prompt for Claude Code
|
|
89
|
+
|
|
90
|
+
**Copy this prompt and send to Claude Code to generate the complete implementation:**
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
Create a Versori webhook workflow for SFCC XML order ingestion to Fluent Commerce GraphQL.
|
|
94
|
+
|
|
95
|
+
REQUIREMENTS:
|
|
96
|
+
1. Runtime: Versori Platform (HTTP webhook)
|
|
97
|
+
2. Source: SFCC XML order data via HTTP POST webhook
|
|
98
|
+
3. Destination: Fluent Commerce GraphQL API (createOrder mutation)
|
|
99
|
+
4. Format: XML (Versori auto-parses to object)
|
|
100
|
+
5. Entity: Order (GraphQL mutation)
|
|
101
|
+
|
|
102
|
+
KEY FEATURES:
|
|
103
|
+
- Sync + fire-and-forget pattern (fast webhook response, background processing)
|
|
104
|
+
- External JSON mapping configuration (config/sfcc-to-fluent-order-mapping.json)
|
|
105
|
+
- Modular architecture (workflows/, services/, config/, types/)
|
|
106
|
+
- SFCC native fields ONLY (no Radial XML)
|
|
107
|
+
- GraphQLMutationMapper with standard map() method (no nodes)
|
|
108
|
+
- Custom resolvers for data transformation
|
|
109
|
+
- Audit trail (save input/output files)
|
|
110
|
+
- Comprehensive error handling with structured logging
|
|
111
|
+
|
|
112
|
+
CRITICAL REQUIREMENTS:
|
|
113
|
+
1. Webhook Mode: response: { mode: 'sync' } with XML onSuccess/onError handlers
|
|
114
|
+
2. Response Format: XML with proper Content-Type headers (production standard for SFCC)
|
|
115
|
+
3. Logger Adapter: Create typed Logger adapter from Versori log
|
|
116
|
+
4. Background Processing: Fire-and-forget pattern (no await on long operations)
|
|
117
|
+
5. Mapping Config: External JSON file (config/sfcc-to-fluent-order-mapping.json)
|
|
118
|
+
6. Custom Resolvers: USE mapWithNodes() when you have custom resolvers (REQUIRED)
|
|
119
|
+
7. Integration Variables: Fail-fast validation of required config (fluentRetailerId)
|
|
120
|
+
8. Modular Structure: Separate services/, config/, types/ folders
|
|
121
|
+
9. Step Documentation: Use visual separator blocks for each workflow step
|
|
122
|
+
10. Error Handling: Structured error responses with recommendations
|
|
123
|
+
|
|
124
|
+
SDK METHODS TO USE (v0.1.41+):
|
|
125
|
+
- createClient({ ...ctx, log }) - Pass full Versori context
|
|
126
|
+
- Logger adapter pattern - Type-safe logging wrapper
|
|
127
|
+
- new GraphQLMutationMapper(mappingConfig, logger, { customResolvers, fluentClient: client }) - Initialize mapper with resolvers (RECOMMENDED)
|
|
128
|
+
- mapper.mapWithNodes(xmlData, undefined, context) - Map with constructor resolvers (auto-returns query)
|
|
129
|
+
- result.query and result.variables - Auto-generated from mapWithNodes()
|
|
130
|
+
- client.graphql({ query: result.query, variables: result.variables }) - Execute mutation
|
|
131
|
+
|
|
132
|
+
FORBIDDEN PATTERNS:
|
|
133
|
+
- ❌ Inline mapping config (use external JSON)
|
|
134
|
+
- ❌ await on background processing (use fire-and-forget)
|
|
135
|
+
- ❌ Passing log directly to SDK (use Logger adapter)
|
|
136
|
+
- ❌ All code in one file (use modular structure)
|
|
137
|
+
- ❌ async mode webhook (use sync + fire-and-forget)
|
|
138
|
+
- ❌ Radial XML references (use SFCC native only)
|
|
139
|
+
- ❌ buildMutation() calls (mapWithNodes() auto-generates query in v0.1.41+)
|
|
140
|
+
- ❌ Silent config fallbacks (fail-fast on missing required variables)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## STEP 3: Detailed Flow Documentation
|
|
146
|
+
|
|
147
|
+
### Complete Processing Flow
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
151
|
+
│ 1. WEBHOOK RECEIVED │
|
|
152
|
+
│ POST https://{workspace}.versori.run/sfcc-order-create │
|
|
153
|
+
│ Content-Type: application/xml │
|
|
154
|
+
│ Body: <orders xmlns="..."><order order-no="..."> │
|
|
155
|
+
│ <original-order-no>...</original-order-no> │
|
|
156
|
+
│ <customer>...</customer> │
|
|
157
|
+
│ <product-lineitems>...</product-lineitems> │
|
|
158
|
+
│ <shipments>...</shipments> │
|
|
159
|
+
│ <totals>...</totals> │
|
|
160
|
+
│ <payments>...</payments> │
|
|
161
|
+
│ </order></orders> │
|
|
162
|
+
└────────────────────┬────────────────────────────────────────┘
|
|
163
|
+
│
|
|
164
|
+
▼
|
|
165
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
166
|
+
│ 2. QUICK VALIDATION (Synchronous, ~10-50ms) │
|
|
167
|
+
│ - Check fluent_commerce connection exists │
|
|
168
|
+
│ - Validate SFCC XML payload present │
|
|
169
|
+
│ - Validate required fields (original-order-no, customer) │
|
|
170
|
+
│ - Return HTTP 200 OK immediately │
|
|
171
|
+
└────────────────────┬────────────────────────────────────────┘
|
|
172
|
+
│
|
|
173
|
+
▼
|
|
174
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
175
|
+
│ 3. BACKGROUND PROCESSING (Fire-and-Forget) │
|
|
176
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
177
|
+
│ │ 3a. Initialize Fluent Client │ │
|
|
178
|
+
│ │ - createClient({ ...ctx, log }) │ │
|
|
179
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
180
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
181
|
+
│ │ 3b. Validate SFCC Structure │ │
|
|
182
|
+
│ │ - Check required SFCC native fields │ │
|
|
183
|
+
│ │ - Validate order structure │ │
|
|
184
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
185
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
186
|
+
│ │ 3c. Map SFCC Native XML to GraphQL Variables │ │
|
|
187
|
+
│ │ - Load mapping config from JSON │ │
|
|
188
|
+
│ │ - Use SFCC native fields ONLY (orders.order.*) │ │
|
|
189
|
+
│ │ - GraphQLMutationMapper.mapWithNodes() (REQUIRED for custom resolvers)│ │
|
|
190
|
+
│ │ - Apply custom resolvers │ │
|
|
191
|
+
│ │ - Returns { query, variables } (GraphQLPayload) │ │
|
|
192
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
193
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
194
|
+
│ │ 3d. Execute GraphQL Mutation │ │
|
|
195
|
+
│ │ - client.graphql(payload) - one simple step! │ │
|
|
196
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
197
|
+
└─────────────────────────────────────────────────────────────┘
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Response Timing
|
|
201
|
+
|
|
202
|
+
| Stage | Timing | Blocking |
|
|
203
|
+
|-------|--------|----------|
|
|
204
|
+
| **Webhook Validation** | ~10-50ms | ✅ Yes (blocks response) |
|
|
205
|
+
| **Background Processing** | ~1000-2000ms | ❌ No (fire-and-forget) |
|
|
206
|
+
| **Total Response Time** | ~10-50ms | ✅ Fast response |
|
|
207
|
+
|
|
208
|
+
**Key Benefit**: Webhook caller receives immediate acknowledgment (~50ms) while order creation happens in background (~1-2s).
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## STEP 4: Production Modular Structure
|
|
213
|
+
|
|
214
|
+
> **✅ This section shows the COMPLETE production-ready modular structure.**
|
|
215
|
+
> All files are shown with proper imports/exports and folder organization.
|
|
216
|
+
|
|
217
|
+
### Complete Project Structure
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
sfcc-xml-order-ingestion/
|
|
221
|
+
├── package.json # Dependencies and Versori config
|
|
222
|
+
├── index.ts # Entry point - exports all workflows
|
|
223
|
+
└── src/
|
|
224
|
+
├── workflows/
|
|
225
|
+
│ └── webhook/
|
|
226
|
+
│ └── order-ingestion.ts # Webhook: Receive SFCC XML orders
|
|
227
|
+
│
|
|
228
|
+
├── services/
|
|
229
|
+
│ └── order-processing.service.ts # Shared orchestration logic (reusable)
|
|
230
|
+
│
|
|
231
|
+
├── resolvers/
|
|
232
|
+
│ ├── index.ts # Export all resolvers
|
|
233
|
+
│ ├── types.ts # TypeScript interfaces
|
|
234
|
+
│ ├── order-resolvers.ts # Order-level resolvers
|
|
235
|
+
│ ├── fulfillment-resolvers.ts # Shipment/fulfillment resolvers
|
|
236
|
+
│ ├── item-resolvers.ts # Product line item resolvers
|
|
237
|
+
│ └── payment-resolvers.ts # Payment resolvers
|
|
238
|
+
│
|
|
239
|
+
├── config/
|
|
240
|
+
│ └── sfcc-to-fluent-order-mapping.json # Mapping configuration (external JSON)
|
|
241
|
+
│
|
|
242
|
+
└── types/
|
|
243
|
+
└── order.types.ts # TypeScript interfaces
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Why This Structure?**
|
|
247
|
+
|
|
248
|
+
- ✅ **Clear separation**: Webhook handlers vs business logic
|
|
249
|
+
- ✅ **Reusable services**: Order processing logic can be reused
|
|
250
|
+
- ✅ **External config**: Mapping changes don't require code changes
|
|
251
|
+
- ✅ **Custom resolvers**: Separate file for complex transformations
|
|
252
|
+
- ✅ **Type safety**: TypeScript interfaces for better IDE support
|
|
253
|
+
- ✅ **Scalable**: Easy to add new webhooks or services
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## CRITICAL: XML Response Architecture (Production Pattern)
|
|
260
|
+
|
|
261
|
+
### Overview
|
|
262
|
+
|
|
263
|
+
This template uses **XML responses** as the PRIMARY pattern for SFCC integration. This is the production-proven approach used in real SFCC → Fluent integrations.
|
|
264
|
+
|
|
265
|
+
### How XML Response Handling Works
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
269
|
+
│ ARCHITECTURE FLOW │
|
|
270
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
271
|
+
│ │
|
|
272
|
+
│ 1. Workflow Steps Return XML Strings │
|
|
273
|
+
│ ↓ ctx.data = "<OrderProcessingResponse>...</>" │
|
|
274
|
+
│ │
|
|
275
|
+
│ 2. onSuccess Handler Receives Context │
|
|
276
|
+
│ ↓ ctx = { data: xmlString, executionId: "..." } │
|
|
277
|
+
│ │
|
|
278
|
+
│ 3. Handler Wraps in Response Object │
|
|
279
|
+
│ ↓ new Response(ctx.data, { headers: {...} }) │
|
|
280
|
+
│ │
|
|
281
|
+
│ 4. Framework Streams Response Directly │
|
|
282
|
+
│ ↓ No JSON encoding - pure XML stream │
|
|
283
|
+
│ │
|
|
284
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Response Format Examples
|
|
288
|
+
|
|
289
|
+
**Success Response:**
|
|
290
|
+
```xml
|
|
291
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
292
|
+
<OrderProcessingResponse>
|
|
293
|
+
<Status>success</Status>
|
|
294
|
+
<Message>Order successfully processed and created in Fluent Commerce</Message>
|
|
295
|
+
<FluentOrderId>6370</FluentOrderId>
|
|
296
|
+
<FluentOrderRef>0017326966182_G_postman_test_1760703173226</FluentOrderRef>
|
|
297
|
+
<SFCCOrderRef>0017326966182_G_postman_test_1760703173226</SFCCOrderRef>
|
|
298
|
+
<Timestamp>2025-11-13T12:12:54.229Z</Timestamp>
|
|
299
|
+
</OrderProcessingResponse>
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Error Response:**
|
|
303
|
+
```xml
|
|
304
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
305
|
+
<OrderProcessingResponse>
|
|
306
|
+
<Status>error</Status>
|
|
307
|
+
<Message>Failed to process order</Message>
|
|
308
|
+
<Error>Missing required field: original-order-no</Error>
|
|
309
|
+
<Timestamp>2025-11-13T12:12:54.229Z</Timestamp>
|
|
310
|
+
</OrderProcessingResponse>
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Key Benefits
|
|
314
|
+
|
|
315
|
+
- ✅ **SFCC Native**: SFCC systems expect XML responses
|
|
316
|
+
- ✅ **Type Safety**: Structured XML schema
|
|
317
|
+
- ✅ **Execution Tracking**: X-Execution-Id header for troubleshooting
|
|
318
|
+
- ✅ **Proper Content-Type**: application/xml; charset=utf-8
|
|
319
|
+
- ✅ **Framework Streaming**: Direct XML stream (no JSON wrapper)
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## XML Handling Patterns
|
|
324
|
+
|
|
325
|
+
### Incoming XML (Versori Auto-Parsing)
|
|
326
|
+
|
|
327
|
+
**CRITICAL**: Versori automatically parses XML when `Content-Type: application/xml` is set.
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// ✅ CORRECT - Versori auto-parses XML into object
|
|
331
|
+
// When SFCC sends: POST /webhook with Content-Type: application/xml
|
|
332
|
+
// Versori automatically converts XML string → parsed object
|
|
333
|
+
// ctx.data is already a parsed object, NOT a string
|
|
334
|
+
|
|
335
|
+
export const webhook = webhook('order-ingestion', { response: { mode: 'sync' } })
|
|
336
|
+
.then(http('process', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
337
|
+
// ctx.data is already parsed! No manual parsing needed
|
|
338
|
+
const sfccData = ctx.data; // Already an object: { orders: { order: {...} } }
|
|
339
|
+
|
|
340
|
+
// Validate structure
|
|
341
|
+
if (!sfccData.orders?.order) {
|
|
342
|
+
throw new Error('Invalid XML structure: missing orders.order');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Use mapWithNodes() because we have custom resolvers
|
|
346
|
+
// ✅ Resolvers passed in constructor, so pass undefined here (or override if needed)
|
|
347
|
+
const result = await mapper.mapWithNodes(sfccData, undefined, context);
|
|
348
|
+
}));
|
|
349
|
+
|
|
350
|
+
// ❌ WRONG - Don't try to parse manually
|
|
351
|
+
const xmlString = ctx.data; // This is already parsed!
|
|
352
|
+
const parsed = parseXML(xmlString); // Unnecessary!
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**Why This Matters:**
|
|
356
|
+
- Versori handles XML parsing automatically
|
|
357
|
+
- `ctx.data` is always a parsed object when Content-Type is `application/xml`
|
|
358
|
+
- No need for `XMLParserService` or manual parsing in Versori workflows
|
|
359
|
+
- Simply validate the structure and use directly
|
|
360
|
+
|
|
361
|
+
### Outgoing XML (Production-Ready Pattern)
|
|
362
|
+
|
|
363
|
+
**RECOMMENDED FOR SFCC**: Use XML responses with custom handlers for production SFCC integrations.
|
|
364
|
+
|
|
365
|
+
**XML Response Pattern (RECOMMENDED)**
|
|
366
|
+
```typescript
|
|
367
|
+
import { XMLBuilder } from '@fluentcommerce/fc-connect-sdk';
|
|
368
|
+
|
|
369
|
+
export const webhook = webhook('order-ingestion', {
|
|
370
|
+
response: {
|
|
371
|
+
mode: 'sync',
|
|
372
|
+
/**
|
|
373
|
+
* onSuccess Handler for XML Response
|
|
374
|
+
*
|
|
375
|
+
* This handler wraps the XML response string with proper headers.
|
|
376
|
+
* The workflow steps return XML strings via ctx.data, and this
|
|
377
|
+
* handler wraps them in Response objects for proper streaming.
|
|
378
|
+
*
|
|
379
|
+
* @param ctx - Context containing:
|
|
380
|
+
* - ctx.data: The XML response string from the final workflow step
|
|
381
|
+
* - ctx.executionId: Unique identifier for this execution
|
|
382
|
+
* @returns Response object with XML body and proper headers
|
|
383
|
+
*/
|
|
384
|
+
onSuccess: (ctx) => new Response(ctx.data, {
|
|
385
|
+
status: 200,
|
|
386
|
+
headers: {
|
|
387
|
+
'Content-Type': 'application/xml; charset=utf-8',
|
|
388
|
+
'X-Execution-Id': ctx.executionId
|
|
389
|
+
}
|
|
390
|
+
}),
|
|
391
|
+
/**
|
|
392
|
+
* onError Handler for XML Error Response
|
|
393
|
+
*
|
|
394
|
+
* This handler wraps error responses with proper status and headers.
|
|
395
|
+
* Returns XML format for consistent error responses.
|
|
396
|
+
*
|
|
397
|
+
* @param ctx - Context containing:
|
|
398
|
+
* - ctx.data: The error XML string from the .catch() step
|
|
399
|
+
* - ctx.executionId: Unique identifier for tracking failed executions
|
|
400
|
+
* @returns Response object with error XML and 500 status
|
|
401
|
+
*/
|
|
402
|
+
onError: (ctx) => new Response(ctx.data, {
|
|
403
|
+
status: 500,
|
|
404
|
+
headers: {
|
|
405
|
+
'Content-Type': 'application/xml; charset=utf-8',
|
|
406
|
+
'X-Execution-Id': ctx.executionId
|
|
407
|
+
}
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
})
|
|
411
|
+
.then(http('process', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
412
|
+
// Process order...
|
|
413
|
+
const orderId = '12345';
|
|
414
|
+
const orderRef = 'ORD-001';
|
|
415
|
+
|
|
416
|
+
// Build XML response string
|
|
417
|
+
const builder = new XMLBuilder({
|
|
418
|
+
xmlDeclaration: true, // Adds <?xml version="1.0" encoding="UTF-8"?>
|
|
419
|
+
prettyPrint: true, // Format with indentation
|
|
420
|
+
encoding: 'UTF-8'
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const responseData = {
|
|
424
|
+
Status: 'success',
|
|
425
|
+
FluentOrderId: orderId,
|
|
426
|
+
FluentOrderRef: orderRef,
|
|
427
|
+
Timestamp: new Date().toISOString()
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// Return XML string (onSuccess handler wraps it in Response)
|
|
431
|
+
return builder.build(responseData, 'OrderProcessingResponse');
|
|
432
|
+
}))
|
|
433
|
+
.catch(({ data, log }) => {
|
|
434
|
+
// Return error XML string (onError handler wraps it)
|
|
435
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
436
|
+
<OrderProcessingResponse>
|
|
437
|
+
<Status>error</Status>
|
|
438
|
+
<Error>${data instanceof Error ? data.message : String(data)}</Error>
|
|
439
|
+
</OrderProcessingResponse>`;
|
|
440
|
+
});
|
|
441
|
+
// Response: <?xml version="1.0"?><OrderProcessingResponse>...
|
|
442
|
+
// Content-Type: application/xml; charset=utf-8
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Alternative: JSON Response (Simple Testing)**
|
|
446
|
+
|
|
447
|
+
For testing or non-SFCC systems, you can use JSON responses:
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
export const webhook = webhook('order-ingestion', {
|
|
451
|
+
response: { mode: 'sync' }
|
|
452
|
+
})
|
|
453
|
+
.then(http('process', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
454
|
+
// Returns JSON object - Versori auto-encodes
|
|
455
|
+
return {
|
|
456
|
+
success: true,
|
|
457
|
+
orderId: '12345',
|
|
458
|
+
orderRef: 'ORD-001'
|
|
459
|
+
};
|
|
460
|
+
}));
|
|
461
|
+
// Response: {"success":true,"orderId":"12345","orderRef":"ORD-001"}
|
|
462
|
+
// Content-Type: application/json
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**Key Decision Points:**
|
|
466
|
+
|
|
467
|
+
| Pattern | Use When | Response Type |
|
|
468
|
+
|---------|----------|---------------|
|
|
469
|
+
| **XML Response** | Production SFCC integration | XML with proper headers |
|
|
470
|
+
| **JSON Response** | Testing, non-SFCC systems | JSON auto-encoded |
|
|
471
|
+
|
|
472
|
+
**Key Points:**
|
|
473
|
+
- ✅ **XML (Recommended)**: Requires custom `onSuccess`/`onError` handlers with `Response` objects
|
|
474
|
+
- ✅ **JSON (Testing)**: Default behavior, no custom handler needed
|
|
475
|
+
- ✅ Workflow steps return raw strings (XML) or objects (JSON)
|
|
476
|
+
- ✅ Response handlers wrap in `Response` objects with proper Content-Type
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## SDK Methods Used
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
// Core SDK imports
|
|
484
|
+
// FC Connect SDK v0.1.41+
|
|
485
|
+
// Install: npm install @fluentcommerce/fc-connect-sdk@^0.1.41
|
|
486
|
+
// Docs: https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk/
|
|
487
|
+
// GitHub: https://github.com/fluentcommerce/fc-connect-sdk
|
|
488
|
+
|
|
489
|
+
import { createClient, GraphQLMutationMapper, XMLBuilder } from '@fluentcommerce/fc-connect-sdk';
|
|
490
|
+
import type { Logger } from '@fluentcommerce/fc-connect-sdk';
|
|
491
|
+
|
|
492
|
+
// ═══════════════════════════════════════════════════════════════
|
|
493
|
+
// LOGGER ADAPTER PATTERN (Production-Proven)
|
|
494
|
+
// ═══════════════════════════════════════════════════════════════
|
|
495
|
+
// Create typed Logger adapter from Versori log
|
|
496
|
+
// Benefits:
|
|
497
|
+
// - Type safety with SDK Logger interface
|
|
498
|
+
// - Centralized logging (easy to modify all calls)
|
|
499
|
+
// - Clear separation between Versori log and SDK Logger
|
|
500
|
+
const logger: Logger = {
|
|
501
|
+
info: (msg: string, meta?: any) => log.info(msg, meta),
|
|
502
|
+
error: (msg: string, error?: Error, meta?: any) => log.error(msg, meta),
|
|
503
|
+
warn: (msg: string, meta?: any) => log.info(msg, meta),
|
|
504
|
+
debug: (msg: string, meta?: any) => log.info(msg, meta)
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// Key methods
|
|
508
|
+
createClient(ctx); // Auto-detects Versori context
|
|
509
|
+
// ✅ RECOMMENDED: Pass resolvers in constructor
|
|
510
|
+
new GraphQLMutationMapper(config, logger, { customResolvers, fluentClient: client });
|
|
511
|
+
// ✅ Alternative: Pass resolvers to mapWithNodes (constructor resolvers merged)
|
|
512
|
+
mapper.mapWithNodes(data, resolvers, context);
|
|
513
|
+
// ✅ v0.1.41+: mapWithNodes() auto-returns query and variables
|
|
514
|
+
const result = await mapper.mapWithNodes(data, undefined, context); // Use constructor resolvers
|
|
515
|
+
// result.query - Auto-generated GraphQL mutation
|
|
516
|
+
// result.variables - Auto-wrapped variables (input object)
|
|
517
|
+
client.graphql({ query: result.query, variables: result.variables }); // Execute
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Why GraphQLMutationMapper (Not UniversalMapper)?
|
|
521
|
+
|
|
522
|
+
**GraphQLMutationMapper** is the right choice for this template because:
|
|
523
|
+
|
|
524
|
+
✅ **Automatic Mutation Building**: Automatically generates GraphQL mutation query from mapping config
|
|
525
|
+
✅ **Input Wrapping**: Automatically wraps mapped data in `input` object (Fluent API requirement)
|
|
526
|
+
✅ **GraphQL-Specific**: Designed specifically for GraphQL mutations with schema awareness
|
|
527
|
+
|
|
528
|
+
**UniversalMapper** would require:
|
|
529
|
+
- ❌ Manual GraphQL mutation query building
|
|
530
|
+
- ❌ Manual `input` wrapping
|
|
531
|
+
- ❌ More boilerplate code
|
|
532
|
+
|
|
533
|
+
**Use UniversalMapper when**: You need general-purpose data transformation (CSV → JSON, GraphQL → Parquet, etc.)
|
|
534
|
+
**Use GraphQLMutationMapper when**: You need XML/JSON → GraphQL mutations (this template's use case)
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
## Complete Working Code
|
|
539
|
+
|
|
540
|
+
### 1. Entry Point (index.ts)
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
/**
|
|
544
|
+
* Entry point - Export all workflows for Versori platform
|
|
545
|
+
*/
|
|
546
|
+
|
|
547
|
+
// Webhook workflows
|
|
548
|
+
export { sfccOrderCreate } from './workflows/webhook/sfcc-order-create';
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### 2. Workflow Entry Point (workflows/webhook/sfcc-order-create.ts)
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
/**
|
|
555
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
556
|
+
* SFCC → FLUENT ORDER CREATE WEBHOOK (Production Pattern)
|
|
557
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
558
|
+
*
|
|
559
|
+
* PURPOSE:
|
|
560
|
+
* This webhook receives order data from Salesforce Commerce Cloud (SFCC) as XML
|
|
561
|
+
* in the request body. It validates, transforms, and creates orders in Fluent
|
|
562
|
+
* Commerce via GraphQL mutations.
|
|
563
|
+
*
|
|
564
|
+
* WEBHOOK CONFIGURATION:
|
|
565
|
+
* - Response mode: synchronous (caller waits for response)
|
|
566
|
+
* - Response format: XML (OrderProcessingResponse)
|
|
567
|
+
* - CORS: enabled (allows cross-origin requests)
|
|
568
|
+
* - Expected payload: XML in request body (Content-Type: application/xml)
|
|
569
|
+
*
|
|
570
|
+
* ARCHITECTURE:
|
|
571
|
+
* Uses production-proven patterns from real SFCC integrations:
|
|
572
|
+
* - XML response handling with proper Content-Type headers
|
|
573
|
+
* - Logger adapter for type-safe logging
|
|
574
|
+
* - Fire-and-forget background processing
|
|
575
|
+
* - Fail-fast validation of required configuration
|
|
576
|
+
* - Visual step documentation blocks
|
|
577
|
+
*
|
|
578
|
+
* WORKFLOW STEPS:
|
|
579
|
+
* 1. Validate XML structure and required fields
|
|
580
|
+
* 2. Apply mapping with custom resolvers
|
|
581
|
+
* 3. Execute GraphQL mutation to create order
|
|
582
|
+
* 4. Build XML response with order creation results
|
|
583
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
584
|
+
*/
|
|
585
|
+
|
|
586
|
+
import { webhook, http, fn } from '@versori/run';
|
|
587
|
+
import type { Context } from '@versori/run';
|
|
588
|
+
import { createClient, GraphQLMutationMapper } from '@fluentcommerce/fc-connect-sdk';
|
|
589
|
+
import type { Logger } from '@fluentcommerce/fc-connect-sdk';
|
|
590
|
+
import { allResolvers } from '../../resolvers';
|
|
591
|
+
import mappingConfig from '../../config/sfcc-to-fluent-order-mapping.json' with { type: 'json' };
|
|
592
|
+
import { XMLBuilder } from '@fluentcommerce/fc-connect-sdk';
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
596
|
+
* XML RESPONSE ARCHITECTURE (Production Pattern)
|
|
597
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
598
|
+
*
|
|
599
|
+
* This webhook uses XML responses as the PRIMARY pattern for SFCC integration.
|
|
600
|
+
* The onSuccess and onError handlers wrap XML strings in Response objects with
|
|
601
|
+
* proper Content-Type headers.
|
|
602
|
+
*
|
|
603
|
+
* RESPONSE FORMAT:
|
|
604
|
+
* Success:
|
|
605
|
+
* <?xml version="1.0" encoding="UTF-8"?>
|
|
606
|
+
* <OrderProcessingResponse>
|
|
607
|
+
* <Status>success</Status>
|
|
608
|
+
* <Message>Order successfully processed and created in Fluent Commerce</Message>
|
|
609
|
+
* <FluentOrderId>6370</FluentOrderId>
|
|
610
|
+
* <FluentOrderRef>0017326966182_G_postman_test_1760703173226</FluentOrderRef>
|
|
611
|
+
* <SFCCOrderRef>0017326966182_G_postman_test_1760703173226</SFCCOrderRef>
|
|
612
|
+
* <Timestamp>2025-11-13T12:12:54.229Z</Timestamp>
|
|
613
|
+
* </OrderProcessingResponse>
|
|
614
|
+
*
|
|
615
|
+
* Error:
|
|
616
|
+
* <?xml version="1.0" encoding="UTF-8"?>
|
|
617
|
+
* <OrderProcessingResponse>
|
|
618
|
+
* <Status>error</Status>
|
|
619
|
+
* <Message>Failed to process order</Message>
|
|
620
|
+
* <Error>Error details here</Error>
|
|
621
|
+
* <Timestamp>2025-11-13T12:12:54.229Z</Timestamp>
|
|
622
|
+
* </OrderProcessingResponse>
|
|
623
|
+
*
|
|
624
|
+
* HOW IT WORKS:
|
|
625
|
+
* 1. Workflow steps return XML strings via ctx.data
|
|
626
|
+
* 2. onSuccess handler receives ctx with the XML string in ctx.data
|
|
627
|
+
* 3. Handler creates Response with Content-Type: application/xml
|
|
628
|
+
* 4. Framework streams the Response directly (no JSON encoding)
|
|
629
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
630
|
+
*/
|
|
631
|
+
export const sfccOrderCreate = webhook('sfcc-order-create', {
|
|
632
|
+
response: {
|
|
633
|
+
mode: 'sync',
|
|
634
|
+
/**
|
|
635
|
+
* onSuccess Handler for XML Response
|
|
636
|
+
*
|
|
637
|
+
* This handler wraps the XML response string with proper headers.
|
|
638
|
+
* Similar to production SFCC workflows, this ensures the XML is
|
|
639
|
+
* streamed directly without JSON encoding.
|
|
640
|
+
*
|
|
641
|
+
* @param ctx - Context containing:
|
|
642
|
+
* - ctx.data: The XML response string from the final workflow step
|
|
643
|
+
* - ctx.executionId: Unique identifier for this execution
|
|
644
|
+
* @returns Response object with XML body and proper headers
|
|
645
|
+
*/
|
|
646
|
+
onSuccess: (ctx) => new Response(ctx.data, {
|
|
647
|
+
status: 200,
|
|
648
|
+
headers: {
|
|
649
|
+
'Content-Type': 'application/xml; charset=utf-8',
|
|
650
|
+
'X-Execution-Id': ctx.executionId
|
|
651
|
+
}
|
|
652
|
+
}),
|
|
653
|
+
/**
|
|
654
|
+
* onError Handler for XML Error Response
|
|
655
|
+
*
|
|
656
|
+
* This handler wraps error responses with proper status and headers.
|
|
657
|
+
* Returns XML format for consistent error responses.
|
|
658
|
+
*
|
|
659
|
+
* @param ctx - Context containing:
|
|
660
|
+
* - ctx.data: The error XML string from the .catch() step
|
|
661
|
+
* - ctx.executionId: Unique identifier for tracking failed executions
|
|
662
|
+
* @returns Response object with error XML and 500 status
|
|
663
|
+
*/
|
|
664
|
+
onError: (ctx) => new Response(ctx.data, {
|
|
665
|
+
status: 500,
|
|
666
|
+
headers: {
|
|
667
|
+
'Content-Type': 'application/xml; charset=utf-8',
|
|
668
|
+
'X-Execution-Id': ctx.executionId
|
|
669
|
+
}
|
|
670
|
+
})
|
|
671
|
+
},
|
|
672
|
+
cors: true
|
|
673
|
+
}).then(
|
|
674
|
+
/**
|
|
675
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
676
|
+
* STEP 1: VALIDATE XML STRUCTURE
|
|
677
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
678
|
+
*
|
|
679
|
+
* This step validates that the incoming data is properly parsed XML from Versori.
|
|
680
|
+
* Uses fail-fast validation pattern to catch configuration issues early.
|
|
681
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
682
|
+
*/
|
|
683
|
+
fn('validate-xml-structure', async ({ data, log, activation }) => {
|
|
684
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
685
|
+
// LOGGER ADAPTER PATTERN (Production-Proven)
|
|
686
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
687
|
+
// Create Logger Adapter for type-safe SDK logging
|
|
688
|
+
// Benefits:
|
|
689
|
+
// - Explicit interface contract (TypeScript Logger type)
|
|
690
|
+
// - Centralized logging adapter (easy to modify all log calls)
|
|
691
|
+
// - Type safety enforced
|
|
692
|
+
// - Clear separation between Versori log and SDK Logger
|
|
693
|
+
const logger: Logger = {
|
|
694
|
+
info: (msg: string, meta?: any) => log.info(msg, meta),
|
|
695
|
+
error: (msg: string, error?: Error, meta?: any) => log.error(msg, meta),
|
|
696
|
+
warn: (msg: string, meta?: any) => log.info(msg, meta),
|
|
697
|
+
debug: (msg: string, meta?: any) => log.info(msg, meta)
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
logger.info('Starting SFCC order webhook processing');
|
|
701
|
+
|
|
702
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
703
|
+
// INITIAL DEPLOYMENT MONITORING PATTERN
|
|
704
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
705
|
+
// TEMPORARY: Complete XML log for short-term monitoring (first 2-4 weeks)
|
|
706
|
+
// TODO: Remove this log once integration is stable and monitoring is complete
|
|
707
|
+
// Purpose: Helps catch XML structure issues and field mapping problems early
|
|
708
|
+
// Impact: Increases log volume - remove after stabilization
|
|
709
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
710
|
+
logger.info('Complete incoming XML data', { completeData: data });
|
|
711
|
+
|
|
712
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
713
|
+
// Validate Pre-Parsed XML Object from Versori
|
|
714
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
715
|
+
if (typeof data !== 'object' || data === null || !('orders' in data)) {
|
|
716
|
+
throw new Error(
|
|
717
|
+
`Invalid data format. Expected XML (as pre-parsed object with 'orders' root). ` +
|
|
718
|
+
`Received: ${typeof data}. ` +
|
|
719
|
+
`Please ensure you are sending XML with Content-Type: application/xml.`
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
logger.info('XML structure validated successfully');
|
|
724
|
+
|
|
725
|
+
return {
|
|
726
|
+
parsedXml: data,
|
|
727
|
+
logger
|
|
728
|
+
};
|
|
729
|
+
})
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
734
|
+
* STEP 2: MAP WITH RESOLVERS USING SDK NODES PATTERN
|
|
735
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
736
|
+
*
|
|
737
|
+
* This step applies field mappings and executes custom resolvers using the SDK's
|
|
738
|
+
* mapWithNodes() method. This method is REQUIRED when using custom resolvers.
|
|
739
|
+
*
|
|
740
|
+
* v0.1.41+ Improvement: mapWithNodes() auto-returns query and variables
|
|
741
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
742
|
+
*/
|
|
743
|
+
.then(
|
|
744
|
+
http('map-with-resolvers', {
|
|
745
|
+
connection: 'fluent_commerce'
|
|
746
|
+
}, async (ctx) => {
|
|
747
|
+
const { data, log, activation } = ctx;
|
|
748
|
+
const { parsedXml, logger } = data;
|
|
749
|
+
|
|
750
|
+
logger.info('Mapping SFCC order to Fluent Commerce with custom resolvers');
|
|
751
|
+
|
|
752
|
+
try {
|
|
753
|
+
const fluentClient = await createClient(ctx);
|
|
754
|
+
|
|
755
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
756
|
+
// INTEGRATION VARIABLES VALIDATION (Fail-Fast Pattern)
|
|
757
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
758
|
+
// Production pattern: Validate required configuration early
|
|
759
|
+
// Benefits:
|
|
760
|
+
// - Fails immediately with explicit error message
|
|
761
|
+
// - Tells user EXACTLY what's wrong and where to fix it
|
|
762
|
+
// - Better than silent fallback to default (hides config issues)
|
|
763
|
+
// - Provides audit trail of successful retrieval
|
|
764
|
+
const fluentRetailerId = activation.getVariable('fluentRetailerId') as string;
|
|
765
|
+
|
|
766
|
+
if (!fluentRetailerId) {
|
|
767
|
+
throw new Error(
|
|
768
|
+
'fluentRetailerId integration variable is required but not set. ' +
|
|
769
|
+
'Please set this variable in Versori Activations > Variables section.'
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
logger.info('Using retailer ID from integration variables', { retailerId: fluentRetailerId });
|
|
774
|
+
|
|
775
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
776
|
+
// INITIALIZE MAPPER WITH RESOLVERS IN CONSTRUCTOR (Recommended Pattern)
|
|
777
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
778
|
+
// ✅ CORRECT: Pass resolvers in constructor options
|
|
779
|
+
// This is consistent with UniversalMapper and allows resolvers to be reused
|
|
780
|
+
const mapper = new GraphQLMutationMapper(
|
|
781
|
+
mappingConfig as any,
|
|
782
|
+
logger,
|
|
783
|
+
{
|
|
784
|
+
customResolvers: allResolvers, // ✅ Resolvers in constructor
|
|
785
|
+
fluentClient: fluentClient as any,
|
|
786
|
+
}
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
790
|
+
// CREATE RESOLVER CONTEXT
|
|
791
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
792
|
+
// Pass configuration to custom resolvers
|
|
793
|
+
// SDK automatically provides helpers (get, ensureArray, logger, etc.)
|
|
794
|
+
// You only need to pass custom config and fluentClient
|
|
795
|
+
const resolverContext = {
|
|
796
|
+
fluentClient: fluentClient as any,
|
|
797
|
+
config: {
|
|
798
|
+
retailerId: fluentRetailerId,
|
|
799
|
+
defaultCountry: 'US',
|
|
800
|
+
companyName: 'YourCompany' // Customize per deployment
|
|
801
|
+
},
|
|
802
|
+
// ✅ SDK automatically provides helpers.get, helpers.ensureArray, helpers.logger
|
|
803
|
+
// No need to manually create them - SDK handles XML attributes via path resolver
|
|
804
|
+
// ✅ Resolvers can access config via helpers.context.config.retailerId
|
|
805
|
+
// OR via the config parameter (3rd param): config.retailerId
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
// Execute mapping WITH custom resolvers
|
|
809
|
+
// ✅ v0.1.41+: mapWithNodes() auto-generates query and variables
|
|
810
|
+
// ✅ Resolvers from constructor are automatically used (can override with 2nd param)
|
|
811
|
+
const mappingResult = await mapper.mapWithNodes(
|
|
812
|
+
parsedXml,
|
|
813
|
+
undefined, // Use constructor resolvers (or pass override resolvers here)
|
|
814
|
+
resolverContext
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
if (!mappingResult.success) {
|
|
818
|
+
throw new Error(`Mapping failed: ${mappingResult.errors?.join(', ')}`);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const orderRef = mappingResult.data?.input?.ref || 'unknown';
|
|
822
|
+
|
|
823
|
+
logger.info('Successfully mapped order data with custom resolvers', { orderRef });
|
|
824
|
+
|
|
825
|
+
return {
|
|
826
|
+
mappedData: mappingResult.data,
|
|
827
|
+
mappingResult, // ✅ Contains auto-generated query and variables
|
|
828
|
+
orderRef,
|
|
829
|
+
fluentClient,
|
|
830
|
+
logger
|
|
831
|
+
};
|
|
832
|
+
} catch (error) {
|
|
833
|
+
log.error('Error during mapping with resolvers', {
|
|
834
|
+
error: error instanceof Error ? error.message : String(error),
|
|
835
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
836
|
+
});
|
|
837
|
+
throw error;
|
|
838
|
+
}
|
|
839
|
+
})
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
844
|
+
* STEP 3: EXECUTE GRAPHQL MUTATION
|
|
845
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
846
|
+
*
|
|
847
|
+
* Execute the GraphQL mutation using auto-generated query from mapWithNodes().
|
|
848
|
+
* v0.1.41+ automatically wraps variables and generates query.
|
|
849
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
850
|
+
*/
|
|
851
|
+
.then(
|
|
852
|
+
http('execute-graphql-mutation', {
|
|
853
|
+
connection: 'fluent_commerce'
|
|
854
|
+
}, async (ctx) => {
|
|
855
|
+
const { data, log } = ctx;
|
|
856
|
+
const { mappingResult, orderRef, fluentClient, logger } = data;
|
|
857
|
+
|
|
858
|
+
logger.info('Executing createOrder GraphQL mutation', { orderRef });
|
|
859
|
+
|
|
860
|
+
try {
|
|
861
|
+
// ✅ v0.1.41+: Use auto-generated query and variables from mapWithNodes()
|
|
862
|
+
// No need to call buildMutation() - query is already generated
|
|
863
|
+
const mutationResult = await (fluentClient as any).graphql({
|
|
864
|
+
query: mappingResult.query, // ✅ Auto-generated mutation query
|
|
865
|
+
variables: mappingResult.variables // ✅ Auto-wrapped variables
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
if (mutationResult.errors && mutationResult.errors.length > 0) {
|
|
869
|
+
logger.error('GraphQL mutation returned errors', {
|
|
870
|
+
errors: mutationResult.errors,
|
|
871
|
+
orderRef
|
|
872
|
+
});
|
|
873
|
+
throw new Error(`GraphQL errors: ${JSON.stringify(mutationResult.errors)}`);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const createdOrder = (mutationResult.data as any)?.createOrder;
|
|
877
|
+
|
|
878
|
+
if (!createdOrder) {
|
|
879
|
+
throw new Error('No order data returned from createOrder mutation');
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Access fields specified in returnFields (id, ref, status, totalPrice)
|
|
883
|
+
logger.info('Successfully created order in Fluent Commerce', {
|
|
884
|
+
fluentOrderId: createdOrder.id, // From returnFields
|
|
885
|
+
fluentOrderRef: createdOrder.ref, // From returnFields
|
|
886
|
+
status: createdOrder.status, // From returnFields
|
|
887
|
+
totalPrice: createdOrder.totalPrice, // From returnFields
|
|
888
|
+
sfccOrderRef: orderRef
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
return {
|
|
892
|
+
success: true,
|
|
893
|
+
fluentOrderId: createdOrder.id,
|
|
894
|
+
fluentOrderRef: createdOrder.ref,
|
|
895
|
+
sfccOrderRef: orderRef,
|
|
896
|
+
orderData: createdOrder
|
|
897
|
+
};
|
|
898
|
+
} catch (error) {
|
|
899
|
+
log.error('Error executing GraphQL mutation', {
|
|
900
|
+
error: error instanceof Error ? error.message : String(error),
|
|
901
|
+
orderRef
|
|
902
|
+
});
|
|
903
|
+
throw error;
|
|
904
|
+
}
|
|
905
|
+
})
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
910
|
+
* STEP 4: BUILD XML RESPONSE
|
|
911
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
912
|
+
*
|
|
913
|
+
* IMPORTANT: Return the XML string here.
|
|
914
|
+
* The onSuccess handler will wrap it in a Response with:
|
|
915
|
+
* - Content-Type: application/xml
|
|
916
|
+
* - Status: 200
|
|
917
|
+
* - X-Execution-Id header
|
|
918
|
+
*
|
|
919
|
+
* This keeps the workflow logic focused on data transformation while
|
|
920
|
+
* the framework handles HTTP response formatting.
|
|
921
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
922
|
+
*/
|
|
923
|
+
.then(
|
|
924
|
+
fn('build-xml-response', ({ data, log }) => {
|
|
925
|
+
log.info('Building XML response for SFCC order processing', {
|
|
926
|
+
fluentOrderId: data.fluentOrderId,
|
|
927
|
+
fluentOrderRef: data.fluentOrderRef,
|
|
928
|
+
sfccOrderRef: data.sfccOrderRef
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
// Initialize XML Builder (SDK-provided)
|
|
932
|
+
const builder = new XMLBuilder({
|
|
933
|
+
xmlDeclaration: true, // Adds <?xml version="1.0" encoding="UTF-8"?>
|
|
934
|
+
prettyPrint: true, // Format with indentation (default: true)
|
|
935
|
+
indent: ' ', // Two-space indentation (default)
|
|
936
|
+
encoding: 'UTF-8' // XML encoding (default)
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
// Build XML response data
|
|
940
|
+
const responseData = {
|
|
941
|
+
Status: 'success',
|
|
942
|
+
Message: 'Order successfully processed and created in Fluent Commerce',
|
|
943
|
+
FluentOrderId: data.fluentOrderId,
|
|
944
|
+
FluentOrderRef: data.fluentOrderRef,
|
|
945
|
+
SFCCOrderRef: data.sfccOrderRef,
|
|
946
|
+
Timestamp: new Date().toISOString()
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
// Build XML with root element
|
|
950
|
+
const xmlString = builder.build(responseData, 'OrderProcessingResponse');
|
|
951
|
+
|
|
952
|
+
log.info('Successfully built XML response', {
|
|
953
|
+
orderRef: data.sfccOrderRef,
|
|
954
|
+
xmlLength: xmlString.length
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Return just the XML string here.
|
|
959
|
+
* The onSuccess handler will wrap it in a Response object with:
|
|
960
|
+
* - Content-Type: application/xml
|
|
961
|
+
* - Status: 200
|
|
962
|
+
* - X-Execution-Id header
|
|
963
|
+
*/
|
|
964
|
+
return xmlString;
|
|
965
|
+
})
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
970
|
+
* ERROR HANDLING
|
|
971
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
972
|
+
*
|
|
973
|
+
* Return error XML string that will be wrapped by onError handler with status 500.
|
|
974
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
975
|
+
*/
|
|
976
|
+
.catch(({ data, log }) => {
|
|
977
|
+
log.error('Order processing failed', {
|
|
978
|
+
error: data instanceof Error ? data.message : String(data)
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Build error XML that will be wrapped by onError handler.
|
|
983
|
+
* The onError handler will receive this string in ctx.data and wrap it
|
|
984
|
+
* in a Response object with proper status and headers.
|
|
985
|
+
*/
|
|
986
|
+
const errorXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
987
|
+
<OrderProcessingResponse>
|
|
988
|
+
<Status>error</Status>
|
|
989
|
+
<Message>Failed to process order</Message>
|
|
990
|
+
<Error>${data instanceof Error ? data.message : String(data)}</Error>
|
|
991
|
+
<Timestamp>${new Date().toISOString()}</Timestamp>
|
|
992
|
+
</OrderProcessingResponse>`;
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Return the error XML string.
|
|
996
|
+
* The onError handler will wrap it in a Response with status 500.
|
|
997
|
+
*/
|
|
998
|
+
return errorXml;
|
|
999
|
+
});
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
### 3. Service Implementation (services/order-ingestion.service.ts)
|
|
1003
|
+
|
|
1004
|
+
```typescript
|
|
1005
|
+
/**
|
|
1006
|
+
* SFCC Order Ingestion Service
|
|
1007
|
+
*
|
|
1008
|
+
* Orchestrates the complete order ingestion process:
|
|
1009
|
+
* 1. Validate webhook payload
|
|
1010
|
+
* 2. Validate SFCC structure
|
|
1011
|
+
* 3. Apply field mapping with custom resolvers
|
|
1012
|
+
* 4. Create order via GraphQL API
|
|
1013
|
+
* 5. Audit trail (save input/output files)
|
|
1014
|
+
*/
|
|
1015
|
+
|
|
1016
|
+
import { createClient, GraphQLMutationMapper } from '@fluentcommerce/fc-connect-sdk';
|
|
1017
|
+
import { allResolvers } from '../resolvers';
|
|
1018
|
+
import mappingConfig from '../config/sfcc-to-fluent-order-mapping.json' with { type: 'json' };
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Process SFCC order ingestion
|
|
1022
|
+
*
|
|
1023
|
+
* @param ctx - Versori context object containing fetch, connections, log, activation, data
|
|
1024
|
+
* @param executionStartTime - Workflow start time for duration tracking
|
|
1025
|
+
*/
|
|
1026
|
+
export async function processOrderIngestion(ctx: any, executionStartTime: number) {
|
|
1027
|
+
const { log, data, activation } = ctx;
|
|
1028
|
+
|
|
1029
|
+
log.info('Starting SFCC order processing');
|
|
1030
|
+
|
|
1031
|
+
try {
|
|
1032
|
+
const sfccData = data;
|
|
1033
|
+
|
|
1034
|
+
if (!sfccData) {
|
|
1035
|
+
log.error('No order data received');
|
|
1036
|
+
return {
|
|
1037
|
+
success: false,
|
|
1038
|
+
error: 'No order data received',
|
|
1039
|
+
timestamp: new Date().toISOString(),
|
|
1040
|
+
duration: Date.now() - executionStartTime,
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
if (!sfccData.orders?.order) {
|
|
1045
|
+
log.error('Invalid order structure: missing orders.order');
|
|
1046
|
+
return {
|
|
1047
|
+
success: false,
|
|
1048
|
+
error: 'Invalid order structure: missing orders.order',
|
|
1049
|
+
timestamp: new Date().toISOString(),
|
|
1050
|
+
duration: Date.now() - executionStartTime,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const order = sfccData.orders.order;
|
|
1055
|
+
if (!order['original-order-no']) {
|
|
1056
|
+
log.error('Missing required field: original-order-no');
|
|
1057
|
+
return {
|
|
1058
|
+
success: false,
|
|
1059
|
+
error: 'Missing required field: original-order-no',
|
|
1060
|
+
timestamp: new Date().toISOString(),
|
|
1061
|
+
duration: Date.now() - executionStartTime,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (!order.customer?.['customer-no']) {
|
|
1066
|
+
log.error('Missing required field: customer.customer-no');
|
|
1067
|
+
return {
|
|
1068
|
+
success: false,
|
|
1069
|
+
error: 'Missing required field: customer.customer-no',
|
|
1070
|
+
timestamp: new Date().toISOString(),
|
|
1071
|
+
duration: Date.now() - executionStartTime,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const fluentClient = await createClient(ctx);
|
|
1076
|
+
log.info('Fluent client initialized');
|
|
1077
|
+
|
|
1078
|
+
log.info('Applying mapping');
|
|
1079
|
+
|
|
1080
|
+
const retailerId = activation?.getVariable('fluentRetailerId') as string;
|
|
1081
|
+
|
|
1082
|
+
if (!retailerId) {
|
|
1083
|
+
log.error('Missing required variable: fluentRetailerId');
|
|
1084
|
+
return {
|
|
1085
|
+
success: false,
|
|
1086
|
+
error: 'Missing required configuration: fluentRetailerId',
|
|
1087
|
+
timestamp: new Date().toISOString(),
|
|
1088
|
+
duration: Date.now() - executionStartTime,
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const productCatalogueRef = activation?.getVariable('productCatalogueRef') as string || 'PC:MASTER:2';
|
|
1093
|
+
|
|
1094
|
+
const mapper = new GraphQLMutationMapper(
|
|
1095
|
+
mappingConfig as any,
|
|
1096
|
+
log,
|
|
1097
|
+
{
|
|
1098
|
+
customResolvers: allResolvers,
|
|
1099
|
+
fluentClient: fluentClient as any,
|
|
1100
|
+
}
|
|
1101
|
+
);
|
|
1102
|
+
|
|
1103
|
+
const resolverContext = {
|
|
1104
|
+
fluentClient: fluentClient as any,
|
|
1105
|
+
config: {
|
|
1106
|
+
retailerId: retailerId,
|
|
1107
|
+
defaultOrderType: 'HD',
|
|
1108
|
+
defaultCountry: 'US',
|
|
1109
|
+
productCatalogueRef: productCatalogueRef,
|
|
1110
|
+
},
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
const result = await mapper.mapWithNodes(sfccData, undefined, resolverContext);
|
|
1114
|
+
|
|
1115
|
+
if (!result.success) {
|
|
1116
|
+
log.error('Mapping failed', { errors: result.errors });
|
|
1117
|
+
return {
|
|
1118
|
+
success: false,
|
|
1119
|
+
error: `Mapping failed: ${result.errors?.join(', ')}`,
|
|
1120
|
+
timestamp: new Date().toISOString(),
|
|
1121
|
+
duration: Date.now() - executionStartTime,
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const orderRef = sfccData.orders?.order?.['original-order-no'] || 'unknown';
|
|
1126
|
+
|
|
1127
|
+
log.info('Mapping completed', { orderRef });
|
|
1128
|
+
|
|
1129
|
+
// Save audit trail to KV storage (Versori-compatible - file system is read-only)
|
|
1130
|
+
try {
|
|
1131
|
+
const kv = openKv(':project:');
|
|
1132
|
+
const timestamp = new Date().toISOString();
|
|
1133
|
+
const auditKey = ['orders', 'audit', orderRef, timestamp];
|
|
1134
|
+
|
|
1135
|
+
await kv.set([...auditKey, 'sfcc-input'], sfccData);
|
|
1136
|
+
await kv.set([...auditKey, 'fluent-mapped'], result.data);
|
|
1137
|
+
|
|
1138
|
+
log.info('Audit trail saved to KV storage', { orderRef, timestamp });
|
|
1139
|
+
} catch (kvError: any) {
|
|
1140
|
+
log.warn('Failed to save audit trail to KV', { error: kvError.message });
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
log.info('Creating order in Fluent Commerce', { orderRef });
|
|
1144
|
+
|
|
1145
|
+
const mutationRes = await (fluentClient as any).graphql({
|
|
1146
|
+
query: result.query,
|
|
1147
|
+
variables: result.variables
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
if (mutationRes.errors && mutationRes.errors.length > 0) {
|
|
1151
|
+
log.error('GraphQL mutation failed', { errors: mutationRes.errors });
|
|
1152
|
+
|
|
1153
|
+
// Save error to KV storage
|
|
1154
|
+
try {
|
|
1155
|
+
const kv = openKv(':project:');
|
|
1156
|
+
const timestamp = new Date().toISOString();
|
|
1157
|
+
const auditKey = ['orders', 'audit', orderRef, timestamp, 'fluent-error'];
|
|
1158
|
+
await kv.set(auditKey, mutationRes);
|
|
1159
|
+
} catch (kvError) {
|
|
1160
|
+
// Ignore KV save errors
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
return {
|
|
1164
|
+
success: false,
|
|
1165
|
+
error: `GraphQL mutation failed: ${mutationRes.errors.map((e: any) => e.message).join(', ')}`,
|
|
1166
|
+
timestamp: new Date().toISOString(),
|
|
1167
|
+
duration: Date.now() - executionStartTime,
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const createOrderData = (mutationRes as any)?.data?.createOrder;
|
|
1172
|
+
|
|
1173
|
+
if (!createOrderData) {
|
|
1174
|
+
log.error('No order data returned from API');
|
|
1175
|
+
return {
|
|
1176
|
+
success: false,
|
|
1177
|
+
error: 'No order data returned from GraphQL mutation',
|
|
1178
|
+
timestamp: new Date().toISOString(),
|
|
1179
|
+
duration: Date.now() - executionStartTime,
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Save successful response to KV storage
|
|
1184
|
+
try {
|
|
1185
|
+
const kv = openKv(':project:');
|
|
1186
|
+
const timestamp = new Date().toISOString();
|
|
1187
|
+
const auditKey = ['orders', 'audit', orderRef, timestamp, 'fluent-response'];
|
|
1188
|
+
await kv.set(auditKey, mutationRes);
|
|
1189
|
+
} catch (kvError) {
|
|
1190
|
+
// Ignore KV save errors
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
log.info('Order created in Fluent Commerce', {
|
|
1194
|
+
orderId: createOrderData?.id,
|
|
1195
|
+
orderRef: createOrderData?.ref,
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
return {
|
|
1199
|
+
success: true,
|
|
1200
|
+
data: {
|
|
1201
|
+
sfccOrderRef: orderRef,
|
|
1202
|
+
fluentOrderId: createOrderData?.id,
|
|
1203
|
+
fluentOrderRef: createOrderData?.ref,
|
|
1204
|
+
timestamp: new Date().toISOString(),
|
|
1205
|
+
},
|
|
1206
|
+
duration: Date.now() - executionStartTime,
|
|
1207
|
+
};
|
|
1208
|
+
} catch (error: any) {
|
|
1209
|
+
log.error('Fatal error', {
|
|
1210
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1211
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
return {
|
|
1215
|
+
success: false,
|
|
1216
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1217
|
+
timestamp: new Date().toISOString(),
|
|
1218
|
+
duration: Date.now() - executionStartTime,
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
```
|
|
1223
|
+
|
|
1224
|
+
### 4. Mapping Configuration (config/sfcc-to-fluent-order-mapping.json)
|
|
1225
|
+
|
|
1226
|
+
```json
|
|
1227
|
+
{
|
|
1228
|
+
"direction": "ingest",
|
|
1229
|
+
"sourceFormat": "xml",
|
|
1230
|
+
"mutation": "createOrder",
|
|
1231
|
+
"returnFields": ["id", "ref", "status", "totalPrice"],
|
|
1232
|
+
"fields": {
|
|
1233
|
+
"ref": {
|
|
1234
|
+
"source": "orders.order.original-order-no",
|
|
1235
|
+
"resolver": "sdk.toString",
|
|
1236
|
+
"comment": "Order reference from SFCC native field (REQUIRED)"
|
|
1237
|
+
},
|
|
1238
|
+
"type": {
|
|
1239
|
+
"resolver": "custom.deriveOrderType",
|
|
1240
|
+
"comment": "Derive from shipment count: 1 shipment = HD, multiple = MULTI (REQUIRED)"
|
|
1241
|
+
},
|
|
1242
|
+
"retailer.id": {
|
|
1243
|
+
"value": "${RETAILER_ID}",
|
|
1244
|
+
"comment": "From environment variable or configuration (REQUIRED)"
|
|
1245
|
+
},
|
|
1246
|
+
"totalPrice": {
|
|
1247
|
+
"source": "orders.order.totals.order-total.net-price",
|
|
1248
|
+
"resolver": "sdk.parseFloat",
|
|
1249
|
+
"comment": "Order total net price from SFCC totals (REQUIRED)"
|
|
1250
|
+
},
|
|
1251
|
+
"totalTaxPrice": {
|
|
1252
|
+
"source": "orders.order.totals.order-total.tax",
|
|
1253
|
+
"resolver": "sdk.parseFloat",
|
|
1254
|
+
"comment": "Order total tax from SFCC totals (REQUIRED)"
|
|
1255
|
+
},
|
|
1256
|
+
"attributes": {
|
|
1257
|
+
"resolver": "custom.buildOrderAttributes",
|
|
1258
|
+
"comment": "Order-level attributes: order-date, created-by, invoice-no, totals JSON"
|
|
1259
|
+
},
|
|
1260
|
+
"customer": {
|
|
1261
|
+
"fields": {
|
|
1262
|
+
"ref": {
|
|
1263
|
+
"source": "orders.order.customer.customer-no",
|
|
1264
|
+
"resolver": "sdk.toString",
|
|
1265
|
+
"comment": "Customer reference from SFCC (REQUIRED)"
|
|
1266
|
+
},
|
|
1267
|
+
"firstName": {
|
|
1268
|
+
"source": "orders.order.customer.customer-name",
|
|
1269
|
+
"resolver": "custom.extractFirstName",
|
|
1270
|
+
"comment": "Extract first name from customer-name"
|
|
1271
|
+
},
|
|
1272
|
+
"lastName": {
|
|
1273
|
+
"source": "orders.order.customer.customer-name",
|
|
1274
|
+
"resolver": "custom.extractLastName",
|
|
1275
|
+
"comment": "Extract last name from customer-name"
|
|
1276
|
+
},
|
|
1277
|
+
"primaryEmail": {
|
|
1278
|
+
"source": "orders.order.customer.customer-email",
|
|
1279
|
+
"resolver": "sdk.toString",
|
|
1280
|
+
"comment": "Customer email from SFCC (REQUIRED)"
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
},
|
|
1284
|
+
"billingAddress": {
|
|
1285
|
+
"fields": {
|
|
1286
|
+
"name": {
|
|
1287
|
+
"source": "orders.order.customer.billing-address.first-name",
|
|
1288
|
+
"resolver": "custom.combineBillingName",
|
|
1289
|
+
"comment": "Combine first-name + last-name"
|
|
1290
|
+
},
|
|
1291
|
+
"street": {
|
|
1292
|
+
"source": "orders.order.customer.billing-address.address1",
|
|
1293
|
+
"resolver": "sdk.toString",
|
|
1294
|
+
"comment": "Billing address line 1 (REQUIRED)"
|
|
1295
|
+
},
|
|
1296
|
+
"street2": {
|
|
1297
|
+
"source": "orders.order.customer.billing-address.address2",
|
|
1298
|
+
"resolver": "sdk.toString",
|
|
1299
|
+
"comment": "Billing address line 2 (optional)"
|
|
1300
|
+
},
|
|
1301
|
+
"city": {
|
|
1302
|
+
"source": "orders.order.customer.billing-address.city",
|
|
1303
|
+
"resolver": "sdk.toString",
|
|
1304
|
+
"comment": "Billing city (REQUIRED)"
|
|
1305
|
+
},
|
|
1306
|
+
"state": {
|
|
1307
|
+
"source": "orders.order.customer.billing-address.state-code",
|
|
1308
|
+
"resolver": "sdk.toString",
|
|
1309
|
+
"comment": "Billing state code (REQUIRED)"
|
|
1310
|
+
},
|
|
1311
|
+
"postcode": {
|
|
1312
|
+
"source": "orders.order.customer.billing-address.postal-code",
|
|
1313
|
+
"resolver": "sdk.toString",
|
|
1314
|
+
"comment": "Billing postal code (REQUIRED)"
|
|
1315
|
+
},
|
|
1316
|
+
"country": {
|
|
1317
|
+
"source": "orders.order.customer.billing-address.country-code",
|
|
1318
|
+
"resolver": "custom.normalizeCountryCode",
|
|
1319
|
+
"comment": "Normalize country code (us → US)"
|
|
1320
|
+
}
|
|
1321
|
+
},
|
|
1322
|
+
"attributes": {
|
|
1323
|
+
"resolver": "custom.buildBillingAddressAttributes",
|
|
1324
|
+
"comment": "Billing address phone and custom attributes"
|
|
1325
|
+
}
|
|
1326
|
+
},
|
|
1327
|
+
"items": {
|
|
1328
|
+
"source": "orders.order.product-lineitems.product-lineitem",
|
|
1329
|
+
"isArray": true,
|
|
1330
|
+
"fields": {
|
|
1331
|
+
"ref": {
|
|
1332
|
+
"source": "$.position",
|
|
1333
|
+
"resolver": "custom.createItemRef",
|
|
1334
|
+
"comment": "Item reference from position (REQUIRED)"
|
|
1335
|
+
},
|
|
1336
|
+
"productRef": {
|
|
1337
|
+
"source": "$.product-id",
|
|
1338
|
+
"resolver": "sdk.toString",
|
|
1339
|
+
"comment": "Product reference from SFCC product-id (REQUIRED)"
|
|
1340
|
+
},
|
|
1341
|
+
"productCatalogueRef": {
|
|
1342
|
+
"resolver": "custom.getProductCatalogueRef",
|
|
1343
|
+
"comment": "Default product catalogue"
|
|
1344
|
+
},
|
|
1345
|
+
"quantity": {
|
|
1346
|
+
"source": "$.quantity",
|
|
1347
|
+
"resolver": "sdk.parseFloat",
|
|
1348
|
+
"comment": "Quantity from SFCC (REQUIRED)"
|
|
1349
|
+
},
|
|
1350
|
+
"price": {
|
|
1351
|
+
"source": "$.base-price",
|
|
1352
|
+
"resolver": "sdk.parseFloat",
|
|
1353
|
+
"comment": "Base price per item from SFCC (REQUIRED)"
|
|
1354
|
+
},
|
|
1355
|
+
"totalPrice": {
|
|
1356
|
+
"source": "$.net-price",
|
|
1357
|
+
"resolver": "sdk.parseFloat",
|
|
1358
|
+
"comment": "Total price (net-price) from SFCC (REQUIRED)"
|
|
1359
|
+
},
|
|
1360
|
+
"taxPrice": {
|
|
1361
|
+
"source": "$.tax",
|
|
1362
|
+
"resolver": "sdk.parseFloat",
|
|
1363
|
+
"comment": "Tax amount from SFCC (REQUIRED)"
|
|
1364
|
+
},
|
|
1365
|
+
"currency": {
|
|
1366
|
+
"source": "orders.order.currency",
|
|
1367
|
+
"resolver": "sdk.toString",
|
|
1368
|
+
"comment": "Currency from order level (REQUIRED)"
|
|
1369
|
+
},
|
|
1370
|
+
"attributes": {
|
|
1371
|
+
"resolver": "custom.buildItemAttributes",
|
|
1372
|
+
"comment": "Item attributes: position, tax-basis, tax-rate, shipment-id, gift, custom-attributes"
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
},
|
|
1376
|
+
"fulfilmentChoices": {
|
|
1377
|
+
"source": "orders.order.shipments.shipment",
|
|
1378
|
+
"isArray": true,
|
|
1379
|
+
"fields": {
|
|
1380
|
+
"type": {
|
|
1381
|
+
"source": "$.shipping-method",
|
|
1382
|
+
"resolver": "custom.mapShippingMethodToFulfilmentType",
|
|
1383
|
+
"comment": "Map ISPU → BOPIS, STANDARD_SHIPPING → SHIP_TO_HOME (REQUIRED)"
|
|
1384
|
+
},
|
|
1385
|
+
"pickupLocationRef": {
|
|
1386
|
+
"source": "$.custom-attributes.custom-attribute[attribute-id=fromStoreId]",
|
|
1387
|
+
"resolver": "sdk.toString",
|
|
1388
|
+
"comment": "Store ID for BOPIS orders"
|
|
1389
|
+
},
|
|
1390
|
+
"deliveryType": {
|
|
1391
|
+
"source": "orders.order.shipping-lineitems.shipping-lineitem.item-id",
|
|
1392
|
+
"resolver": "sdk.toString",
|
|
1393
|
+
"comment": "Delivery type (STANDARD_SHIPPING, etc.)"
|
|
1394
|
+
},
|
|
1395
|
+
"deliveryAddress": {
|
|
1396
|
+
"fields": {
|
|
1397
|
+
"name": {
|
|
1398
|
+
"source": "$.shipping-address.first-name",
|
|
1399
|
+
"resolver": "custom.combineShippingName",
|
|
1400
|
+
"comment": "Combine first-name + last-name"
|
|
1401
|
+
},
|
|
1402
|
+
"street": {
|
|
1403
|
+
"source": "$.shipping-address.address1",
|
|
1404
|
+
"resolver": "sdk.toString",
|
|
1405
|
+
"comment": "Shipping address line 1 (REQUIRED)"
|
|
1406
|
+
},
|
|
1407
|
+
"street2": {
|
|
1408
|
+
"source": "$.shipping-address.address2",
|
|
1409
|
+
"resolver": "sdk.toString",
|
|
1410
|
+
"comment": "Shipping address line 2 (optional)"
|
|
1411
|
+
},
|
|
1412
|
+
"city": {
|
|
1413
|
+
"source": "$.shipping-address.city",
|
|
1414
|
+
"resolver": "sdk.toString",
|
|
1415
|
+
"comment": "Shipping city (REQUIRED)"
|
|
1416
|
+
},
|
|
1417
|
+
"state": {
|
|
1418
|
+
"source": "$.shipping-address.state-code",
|
|
1419
|
+
"resolver": "sdk.toString",
|
|
1420
|
+
"comment": "Shipping state code (REQUIRED)"
|
|
1421
|
+
},
|
|
1422
|
+
"postcode": {
|
|
1423
|
+
"source": "$.shipping-address.postal-code",
|
|
1424
|
+
"resolver": "sdk.toString",
|
|
1425
|
+
"comment": "Shipping postal code (REQUIRED)"
|
|
1426
|
+
},
|
|
1427
|
+
"country": {
|
|
1428
|
+
"source": "$.shipping-address.country-code",
|
|
1429
|
+
"resolver": "custom.normalizeCountryCode",
|
|
1430
|
+
"comment": "Normalize country code (US → US)"
|
|
1431
|
+
}
|
|
1432
|
+
},
|
|
1433
|
+
"attributes": {
|
|
1434
|
+
"resolver": "custom.buildFulfilmentAddressAttributes",
|
|
1435
|
+
"comment": "Shipping address phone and custom attributes"
|
|
1436
|
+
}
|
|
1437
|
+
},
|
|
1438
|
+
"attributes": {
|
|
1439
|
+
"resolver": "custom.buildFulfilmentChoicesAttributes",
|
|
1440
|
+
"comment": "Fulfilment attributes: gift, totals JSON (merchandize-total, shipping-total, shipment-total), custom-attributes"
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
},
|
|
1444
|
+
"financialTransactions": {
|
|
1445
|
+
"source": "orders.order.payments.payment",
|
|
1446
|
+
"isArray": true,
|
|
1447
|
+
"fields": {
|
|
1448
|
+
"ref": {
|
|
1449
|
+
"source": "$.transaction-id",
|
|
1450
|
+
"resolver": "sdk.toString",
|
|
1451
|
+
"comment": "Transaction ID from SFCC (REQUIRED)"
|
|
1452
|
+
},
|
|
1453
|
+
"type": {
|
|
1454
|
+
"value": "AUTHORIZATION",
|
|
1455
|
+
"comment": "Payment type (REQUIRED)"
|
|
1456
|
+
},
|
|
1457
|
+
"amount": {
|
|
1458
|
+
"source": "$.amount",
|
|
1459
|
+
"resolver": "sdk.parseFloat",
|
|
1460
|
+
"comment": "Payment amount from SFCC (REQUIRED)"
|
|
1461
|
+
},
|
|
1462
|
+
"currency": {
|
|
1463
|
+
"source": "orders.order.currency",
|
|
1464
|
+
"resolver": "sdk.toString",
|
|
1465
|
+
"comment": "Currency from order level (REQUIRED)"
|
|
1466
|
+
},
|
|
1467
|
+
"paymentMethod": {
|
|
1468
|
+
"source": "$.custom-method.method-name",
|
|
1469
|
+
"resolver": "custom.mapPaymentMethod",
|
|
1470
|
+
"comment": "Payment method (KLARNA, CreditCard, etc.) (REQUIRED)"
|
|
1471
|
+
},
|
|
1472
|
+
"externalTransactionCode": {
|
|
1473
|
+
"source": "$.custom-attributes.custom-attribute[attribute-id=klarnaAuthorizationCode]",
|
|
1474
|
+
"resolver": "sdk.toString",
|
|
1475
|
+
"comment": "Authorization code (Klarna, etc.)"
|
|
1476
|
+
},
|
|
1477
|
+
"externalTransactionId": {
|
|
1478
|
+
"source": "$.custom-method.custom-attributes.custom-attribute[attribute-id=klarnaClientToken]",
|
|
1479
|
+
"resolver": "sdk.toString",
|
|
1480
|
+
"comment": "External transaction ID (Klarna client token, etc.)"
|
|
1481
|
+
},
|
|
1482
|
+
"attributes": {
|
|
1483
|
+
"resolver": "custom.buildPaymentAttributes",
|
|
1484
|
+
"comment": "Payment attributes: processor-id, custom-attributes"
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
},
|
|
1489
|
+
"returnFields": ["id", "ref", "status", "totalPrice"]
|
|
1490
|
+
}
|
|
1491
|
+
```
|
|
1492
|
+
|
|
1493
|
+
**Note:** The `returnFields` array specifies which fields are returned in the GraphQL mutation response. These fields are available in `mutationRes.data.createOrder` after executing the mutation. If omitted, defaults to `["id", "ref"]`.
|
|
1494
|
+
|
|
1495
|
+
### 5. Custom Resolvers (src/resolvers/order-resolvers.ts)
|
|
1496
|
+
|
|
1497
|
+
```typescript
|
|
1498
|
+
/**
|
|
1499
|
+
* Order Resolvers for SFCC Native → Fluent
|
|
1500
|
+
*
|
|
1501
|
+
* Maps from SFCC native structure to Fluent order fields
|
|
1502
|
+
*/
|
|
1503
|
+
|
|
1504
|
+
import type { ResolverMap } from './types';
|
|
1505
|
+
|
|
1506
|
+
export const orderResolvers: ResolverMap = {
|
|
1507
|
+
/**
|
|
1508
|
+
* Derive order type from SFCC order structure
|
|
1509
|
+
* Example: 1 shipment → "HD", multiple shipments → "MULTI"
|
|
1510
|
+
*/
|
|
1511
|
+
'custom.deriveOrderType': (value: any, data: any, config: any, helpers: any): string => {
|
|
1512
|
+
const sfccData = data.root || data || {};
|
|
1513
|
+
const shipments = helpers.ensureArray(helpers.get(sfccData, 'orders.order.shipments.shipment'));
|
|
1514
|
+
|
|
1515
|
+
if (shipments.length > 1) {
|
|
1516
|
+
return 'MULTI';
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
return config.defaultOrderType || 'HD';
|
|
1520
|
+
},
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* Get product catalogue reference
|
|
1524
|
+
* Returns default catalogue ref for all items
|
|
1525
|
+
*/
|
|
1526
|
+
'custom.getProductCatalogueRef': (value: any, data: any, config: any, helpers: any): string => {
|
|
1527
|
+
return process.env.PRODUCT_CATALOGUE_REF || 'PC:MASTER:2';
|
|
1528
|
+
},
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* Build order-level attributes from SFCC fields
|
|
1532
|
+
* Extracts order metadata (order-date, created-by, invoice-no, totals)
|
|
1533
|
+
*/
|
|
1534
|
+
'custom.buildOrderAttributes': (
|
|
1535
|
+
value: any,
|
|
1536
|
+
data: any,
|
|
1537
|
+
config: any,
|
|
1538
|
+
helpers: any
|
|
1539
|
+
): Array<{ name: string; type: string; value: any }> => {
|
|
1540
|
+
const sfccData = data.root || data || {};
|
|
1541
|
+
const order = helpers.get(sfccData, 'orders.order');
|
|
1542
|
+
const attributes: Array<{ name: string; type: string; value: any }> = [];
|
|
1543
|
+
|
|
1544
|
+
// Order date
|
|
1545
|
+
const orderDate = helpers.get(order, 'order-date');
|
|
1546
|
+
if (orderDate) {
|
|
1547
|
+
attributes.push({
|
|
1548
|
+
name: 'order-date',
|
|
1549
|
+
type: 'String',
|
|
1550
|
+
value: orderDate,
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// Created by
|
|
1555
|
+
const createdBy = helpers.get(order, 'created-by');
|
|
1556
|
+
if (createdBy) {
|
|
1557
|
+
attributes.push({
|
|
1558
|
+
name: 'created-by',
|
|
1559
|
+
type: 'String',
|
|
1560
|
+
value: createdBy,
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// Invoice number
|
|
1565
|
+
const invoiceNo = helpers.get(order, 'invoice-no');
|
|
1566
|
+
if (invoiceNo) {
|
|
1567
|
+
attributes.push({
|
|
1568
|
+
name: 'invoice-no',
|
|
1569
|
+
type: 'String',
|
|
1570
|
+
value: invoiceNo,
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// Order totals as JSON
|
|
1575
|
+
const totals = helpers.get(order, 'totals');
|
|
1576
|
+
if (totals) {
|
|
1577
|
+
attributes.push({
|
|
1578
|
+
name: 'totals',
|
|
1579
|
+
type: 'JSON',
|
|
1580
|
+
value: JSON.stringify(totals),
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
return attributes;
|
|
1585
|
+
},
|
|
1586
|
+
};
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1589
|
+
### 6. Additional Resolvers (src/resolvers/fulfillment-resolvers.ts, item-resolvers.ts, payment-resolvers.ts)
|
|
1590
|
+
|
|
1591
|
+
```typescript
|
|
1592
|
+
/**
|
|
1593
|
+
* Fulfillment Resolvers
|
|
1594
|
+
* Handle SFCC shipment data → Fluent fulfilmentChoices
|
|
1595
|
+
*/
|
|
1596
|
+
|
|
1597
|
+
export const fulfillmentResolvers: ResolverMap = {
|
|
1598
|
+
/**
|
|
1599
|
+
* Extract first name from SFCC customer-name field
|
|
1600
|
+
* Example: "John Smith" → "John"
|
|
1601
|
+
*/
|
|
1602
|
+
'custom.extractFirstName': (value: any, data: any, config: any, helpers: any): string => {
|
|
1603
|
+
if (!value) return '';
|
|
1604
|
+
const parts = String(value).trim().split(/\s+/);
|
|
1605
|
+
return parts[0] || '';
|
|
1606
|
+
},
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* Extract last name from SFCC customer-name field
|
|
1610
|
+
* Example: "John Smith" → "Smith"
|
|
1611
|
+
*/
|
|
1612
|
+
'custom.extractLastName': (value: any, data: any, config: any, helpers: any): string => {
|
|
1613
|
+
if (!value) return '';
|
|
1614
|
+
const parts = String(value).trim().split(/\s+/);
|
|
1615
|
+
return parts.length > 1 ? parts.slice(1).join(' ') : '';
|
|
1616
|
+
},
|
|
1617
|
+
|
|
1618
|
+
/**
|
|
1619
|
+
* Combine billing address first-name and last-name
|
|
1620
|
+
* Example: "John" + "Smith" → "John Smith"
|
|
1621
|
+
*/
|
|
1622
|
+
'custom.combineBillingName': (value: any, data: any, config: any, helpers: any): string => {
|
|
1623
|
+
const sfccData = data.root || data || {};
|
|
1624
|
+
const firstName = value || helpers.get(sfccData, 'orders.order.customer.billing-address.first-name') || '';
|
|
1625
|
+
const lastName = helpers.get(sfccData, 'orders.order.customer.billing-address.last-name') || '';
|
|
1626
|
+
return `${firstName} ${lastName}`.trim();
|
|
1627
|
+
},
|
|
1628
|
+
|
|
1629
|
+
/**
|
|
1630
|
+
* Combine shipping address first-name and last-name
|
|
1631
|
+
* Example: "Acme" + "Store" → "Acme Store"
|
|
1632
|
+
*/
|
|
1633
|
+
'custom.combineShippingName': (value: any, data: any, config: any, helpers: any): string => {
|
|
1634
|
+
const sfccData = data.root || data || {};
|
|
1635
|
+
const firstName = value || helpers.get(sfccData, 'orders.order.shipments.shipment.shipping-address.first-name') || '';
|
|
1636
|
+
const lastName = helpers.get(sfccData, 'orders.order.shipments.shipment.shipping-address.last-name') || '';
|
|
1637
|
+
return `${firstName} ${lastName}`.trim();
|
|
1638
|
+
},
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* Normalize country code to uppercase
|
|
1642
|
+
* Example: "us" → "US", "US" → "US"
|
|
1643
|
+
*/
|
|
1644
|
+
'custom.normalizeCountryCode': (value: any, data: any, config: any, helpers: any): string => {
|
|
1645
|
+
if (!value) return config.defaultCountry || 'US';
|
|
1646
|
+
return String(value).toUpperCase();
|
|
1647
|
+
},
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* Map SFCC shipping-method to Fluent fulfilment type
|
|
1651
|
+
* Example: "ISPU" → "BOPIS", "STANDARD_SHIPPING" → "SHIP_TO_HOME"
|
|
1652
|
+
*/
|
|
1653
|
+
'custom.mapShippingMethodToFulfilmentType': (value: any, data: any, config: any, helpers: any): string => {
|
|
1654
|
+
const shippingMethod = String(value || '').toUpperCase();
|
|
1655
|
+
const mapping: Record<string, string> = {
|
|
1656
|
+
ISPU: 'BOPIS',
|
|
1657
|
+
STANDARD_SHIPPING: 'SHIP_TO_HOME',
|
|
1658
|
+
EXPRESS_SHIPPING: 'SHIP_TO_HOME',
|
|
1659
|
+
GROUND_SHIPPING: 'SHIP_TO_HOME',
|
|
1660
|
+
};
|
|
1661
|
+
return mapping[shippingMethod] || 'SHIP_TO_HOME';
|
|
1662
|
+
},
|
|
1663
|
+
|
|
1664
|
+
/**
|
|
1665
|
+
* Build billing address attributes (phone)
|
|
1666
|
+
*/
|
|
1667
|
+
'custom.buildBillingAddressAttributes': (
|
|
1668
|
+
value: any,
|
|
1669
|
+
data: any,
|
|
1670
|
+
config: any,
|
|
1671
|
+
helpers: any
|
|
1672
|
+
): Array<{ name: string; type: string; value: any }> => {
|
|
1673
|
+
const sfccData = data.root || data || {};
|
|
1674
|
+
const attributes: Array<{ name: string; type: string; value: any }> = [];
|
|
1675
|
+
|
|
1676
|
+
const phone = helpers.get(sfccData, 'orders.order.customer.billing-address.phone');
|
|
1677
|
+
if (phone) {
|
|
1678
|
+
attributes.push({ name: 'phone', type: 'String', value: String(phone) });
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
return attributes;
|
|
1682
|
+
},
|
|
1683
|
+
|
|
1684
|
+
/**
|
|
1685
|
+
* Build fulfilment address attributes (phone)
|
|
1686
|
+
*/
|
|
1687
|
+
'custom.buildFulfilmentAddressAttributes': (
|
|
1688
|
+
value: any,
|
|
1689
|
+
data: any,
|
|
1690
|
+
config: any,
|
|
1691
|
+
helpers: any
|
|
1692
|
+
): Array<{ name: string; type: string; value: any }> => {
|
|
1693
|
+
const sfccData = data.root || data || {};
|
|
1694
|
+
const attributes: Array<{ name: string; type: string; value: any }> = [];
|
|
1695
|
+
|
|
1696
|
+
const phone = helpers.get(sfccData, 'orders.order.shipments.shipment.shipping-address.phone');
|
|
1697
|
+
if (phone) {
|
|
1698
|
+
attributes.push({ name: 'phone', type: 'String', value: String(phone) });
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
return attributes;
|
|
1702
|
+
},
|
|
1703
|
+
|
|
1704
|
+
/**
|
|
1705
|
+
* Build fulfilment choices attributes
|
|
1706
|
+
* Includes gift flag, totals JSON, and custom attributes
|
|
1707
|
+
*/
|
|
1708
|
+
'custom.buildFulfilmentChoicesAttributes': (
|
|
1709
|
+
value: any,
|
|
1710
|
+
data: any,
|
|
1711
|
+
config: any,
|
|
1712
|
+
helpers: any
|
|
1713
|
+
): Array<{ name: string; type: string; value: any }> => {
|
|
1714
|
+
const sfccData = data.root || data || {};
|
|
1715
|
+
const attributes: Array<{ name: string; type: string; value: any }> = [];
|
|
1716
|
+
|
|
1717
|
+
// Gift flag
|
|
1718
|
+
const gift = helpers.get(sfccData, 'orders.order.shipments.shipment.gift');
|
|
1719
|
+
if (gift !== undefined) {
|
|
1720
|
+
attributes.push({ name: 'gift', type: 'Boolean', value: gift === 'true' || gift === true });
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// Shipment totals as JSON
|
|
1724
|
+
const totals = helpers.get(sfccData, 'orders.order.shipments.shipment.totals');
|
|
1725
|
+
if (totals) {
|
|
1726
|
+
attributes.push({
|
|
1727
|
+
name: 'totals',
|
|
1728
|
+
type: 'JSON',
|
|
1729
|
+
value: JSON.stringify(totals),
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// Custom attributes (fromStoreId, shipmentType, etc.)
|
|
1734
|
+
const customAttrs = helpers.ensureArray(
|
|
1735
|
+
helpers.get(sfccData, 'orders.order.shipments.shipment.custom-attributes.custom-attribute')
|
|
1736
|
+
);
|
|
1737
|
+
for (const attr of customAttrs) {
|
|
1738
|
+
const attrId = attr['@attribute-id'];
|
|
1739
|
+
const attrValue = attr._ || attr['#text'] || attr;
|
|
1740
|
+
if (attrId && attrValue !== undefined) {
|
|
1741
|
+
attributes.push({
|
|
1742
|
+
name: attrId,
|
|
1743
|
+
type: 'String',
|
|
1744
|
+
value: String(attrValue),
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
return attributes;
|
|
1750
|
+
},
|
|
1751
|
+
};
|
|
1752
|
+
|
|
1753
|
+
/**
|
|
1754
|
+
* Item Resolvers
|
|
1755
|
+
* Handle SFCC product-lineitem data → Fluent order items
|
|
1756
|
+
*/
|
|
1757
|
+
|
|
1758
|
+
export const itemResolvers: ResolverMap = {
|
|
1759
|
+
/**
|
|
1760
|
+
* Create item reference from position
|
|
1761
|
+
* Example: position "1" → "item_1"
|
|
1762
|
+
*/
|
|
1763
|
+
'custom.createItemRef': (value: any, data: any, config: any, helpers: any): string => {
|
|
1764
|
+
const position = String(value || '1');
|
|
1765
|
+
return `item_${position}`;
|
|
1766
|
+
},
|
|
1767
|
+
|
|
1768
|
+
/**
|
|
1769
|
+
* Build item attributes
|
|
1770
|
+
* Includes position, tax-basis, tax-rate, shipment-id, gift, custom-attributes
|
|
1771
|
+
*/
|
|
1772
|
+
'custom.buildItemAttributes': (
|
|
1773
|
+
value: any,
|
|
1774
|
+
data: any,
|
|
1775
|
+
config: any,
|
|
1776
|
+
helpers: any
|
|
1777
|
+
): Array<{ name: string; type: string; value: any }> => {
|
|
1778
|
+
const itemData = data || {};
|
|
1779
|
+
const sfccData = data.root || data || {};
|
|
1780
|
+
const attributes: Array<{ name: string; type: string; value: any }> = [];
|
|
1781
|
+
|
|
1782
|
+
// Position
|
|
1783
|
+
const position = itemData.position || helpers.get(sfccData, 'orders.order.product-lineitems.product-lineitem.position');
|
|
1784
|
+
if (position) {
|
|
1785
|
+
attributes.push({ name: 'position', type: 'String', value: String(position) });
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// Tax basis
|
|
1789
|
+
const taxBasis = itemData['tax-basis'] || helpers.get(sfccData, 'orders.order.product-lineitems.product-lineitem.tax-basis');
|
|
1790
|
+
if (taxBasis) {
|
|
1791
|
+
attributes.push({ name: 'tax-basis', type: 'String', value: String(taxBasis) });
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// Tax rate
|
|
1795
|
+
const taxRate = itemData['tax-rate'] || helpers.get(sfccData, 'orders.order.product-lineitems.product-lineitem.tax-rate');
|
|
1796
|
+
if (taxRate) {
|
|
1797
|
+
attributes.push({ name: 'tax-rate', type: 'String', value: String(taxRate) });
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// Shipment ID
|
|
1801
|
+
const shipmentId = itemData['shipment-id'] || helpers.get(sfccData, 'orders.order.product-lineitems.product-lineitem.shipment-id');
|
|
1802
|
+
if (shipmentId) {
|
|
1803
|
+
attributes.push({ name: 'shipment-id', type: 'String', value: String(shipmentId) });
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// Gift flag
|
|
1807
|
+
const gift = itemData.gift || helpers.get(sfccData, 'orders.order.product-lineitems.product-lineitem.gift');
|
|
1808
|
+
if (gift !== undefined) {
|
|
1809
|
+
attributes.push({ name: 'gift', type: 'Boolean', value: gift === 'true' || gift === true });
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// Custom attributes (fromStoreId, storeQty, etc.)
|
|
1813
|
+
const customAttrs = helpers.ensureArray(
|
|
1814
|
+
itemData['custom-attributes']?.['custom-attribute'] ||
|
|
1815
|
+
helpers.get(sfccData, 'orders.order.product-lineitems.product-lineitem.custom-attributes.custom-attribute')
|
|
1816
|
+
);
|
|
1817
|
+
for (const attr of customAttrs) {
|
|
1818
|
+
const attrId = attr['@attribute-id'];
|
|
1819
|
+
const attrValue = attr._ || attr['#text'] || attr;
|
|
1820
|
+
if (attrId && attrValue !== undefined) {
|
|
1821
|
+
attributes.push({
|
|
1822
|
+
name: attrId,
|
|
1823
|
+
type: 'String',
|
|
1824
|
+
value: String(attrValue),
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
return attributes;
|
|
1830
|
+
},
|
|
1831
|
+
};
|
|
1832
|
+
|
|
1833
|
+
/**
|
|
1834
|
+
* Payment Resolvers
|
|
1835
|
+
* Handle SFCC payment data → Fluent financialTransactions
|
|
1836
|
+
*/
|
|
1837
|
+
|
|
1838
|
+
export const paymentResolvers: ResolverMap = {
|
|
1839
|
+
/**
|
|
1840
|
+
* Map SFCC payment method to Fluent payment method
|
|
1841
|
+
* Example: "KLARNA" → "KLARNA", "CreditCard" → "CreditCard"
|
|
1842
|
+
*/
|
|
1843
|
+
'custom.mapPaymentMethod': (value: any, data: any, config: any, helpers: any): string => {
|
|
1844
|
+
const methodName = String(value || '').toUpperCase();
|
|
1845
|
+
const mapping: Record<string, string> = {
|
|
1846
|
+
KLARNA: 'KLARNA',
|
|
1847
|
+
CREDITCARD: 'CreditCard',
|
|
1848
|
+
PAYPAL: 'PayPal',
|
|
1849
|
+
APPLEPAY: 'ApplePay',
|
|
1850
|
+
GOOGLEPAY: 'GooglePay',
|
|
1851
|
+
};
|
|
1852
|
+
return mapping[methodName] || methodName;
|
|
1853
|
+
},
|
|
1854
|
+
|
|
1855
|
+
/**
|
|
1856
|
+
* Build payment attributes
|
|
1857
|
+
* Includes processor-id, custom-attributes (klarnaAuthorizationCode, etc.)
|
|
1858
|
+
*/
|
|
1859
|
+
'custom.buildPaymentAttributes': (
|
|
1860
|
+
value: any,
|
|
1861
|
+
data: any,
|
|
1862
|
+
config: any,
|
|
1863
|
+
helpers: any
|
|
1864
|
+
): Array<{ name: string; type: string; value: any }> => {
|
|
1865
|
+
const paymentData = data || {};
|
|
1866
|
+
const sfccData = data.root || data || {};
|
|
1867
|
+
const attributes: Array<{ name: string; type: string; value: any }> = [];
|
|
1868
|
+
|
|
1869
|
+
// Processor ID
|
|
1870
|
+
const processorId = paymentData['processor-id'] || helpers.get(sfccData, 'orders.order.payments.payment.processor-id');
|
|
1871
|
+
if (processorId) {
|
|
1872
|
+
attributes.push({ name: 'processor-id', type: 'String', value: String(processorId) });
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// Custom attributes
|
|
1876
|
+
const customAttrs = helpers.ensureArray(
|
|
1877
|
+
paymentData['custom-attributes']?.['custom-attribute'] ||
|
|
1878
|
+
helpers.get(sfccData, 'orders.order.payments.payment.custom-attributes.custom-attribute')
|
|
1879
|
+
);
|
|
1880
|
+
for (const attr of customAttrs) {
|
|
1881
|
+
const attrId = attr['@attribute-id'];
|
|
1882
|
+
const attrValue = attr._ || attr['#text'] || attr;
|
|
1883
|
+
if (attrId && attrValue !== undefined) {
|
|
1884
|
+
attributes.push({
|
|
1885
|
+
name: attrId,
|
|
1886
|
+
type: 'String',
|
|
1887
|
+
value: String(attrValue),
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
return attributes;
|
|
1893
|
+
},
|
|
1894
|
+
};
|
|
1895
|
+
```
|
|
1896
|
+
|
|
1897
|
+
### 7. Resolver Types (src/resolvers/types.ts)
|
|
1898
|
+
|
|
1899
|
+
```typescript
|
|
1900
|
+
/**
|
|
1901
|
+
* Type definitions for custom resolvers
|
|
1902
|
+
*/
|
|
1903
|
+
|
|
1904
|
+
export interface ResolverHelpers {
|
|
1905
|
+
logger?: any;
|
|
1906
|
+
fluentClient?: any;
|
|
1907
|
+
get: (obj: any, path: string) => any;
|
|
1908
|
+
ensureArray: (val: any) => any[];
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
export type ResolverFunction = (
|
|
1912
|
+
value: any,
|
|
1913
|
+
data: any,
|
|
1914
|
+
config: any,
|
|
1915
|
+
helpers: ResolverHelpers
|
|
1916
|
+
) => any | Promise<any>;
|
|
1917
|
+
|
|
1918
|
+
export type ResolverMap = Record<string, ResolverFunction>;
|
|
1919
|
+
```
|
|
1920
|
+
|
|
1921
|
+
### 8. Export All Resolvers (src/resolvers/index.ts)
|
|
1922
|
+
|
|
1923
|
+
```typescript
|
|
1924
|
+
/**
|
|
1925
|
+
* All custom resolvers for SFCC native mapping
|
|
1926
|
+
*/
|
|
1927
|
+
|
|
1928
|
+
import { orderResolvers } from './order-resolvers';
|
|
1929
|
+
import { fulfillmentResolvers } from './fulfillment-resolvers';
|
|
1930
|
+
import { itemResolvers } from './item-resolvers';
|
|
1931
|
+
import { paymentResolvers } from './payment-resolvers';
|
|
1932
|
+
|
|
1933
|
+
export const allResolvers = {
|
|
1934
|
+
...orderResolvers,
|
|
1935
|
+
...fulfillmentResolvers,
|
|
1936
|
+
...itemResolvers,
|
|
1937
|
+
...paymentResolvers,
|
|
1938
|
+
};
|
|
1939
|
+
|
|
1940
|
+
// Export individual resolver maps for testing
|
|
1941
|
+
export { orderResolvers, fulfillmentResolvers, itemResolvers, paymentResolvers };
|
|
1942
|
+
```
|
|
1943
|
+
|
|
1944
|
+
### 9. Versori Deployment Files
|
|
1945
|
+
|
|
1946
|
+
#### package.json
|
|
1947
|
+
|
|
1948
|
+
```json
|
|
1949
|
+
{
|
|
1950
|
+
"name": "sfcc-order-webhook",
|
|
1951
|
+
"version": "1.0.0",
|
|
1952
|
+
"type": "module",
|
|
1953
|
+
"dependencies": {
|
|
1954
|
+
"@fluentcommerce/fc-connect-sdk": "^0.1.41",
|
|
1955
|
+
"@versori/run": "latest"
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
```
|
|
1959
|
+
|
|
1960
|
+
#### import_map.json
|
|
1961
|
+
|
|
1962
|
+
```json
|
|
1963
|
+
{
|
|
1964
|
+
"imports": {
|
|
1965
|
+
"@fluentcommerce/fc-connect-sdk": "https://esm.sh/@fluentcommerce/fc-connect-sdk@0.1.41",
|
|
1966
|
+
"@versori/run": "https://esm.sh/@versori/run@latest",
|
|
1967
|
+
"node:buffer": "https://deno.land/std@0.224.0/node/buffer.ts",
|
|
1968
|
+
"node:fs": "https://deno.land/std@0.224.0/node/fs.ts",
|
|
1969
|
+
"node:path": "https://deno.land/std@0.224.0/node/path.ts"
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
```
|
|
1973
|
+
|
|
1974
|
+
---
|
|
1975
|
+
|
|
1976
|
+
## Common Issues
|
|
1977
|
+
|
|
1978
|
+
### Issue 1: "Missing required field: orders.order"
|
|
1979
|
+
|
|
1980
|
+
**Cause**: XML structure doesn't match expected SFCC format
|
|
1981
|
+
|
|
1982
|
+
**Solution**: Validate XML structure
|
|
1983
|
+
|
|
1984
|
+
```typescript
|
|
1985
|
+
// Check structure
|
|
1986
|
+
if (!sfccData.orders?.order) {
|
|
1987
|
+
throw new Error('Invalid XML structure: missing orders.order');
|
|
1988
|
+
}
|
|
1989
|
+
```
|
|
1990
|
+
|
|
1991
|
+
### Issue 2: "Order type is undefined"
|
|
1992
|
+
|
|
1993
|
+
**Cause**: Resolver not deriving order type correctly
|
|
1994
|
+
|
|
1995
|
+
**Solution**: Check shipments array
|
|
1996
|
+
|
|
1997
|
+
```typescript
|
|
1998
|
+
const shipments = helpers.ensureArray(helpers.get(sfccData, 'orders.order.shipments.shipment'));
|
|
1999
|
+
return shipments.length > 1 ? 'MULTI' : 'HD';
|
|
2000
|
+
```
|
|
2001
|
+
|
|
2002
|
+
### Issue 3: "Expected array at path but got object"
|
|
2003
|
+
|
|
2004
|
+
**Cause**: XML parser doesn't create arrays for single items
|
|
2005
|
+
|
|
2006
|
+
**Solution**: Use `helpers.ensureArray()` in resolvers
|
|
2007
|
+
|
|
2008
|
+
```typescript
|
|
2009
|
+
// ✅ CORRECT - wraps single items
|
|
2010
|
+
const items = helpers.ensureArray(helpers.get(sfccData, 'orders.order.product-lineitems.product-lineitem'));
|
|
2011
|
+
```
|
|
2012
|
+
|
|
2013
|
+
---
|
|
2014
|
+
|
|
2015
|
+
## Production Checklist
|
|
2016
|
+
|
|
2017
|
+
Before deploying to production:
|
|
2018
|
+
|
|
2019
|
+
- [ ] Set proper environment variables in Versori (RETAILER_ID, PRODUCT_CATALOGUE_REF)
|
|
2020
|
+
- [ ] Configure error alerting/monitoring
|
|
2021
|
+
- [ ] Test with real SFCC webhook payload
|
|
2022
|
+
- [ ] Test all resolver edge cases (missing data, null values)
|
|
2023
|
+
- [ ] Set up log aggregation
|
|
2024
|
+
- [ ] Configure connection retry logic for transient failures
|
|
2025
|
+
- [ ] Document any customer-specific field mappings
|
|
2026
|
+
|
|
2027
|
+
---
|
|
2028
|
+
|
|
2029
|
+
**Next Steps**: For more advanced SFCC integration patterns, explore the Radial XML version of this template for orders requiring Radial-specific fields.
|