@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,2240 +1,2240 @@
|
|
|
1
|
-
---
|
|
2
|
-
template_id: tpl-webhook-rma-returns-comprehensive
|
|
3
|
-
canonical_filename: template-webhook-rma-returns-comprehensive.md
|
|
4
|
-
version: 2.0.0
|
|
5
|
-
sdk_version: ^0.1.39
|
|
6
|
-
runtime: versori
|
|
7
|
-
direction: ingestion
|
|
8
|
-
source: webhook-json-xml-return
|
|
9
|
-
destination: fluent-graphql
|
|
10
|
-
entity: rma
|
|
11
|
-
format: json-xml
|
|
12
|
-
logging: versori
|
|
13
|
-
status: stable
|
|
14
|
-
features:
|
|
15
|
-
- webhook-signature-validation
|
|
16
|
-
- batched-events
|
|
17
|
-
- attribute-transformation
|
|
18
|
-
- memory-management
|
|
19
|
-
- enhanced-logging
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
# Template: Webhook - RMA Returns Processing
|
|
23
|
-
|
|
24
|
-
**Template Version:** 2.0.0
|
|
25
|
-
**SDK Version:** @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
26
|
-
**Last Updated:** 2025-01-24
|
|
27
|
-
**Deployment Target:** Versori Platform
|
|
28
|
-
|
|
29
|
-
**🆕 Version 2.0.0 Enhancements:**
|
|
30
|
-
- ✅ **Webhook Signature Validation** - Secure webhook verification with HMAC-SHA256
|
|
31
|
-
- ✅ **Batched Events** - Process events in optimized batches to reduce API calls
|
|
32
|
-
- ✅ **Attribute Transformation** - Handle nested arrays and complex data structures
|
|
33
|
-
- ✅ **Memory Management** - Clear large arrays after processing batches
|
|
34
|
-
- ✅ **Enhanced Logging** - Track batch processing and event submission with emoji indicators
|
|
35
|
-
|
|
36
|
-
**FC Connect SDK Use Case Guide**
|
|
37
|
-
|
|
38
|
-
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
39
|
-
> **Version**: Use latest - `npm install @fluentcommerce/fc-connect-sdk@latest`
|
|
40
|
-
|
|
41
|
-
**Context**: Handle customer returns end-to-end from initiation through refund/exchange
|
|
42
|
-
|
|
43
|
-
**Complexity**: Medium-High
|
|
44
|
-
|
|
45
|
-
**Runtime**: Versori Platform
|
|
46
|
-
|
|
47
|
-
**Estimated Lines**: ~950 lines (modular structure)
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
|
|
51
|
-
## STEP 1: Understand This Template
|
|
52
|
-
|
|
53
|
-
**What This Template Does:**
|
|
54
|
-
|
|
55
|
-
- Versori HTTP webhook for return requests
|
|
56
|
-
- Parse return payload from e-commerce (Shopify/SFCC)
|
|
57
|
-
- Create RMA in Fluent Commerce (custom mutation)
|
|
58
|
-
- Generate return shipping label (ShipStation/EasyPost)
|
|
59
|
-
- Track return shipment status
|
|
60
|
-
- Quality inspection workflow (receive → inspect → approve/reject)
|
|
61
|
-
- Process refund or exchange
|
|
62
|
-
- Inventory adjustment (return to stock vs damaged/scrap)
|
|
63
|
-
- Email notifications at each stage
|
|
64
|
-
- VersoriKV state tracking for RMA lifecycle
|
|
65
|
-
- **Sync + Fire-and-Forget Pattern**: Fast webhook response, background processing
|
|
66
|
-
|
|
67
|
-
**Key SDK Components:**
|
|
68
|
-
|
|
69
|
-
- `createClient()` - Universal client factory (auto-detects Versori context)
|
|
70
|
-
- `UniversalMapper` - Transform return payload with custom resolvers
|
|
71
|
-
- `GraphQLMutationMapper` - Map return data to GraphQL mutations
|
|
72
|
-
- `VersoriKVAdapter` - RMA state tracking (KV storage)
|
|
73
|
-
- Native Versori `log` - Use `log` from context
|
|
74
|
-
|
|
75
|
-
**Entity Type:**
|
|
76
|
-
|
|
77
|
-
- **RMA** - Fluent entity for return merchandise authorization
|
|
78
|
-
- **GraphQL Mutations** - Create RMA, update RMA status, process refunds
|
|
79
|
-
|
|
80
|
-
**Critical Patterns:**
|
|
81
|
-
|
|
82
|
-
- **Sync + Fire-and-Forget**: Webhook validates quickly, returns immediately, processes RMA in background
|
|
83
|
-
- **External JSON Config**: Return mapping configuration in separate JSON file (`config/return-mapping.json`)
|
|
84
|
-
- **Modular Architecture**: Separate services, workflows, config, types folders
|
|
85
|
-
- **Background Processing**: Long-running operations (RMA creation, label generation, refunds) happen asynchronously
|
|
86
|
-
- **State Management**: KV storage for RMA lifecycle tracking across multiple webhooks
|
|
87
|
-
- **Multi-Webhook Flow**: Multiple webhooks work together (create → track → inspect → refund)
|
|
88
|
-
|
|
89
|
-
**When to Use This Template:**
|
|
90
|
-
|
|
91
|
-
- ✅ End-to-end returns processing
|
|
92
|
-
- ✅ Multiple return sources (Shopify, SFCC, custom)
|
|
93
|
-
- ✅ Need fast webhook response (don't wait for RMA creation)
|
|
94
|
-
- ✅ Multi-stage returns workflow (create → track → inspect → refund)
|
|
95
|
-
- ✅ State tracking across multiple webhooks
|
|
96
|
-
|
|
97
|
-
**When NOT to Use:**
|
|
98
|
-
|
|
99
|
-
- ❌ Simple return processing (use single webhook)
|
|
100
|
-
- ❌ Bulk return processing (use Batch API or scheduled workflows)
|
|
101
|
-
- ❌ Need synchronous RMA creation (wait for result before responding)
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
## STEP 2: Implementation Prompt for Claude Code
|
|
106
|
-
|
|
107
|
-
**Copy this prompt and send to Claude Code to generate the complete implementation:**
|
|
108
|
-
|
|
109
|
-
```
|
|
110
|
-
Create Versori webhook workflows for comprehensive RMA returns processing to Fluent Commerce.
|
|
111
|
-
|
|
112
|
-
REQUIREMENTS:
|
|
113
|
-
1. Runtime: Versori Platform (HTTP webhooks)
|
|
114
|
-
2. Source: Return data via HTTP POST webhooks (JSON or XML, Shopify/SFCC format)
|
|
115
|
-
3. Destination: Fluent Commerce GraphQL API (RMA mutations)
|
|
116
|
-
4. Format: JSON or XML (Shopify/SFCC compatible)
|
|
117
|
-
5. Entity: RMA (GraphQL mutations for lifecycle management)
|
|
118
|
-
|
|
119
|
-
KEY FEATURES:
|
|
120
|
-
- Sync + fire-and-forget pattern (fast webhook response, background processing)
|
|
121
|
-
- External JSON mapping configuration (config/return-mapping.json)
|
|
122
|
-
- Modular architecture (workflows/, services/, config/, types/)
|
|
123
|
-
- Multiple webhooks for RMA lifecycle (create → track → inspect → refund)
|
|
124
|
-
- UniversalMapper for return payload transformation
|
|
125
|
-
- KV storage for cross-webhook state tracking
|
|
126
|
-
- Comprehensive error handling
|
|
127
|
-
|
|
128
|
-
CRITICAL REQUIREMENTS:
|
|
129
|
-
1. Webhook Mode: response: { mode: 'sync' } (fast response)
|
|
130
|
-
2. Background Processing: Fire-and-forget pattern (no await on long operations)
|
|
131
|
-
3. Mapping Config: External JSON file (config/return-mapping.json)
|
|
132
|
-
4. Modular Structure: Separate services/, config/, types/ folders
|
|
133
|
-
5. Native Logging: Use log from context (no LoggingService)
|
|
134
|
-
6. State Management: VersoriKVAdapter for RMA lifecycle tracking
|
|
135
|
-
|
|
136
|
-
SDK METHODS TO USE:
|
|
137
|
-
- createClient({ ...ctx, log }) - Pass full Versori context
|
|
138
|
-
- new UniversalMapper(mappingConfig) - Transform return payload
|
|
139
|
-
- new GraphQLMutationMapper(mappingConfig, log, { fluentClient: client }) - Map to GraphQL
|
|
140
|
-
- new VersoriKVAdapter(openKv(':project:')) - RMA state storage
|
|
141
|
-
- client.graphql({ query, variables }) - Execute GraphQL mutations
|
|
142
|
-
|
|
143
|
-
FORBIDDEN PATTERNS:
|
|
144
|
-
- ❌ Inline mapping config (use external JSON)
|
|
145
|
-
- ❌ await on background processing (use fire-and-forget)
|
|
146
|
-
- ❌ LoggingService (use native log from context)
|
|
147
|
-
- ❌ All code in one file (use modular structure)
|
|
148
|
-
- ❌ async mode webhook (use sync + fire-and-forget)
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
---
|
|
152
|
-
|
|
153
|
-
## STEP 3: Detailed Flow Documentation
|
|
154
|
-
|
|
155
|
-
### Complete Processing Flow
|
|
156
|
-
|
|
157
|
-
```
|
|
158
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
159
|
-
│ 1. WEBHOOK RECEIVED (Create RMA) │
|
|
160
|
-
│ POST https://{workspace}.versori.run/create-rma │
|
|
161
|
-
│ Content-Type: application/json │
|
|
162
|
-
│ Body: { order_id: "...", return_line_items: [...] } │
|
|
163
|
-
└────────────────────┬────────────────────────────────────────┘
|
|
164
|
-
│
|
|
165
|
-
▼
|
|
166
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
167
|
-
│ 2. QUICK VALIDATION (Synchronous, ~10-50ms) │
|
|
168
|
-
│ - Check fluent_commerce connection exists │
|
|
169
|
-
│ - Validate return payload present │
|
|
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. Transform Return Payload │ │
|
|
182
|
-
│ │ - UniversalMapper with custom resolvers │ │
|
|
183
|
-
│ │ - Map return reasons, calculate fees │ │
|
|
184
|
-
│ └─────────────────────────────────────────────────────┘ │
|
|
185
|
-
│ ┌─────────────────────────────────────────────────────┐ │
|
|
186
|
-
│ │ 3c. Create RMA in Fluent │ │
|
|
187
|
-
│ │ - GraphQL mutation to create RMA │ │
|
|
188
|
-
│ │ - Store RMA state in KV │ │
|
|
189
|
-
│ └─────────────────────────────────────────────────────┘ │
|
|
190
|
-
│ ┌─────────────────────────────────────────────────────┐ │
|
|
191
|
-
│ │ 3d. Generate Return Shipping Label │ │
|
|
192
|
-
│ │ - Call shipping provider API │ │
|
|
193
|
-
│ │ - Update RMA with label URL │ │
|
|
194
|
-
│ └─────────────────────────────────────────────────────┘ │
|
|
195
|
-
└─────────────────────────────────────────────────────────────┘
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
### Response Timing
|
|
199
|
-
|
|
200
|
-
| Stage | Timing | Blocking |
|
|
201
|
-
|-------|--------|----------|
|
|
202
|
-
| **Webhook Validation** | ~10-50ms | ✅ Yes (blocks response) |
|
|
203
|
-
| **Background Processing** | ~2000-5000ms | ❌ No (fire-and-forget) |
|
|
204
|
-
| **Total Response Time** | ~10-50ms | ✅ Fast response |
|
|
205
|
-
|
|
206
|
-
**Key Benefit**: Webhook caller receives immediate acknowledgment (~50ms) while RMA creation happens in background (~2-5s).
|
|
207
|
-
|
|
208
|
-
---
|
|
209
|
-
|
|
210
|
-
## STEP 4: Production Modular Structure
|
|
211
|
-
|
|
212
|
-
> **✅ This section shows the COMPLETE production-ready modular structure.**
|
|
213
|
-
> All files are shown with proper imports/exports and folder organization.
|
|
214
|
-
|
|
215
|
-
### Complete Project Structure
|
|
216
|
-
|
|
217
|
-
```
|
|
218
|
-
rma-returns-processing/
|
|
219
|
-
├── package.json # Dependencies and Versori config
|
|
220
|
-
├── index.ts # Entry point - exports all workflows
|
|
221
|
-
└── src/
|
|
222
|
-
├── workflows/
|
|
223
|
-
│ └── webhook/
|
|
224
|
-
│ ├── create-rma.ts # Webhook: Create RMA from return
|
|
225
|
-
│ ├── track-shipment.ts # Webhook: Track return shipment
|
|
226
|
-
│ ├── quality-inspection.ts # Webhook: Quality inspection
|
|
227
|
-
│ └── process-refund-exchange.ts # Webhook: Process refund/exchange
|
|
228
|
-
│
|
|
229
|
-
├── services/
|
|
230
|
-
│ └── return-processing.service.ts # Shared orchestration logic (reusable)
|
|
231
|
-
│
|
|
232
|
-
├── resolvers/
|
|
233
|
-
│ └── return-resolvers.ts # Custom resolvers for transformations
|
|
234
|
-
│
|
|
235
|
-
├── config/
|
|
236
|
-
│ ├── return-mapping.json # Mapping configuration (external JSON)
|
|
237
|
-
│ └── inspection-mapping.json # Inspection mapping (external JSON)
|
|
238
|
-
│
|
|
239
|
-
└── types/
|
|
240
|
-
└── rma.types.ts # TypeScript interfaces
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
**Why This Structure?**
|
|
244
|
-
|
|
245
|
-
- ✅ **Clear separation**: Multiple webhook handlers vs business logic
|
|
246
|
-
- ✅ **Reusable services**: Return processing logic can be reused
|
|
247
|
-
- ✅ **External config**: Mapping changes don't require code changes
|
|
248
|
-
- ✅ **Custom resolvers**: Separate file for complex transformations
|
|
249
|
-
- ✅ **Type safety**: TypeScript interfaces for better IDE support
|
|
250
|
-
- ✅ **Scalable**: Easy to add new return sources or processing steps
|
|
251
|
-
|
|
252
|
-
---
|
|
253
|
-
|
|
254
|
-
## SDK Methods Used
|
|
255
|
-
|
|
256
|
-
- `webhook(name, handler)` - HTTP webhook endpoint (from `@versori/run`)
|
|
257
|
-
- `fn(name, handler)` - Function handler (from `@versori/run`)
|
|
258
|
-
- `createClient(ctx)` - Auto-detects Versori context, creates FluentClient
|
|
259
|
-
- `UniversalMapper(config, { customResolvers })` - Transform return payload with custom resolvers
|
|
260
|
-
- `client.graphql({ query, variables })` - Execute GraphQL queries/mutations
|
|
261
|
-
- `VersoriKVAdapter(ctx.openKv(':project:'))` - Versori KV storage adapter (:project: scope for cross-webhook state)
|
|
262
|
-
- `kvAdapter.get(key)` - Retrieve state from KV (returns JSON string)
|
|
263
|
-
- `kvAdapter.set(key, value)` - Store state in KV (stores JSON string)
|
|
264
|
-
- Custom resolvers for return reason mapping, restocking fees, eligibility checks
|
|
265
|
-
|
|
266
|
-
---
|
|
267
|
-
|
|
268
|
-
## Versori Workflows Structure
|
|
269
|
-
|
|
270
|
-
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
271
|
-
|
|
272
|
-
**Trigger Types:**
|
|
273
|
-
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
274
|
-
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
275
|
-
- **`http()`** → External API calls (chained from webhook/schedule)
|
|
276
|
-
- **`fn()`** → Internal processing (chained from webhook/schedule)
|
|
277
|
-
|
|
278
|
-
### Recommended Project Structure
|
|
279
|
-
|
|
280
|
-
```
|
|
281
|
-
rma-returns-processing/
|
|
282
|
-
├── index.ts # Entry point - exports all workflows
|
|
283
|
-
└── src/
|
|
284
|
-
├── workflows/
|
|
285
|
-
│ └── webhook/
|
|
286
|
-
│ └── return-processing.ts # Webhook: Process return requests
|
|
287
|
-
│
|
|
288
|
-
├── services/
|
|
289
|
-
│ └── return-processing.service.ts # Shared orchestration logic (reusable)
|
|
290
|
-
│
|
|
291
|
-
└── config/
|
|
292
|
-
└── return-mapping.json # Mapping configuration
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
**Benefits:**
|
|
296
|
-
- ✅ Clear trigger separation (`webhook/` vs `scheduled/`)
|
|
297
|
-
- ✅ Descriptive file names (easy to browse and understand)
|
|
298
|
-
- ✅ Scalable (add new workflows without cluttering)
|
|
299
|
-
- ✅ Reusable code in `services/` (DRY principle)
|
|
300
|
-
- ✅ Easy to modify individual workflows without affecting others
|
|
301
|
-
|
|
302
|
-
---
|
|
303
|
-
|
|
304
|
-
## Complete Working Code
|
|
305
|
-
|
|
306
|
-
### 1. Main Workflow File: `index.ts`
|
|
307
|
-
|
|
308
|
-
```typescript
|
|
309
|
-
/**
|
|
310
|
-
* RMA Returns Processing Workflow
|
|
311
|
-
*
|
|
312
|
-
* Complete returns management from customer initiation through refund/exchange.
|
|
313
|
-
* Supports Shopify and SFCC webhook payloads.
|
|
314
|
-
*
|
|
315
|
-
* Flow:
|
|
316
|
-
* 1. Receive return request webhook
|
|
317
|
-
* 2. Create RMA in Fluent
|
|
318
|
-
* 3. Generate return shipping label
|
|
319
|
-
* 4. Track shipment
|
|
320
|
-
* 5. Quality inspection
|
|
321
|
-
* 6. Process refund/exchange
|
|
322
|
-
* 7. Adjust inventory
|
|
323
|
-
*/
|
|
324
|
-
|
|
325
|
-
import { webhook, http, fn } from '@versori/run';
|
|
326
|
-
import {
|
|
327
|
-
createClient,
|
|
328
|
-
GraphQLMutationMapper,
|
|
329
|
-
UniversalMapper,
|
|
330
|
-
VersoriKVAdapter,
|
|
331
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
332
|
-
|
|
333
|
-
// Import mapping configurations
|
|
334
|
-
import rmaCreationMapping from './mappings/shopify-return-to-rma.json' with { type: 'json' };
|
|
335
|
-
import inspectionMapping from './mappings/inspection-update.json' with { type: 'json' };
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* WEBHOOK 1: Create RMA from Return Request
|
|
339
|
-
*
|
|
340
|
-
* Receives return request from e-commerce platform, creates RMA in Fluent,
|
|
341
|
-
* and generates return shipping label.
|
|
342
|
-
*/
|
|
343
|
-
/**
|
|
344
|
-
* Background processing function for RMA creation
|
|
345
|
-
* Handles all long-running operations (transformation, RMA creation, label generation)
|
|
346
|
-
*/
|
|
347
|
-
async function processRmaCreation(ctx: any, startTime: number): Promise<void> {
|
|
348
|
-
const { log, activation } = ctx;
|
|
349
|
-
const payload = activation?.body || ctx.data;
|
|
350
|
-
|
|
351
|
-
try {
|
|
352
|
-
log.info('🔄 [BACKGROUND] Starting background RMA creation processing', {
|
|
353
|
-
orderId: payload?.order_id || payload?.orderId,
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
// =================================================================
|
|
357
|
-
// STEP 1: PARSE AND VALIDATE RETURN REQUEST
|
|
358
|
-
// =================================================================
|
|
359
|
-
const fluentClient = await createClient(ctx, { validateConnection: true });
|
|
360
|
-
|
|
361
|
-
if (!ctx.connections || !ctx.connections.fluent_commerce) {
|
|
362
|
-
log.error('❌ [BACKGROUND] Missing fluent_commerce connection');
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Use :project: scope for cross-webhook RMA state sharing
|
|
367
|
-
// - createRmaFromReturn stores initial state
|
|
368
|
-
// - trackReturnShipment updates shipping status
|
|
369
|
-
// - qualityInspection adds inspection results
|
|
370
|
-
// - processRefundOrExchange marks completion
|
|
371
|
-
const kvAdapter = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
372
|
-
|
|
373
|
-
// Detect payload format (Shopify JSON vs SFCC XML)
|
|
374
|
-
const isShopify = payload.order_id && payload.return_line_items;
|
|
375
|
-
const isSFCC = payload.ReturnRequest || payload.return;
|
|
376
|
-
|
|
377
|
-
if (!isShopify && !isSFCC) {
|
|
378
|
-
log.error('❌ [BACKGROUND] Invalid payload format');
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
log.info(`🔍 [BACKGROUND] Detected source: ${isShopify ? 'Shopify' : 'SFCC'}`);
|
|
383
|
-
|
|
384
|
-
// =================================================================
|
|
385
|
-
// STEP 2: TRANSFORM TO FLUENT RMA FORMAT
|
|
386
|
-
// =================================================================
|
|
387
|
-
// Custom resolvers for return reason mapping
|
|
388
|
-
const customResolvers = {
|
|
389
|
-
// Map e-commerce return reasons to Fluent reason codes
|
|
390
|
-
'custom.mapReturnReason': (reason: string) => {
|
|
391
|
-
const reasonMap: Record<string, string> = {
|
|
392
|
-
'changed_mind': 'CUSTOMER_REMORSE',
|
|
393
|
-
'defective': 'DEFECTIVE',
|
|
394
|
-
'wrong_item': 'WRONG_ITEM_SHIPPED',
|
|
395
|
-
'damaged': 'DAMAGED_IN_TRANSIT',
|
|
396
|
-
'not_as_described': 'NOT_AS_DESCRIBED',
|
|
397
|
-
'sizing_issues': 'SIZE_FIT_ISSUE',
|
|
398
|
-
'late_delivery': 'LATE_DELIVERY',
|
|
399
|
-
'other': 'OTHER',
|
|
400
|
-
};
|
|
401
|
-
return reasonMap[reason?.toLowerCase()] || 'OTHER';
|
|
402
|
-
},
|
|
403
|
-
|
|
404
|
-
// Calculate restocking fee based on reason
|
|
405
|
-
// Resolver-only field: receives (value, sourceData, helpers)
|
|
406
|
-
// value = undefined (no source), sourceData = current array item
|
|
407
|
-
'custom.calculateRestockingFee': (value: any, sourceData: any, helpers: any) => {
|
|
408
|
-
const feeMap: Record<string, number> = {
|
|
409
|
-
'CUSTOMER_REMORSE': 0.15, // 15% restocking fee
|
|
410
|
-
'SIZE_FIT_ISSUE': 0.10, // 10% restocking fee
|
|
411
|
-
'OTHER': 0.10,
|
|
412
|
-
'DEFECTIVE': 0, // No fee for defective items
|
|
413
|
-
'WRONG_ITEM_SHIPPED': 0, // No fee for our errors
|
|
414
|
-
'DAMAGED_IN_TRANSIT': 0,
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
// Extract fields from sourceData (the current item)
|
|
418
|
-
const reason = sourceData?.reason || 'OTHER';
|
|
419
|
-
const price = helpers.parseFloatSafe(sourceData?.price, 0);
|
|
420
|
-
|
|
421
|
-
const feePercentage = feeMap[reason] || 0;
|
|
422
|
-
return price * feePercentage;
|
|
423
|
-
},
|
|
424
|
-
|
|
425
|
-
// Generate RMA reference
|
|
426
|
-
'custom.generateRmaRef': (orderId: string) => {
|
|
427
|
-
const timestamp = Date.now().toString().slice(-6);
|
|
428
|
-
return `RMA-${orderId}-${timestamp}`;
|
|
429
|
-
},
|
|
430
|
-
|
|
431
|
-
// Determine if return is eligible (30-day window)
|
|
432
|
-
'custom.isReturnEligible': (orderDate: string) => {
|
|
433
|
-
const orderTime = new Date(orderDate).getTime();
|
|
434
|
-
const now = Date.now();
|
|
435
|
-
const daysSinceOrder = (now - orderTime) / (1000 * 60 * 60 * 24);
|
|
436
|
-
return daysSinceOrder <= 30; // 30-day return window
|
|
437
|
-
},
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
// Transform return request to RMA format
|
|
441
|
-
const mapper = new UniversalMapper(rmaCreationMapping, {
|
|
442
|
-
customResolvers,
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
const transformResult = await mapper.map(payload);
|
|
446
|
-
|
|
447
|
-
if (!transformResult.success) {
|
|
448
|
-
log.error('❌ [BACKGROUND] Transformation failed', {
|
|
449
|
-
errors: transformResult.errors,
|
|
450
|
-
});
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const rmaData = transformResult.data;
|
|
455
|
-
|
|
456
|
-
log.info('✅ [RMA] Return request transformed', {
|
|
457
|
-
rmaRef: rmaData.ref,
|
|
458
|
-
itemCount: rmaData.items?.length || 0,
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
// =================================================================
|
|
462
|
-
// STEP 3: CHECK RETURN ELIGIBILITY
|
|
463
|
-
// =================================================================
|
|
464
|
-
if (!rmaData.eligible) {
|
|
465
|
-
log.warn('⚠️ [BACKGROUND] Return request ineligible', {
|
|
466
|
-
orderId: rmaData.orderRef,
|
|
467
|
-
reason: 'Outside return window',
|
|
468
|
-
});
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// =================================================================
|
|
473
|
-
// STEP 4: CHECK FOR DUPLICATE RMA
|
|
474
|
-
// =================================================================
|
|
475
|
-
const existingRmaJson = await kvAdapter.get(`rma-creation:${payload.order_id}`);
|
|
476
|
-
|
|
477
|
-
if (existingRmaJson) {
|
|
478
|
-
const existingRma = JSON.parse(existingRmaJson);
|
|
479
|
-
log.info('🔍 [BACKGROUND] RMA already exists for this order', {
|
|
480
|
-
orderId: payload.order_id,
|
|
481
|
-
existingRmaRef: existingRma.rmaRef,
|
|
482
|
-
});
|
|
483
|
-
return; // Already exists, exit early
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// =================================================================
|
|
487
|
-
// STEP 5: CREATE RMA IN FLUENT COMMERCE
|
|
488
|
-
// =================================================================
|
|
489
|
-
log.info('📝 [RMA] Creating RMA in Fluent', {
|
|
490
|
-
rmaRef: rmaData.ref,
|
|
491
|
-
orderRef: rmaData.orderRef,
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
// Create RMA using custom mutation
|
|
495
|
-
const rmaResult = await fluentClient.graphql({
|
|
496
|
-
query: `mutation CreateRMA($input: CreateRMAInput!) {
|
|
497
|
-
createRMA(input: $input) {
|
|
498
|
-
id
|
|
499
|
-
ref
|
|
500
|
-
status
|
|
501
|
-
orderRef
|
|
502
|
-
items {
|
|
503
|
-
id
|
|
504
|
-
productRef
|
|
505
|
-
quantity
|
|
506
|
-
returnReason
|
|
507
|
-
restockingFee
|
|
508
|
-
}
|
|
509
|
-
customer {
|
|
510
|
-
id
|
|
511
|
-
firstName
|
|
512
|
-
lastName
|
|
513
|
-
email
|
|
514
|
-
}
|
|
515
|
-
createdOn
|
|
516
|
-
attributes {
|
|
517
|
-
name
|
|
518
|
-
value
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}`,
|
|
522
|
-
variables: {
|
|
523
|
-
input: {
|
|
524
|
-
ref: rmaData.ref,
|
|
525
|
-
orderRef: rmaData.orderRef,
|
|
526
|
-
customerId: rmaData.customerId,
|
|
527
|
-
items: rmaData.items,
|
|
528
|
-
status: 'PENDING_APPROVAL',
|
|
529
|
-
returnMethod: rmaData.returnMethod || 'SHIP_BACK',
|
|
530
|
-
attributes: [
|
|
531
|
-
{ name: 'source', type: 'STRING', value: isShopify ? 'Shopify' : 'SFCC' },
|
|
532
|
-
{ name: 'returnReason', type: 'STRING', value: rmaData.primaryReason },
|
|
533
|
-
{ name: 'customerNotes', type: 'STRING', value: rmaData.notes || '' },
|
|
534
|
-
],
|
|
535
|
-
},
|
|
536
|
-
},
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
if (!rmaResult.data?.createRMA) {
|
|
540
|
-
log.error('❌ [BACKGROUND] RMA creation failed', {
|
|
541
|
-
errors: rmaResult.errors,
|
|
542
|
-
});
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
const rma = rmaResult.data.createRMA;
|
|
547
|
-
|
|
548
|
-
log.info('✅ [RMA] RMA created successfully', {
|
|
549
|
-
rmaId: rma.id,
|
|
550
|
-
rmaRef: rma.ref,
|
|
551
|
-
status: rma.status,
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
// =================================================================
|
|
555
|
-
// STEP 6: STORE RMA STATE IN VERSORI KV
|
|
556
|
-
// =================================================================
|
|
557
|
-
const rmaState = {
|
|
558
|
-
rmaId: rma.id,
|
|
559
|
-
rmaRef: rma.ref,
|
|
560
|
-
orderRef: rma.orderRef,
|
|
561
|
-
status: rma.status,
|
|
562
|
-
customerId: rma.customer.id,
|
|
563
|
-
customerEmail: rma.customer.email,
|
|
564
|
-
itemCount: rma.items.length,
|
|
565
|
-
createdOn: rma.createdOn,
|
|
566
|
-
source: isShopify ? 'Shopify' : 'SFCC',
|
|
567
|
-
stage: 'RMA_CREATED',
|
|
568
|
-
};
|
|
569
|
-
|
|
570
|
-
await kvAdapter.set(`rma:${rma.ref}`, JSON.stringify(rmaState));
|
|
571
|
-
|
|
572
|
-
// Store creation timestamp to prevent duplicates
|
|
573
|
-
const creationState = {
|
|
574
|
-
rmaRef: rma.ref,
|
|
575
|
-
createdAt: new Date().toISOString(),
|
|
576
|
-
};
|
|
577
|
-
await kvAdapter.set(`rma-creation:${payload.order_id}`, JSON.stringify(creationState));
|
|
578
|
-
|
|
579
|
-
log.info('[RMA] State stored in KV', { rmaRef: rma.ref });
|
|
580
|
-
|
|
581
|
-
// =================================================================
|
|
582
|
-
// STEP 7: GENERATE RETURN SHIPPING LABEL (MOCK)
|
|
583
|
-
// =================================================================
|
|
584
|
-
log.info('[RMA] Generating return shipping label', {
|
|
585
|
-
rmaRef: rma.ref,
|
|
586
|
-
customerEmail: rma.customer.email,
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
// In production, integrate with ShipStation, EasyPost, or carrier API
|
|
590
|
-
const shippingLabel = {
|
|
591
|
-
trackingNumber: `TRK-${Date.now()}`,
|
|
592
|
-
labelUrl: `https://labels.example.com/${rma.ref}.pdf`,
|
|
593
|
-
carrier: 'USPS',
|
|
594
|
-
service: 'Priority Mail',
|
|
595
|
-
estimatedDelivery: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
596
|
-
};
|
|
597
|
-
|
|
598
|
-
// Update RMA with shipping label info
|
|
599
|
-
await fluentClient.graphql({
|
|
600
|
-
query: `mutation UpdateRMA($id: ID!, $input: UpdateRMAInput!) {
|
|
601
|
-
updateRMA(id: $id, input: $input) {
|
|
602
|
-
id
|
|
603
|
-
ref
|
|
604
|
-
attributes {
|
|
605
|
-
name
|
|
606
|
-
value
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}`,
|
|
610
|
-
variables: {
|
|
611
|
-
id: rma.id,
|
|
612
|
-
input: {
|
|
613
|
-
attributes: [
|
|
614
|
-
{ name: 'returnTrackingNumber', type: 'STRING', value: shippingLabel.trackingNumber },
|
|
615
|
-
{ name: 'returnLabelUrl', type: 'STRING', value: shippingLabel.labelUrl },
|
|
616
|
-
{ name: 'returnCarrier', type: 'STRING', value: shippingLabel.carrier },
|
|
617
|
-
],
|
|
618
|
-
},
|
|
619
|
-
},
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
log.info('[RMA] Shipping label generated', {
|
|
623
|
-
trackingNumber: shippingLabel.trackingNumber,
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
// =================================================================
|
|
627
|
-
// STEP 8: SEND EMAIL NOTIFICATION
|
|
628
|
-
// =================================================================
|
|
629
|
-
// In production, integrate with SendGrid, AWS SES, etc.
|
|
630
|
-
log.info('[RMA] Sending email notification', {
|
|
631
|
-
to: rma.customer.email,
|
|
632
|
-
rmaRef: rma.ref,
|
|
633
|
-
trackingNumber: shippingLabel.trackingNumber,
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
// =================================================================
|
|
637
|
-
// STEP 9: LOG SUCCESS
|
|
638
|
-
// =================================================================
|
|
639
|
-
const duration = Date.now() - startTime;
|
|
640
|
-
log.info('✅ [BACKGROUND] RMA creation completed successfully', {
|
|
641
|
-
rmaRef: rma.ref,
|
|
642
|
-
rmaId: rma.id,
|
|
643
|
-
duration: `${duration}ms`,
|
|
644
|
-
});
|
|
645
|
-
} catch (error: any) {
|
|
646
|
-
// ? Enhanced: Error logging with recommendations
|
|
647
|
-
const errorDetails = {
|
|
648
|
-
message: error instanceof Error ? error.message : String(error),
|
|
649
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
650
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
651
|
-
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
652
|
-
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
653
|
-
: error.message?.includes('mapping') || error.message?.includes('field')
|
|
654
|
-
? 'Check mapping configuration JSON and verify source paths match incoming return request structure'
|
|
655
|
-
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
656
|
-
? 'Check network connectivity and Fluent Commerce API availability'
|
|
657
|
-
: error.message?.includes('duplicate') || error.message?.includes('already exists')
|
|
658
|
-
? 'RMA may already exist for this order - check existing RMAs'
|
|
659
|
-
: error.message?.includes('eligible') || error.message?.includes('return window')
|
|
660
|
-
? 'Return request is outside the return window - verify order date'
|
|
661
|
-
: 'Review error details and check return request payload structure',
|
|
662
|
-
};
|
|
663
|
-
log.error('❌ [BACKGROUND] RMA creation failed', errorDetails);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
/**
|
|
668
|
-
* RMA Creation Webhook Handler
|
|
669
|
-
* Uses sync + fire-and-forget pattern for fast response
|
|
670
|
-
*/
|
|
671
|
-
export const createRmaFromReturn = webhook('create-rma', {
|
|
672
|
-
response: { mode: 'sync' }, // ✅ Sync mode: response sent when handler returns
|
|
673
|
-
}, async (ctx) => {
|
|
674
|
-
const { log, activation } = ctx;
|
|
675
|
-
const startTime = Date.now();
|
|
676
|
-
const payload = activation?.body || ctx.data;
|
|
677
|
-
|
|
678
|
-
log.info('🚀 [WEBHOOK] Received RMA creation webhook', {
|
|
679
|
-
source: payload?.source || 'unknown',
|
|
680
|
-
orderId: payload?.order_id || payload?.orderId,
|
|
681
|
-
timestamp: new Date().toISOString(),
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
// Quick validation
|
|
685
|
-
if (!ctx.connections || !ctx.connections.fluent_commerce) {
|
|
686
|
-
log.error('❌ [WEBHOOK] Missing fluent_commerce connection');
|
|
687
|
-
return {
|
|
688
|
-
status: 500,
|
|
689
|
-
body: {
|
|
690
|
-
success: false,
|
|
691
|
-
error: 'Missing fluent_commerce connection',
|
|
692
|
-
recommendation: 'Configure fluent_commerce connection in Connections section with OAuth2 credentials',
|
|
693
|
-
timestamp: new Date().toISOString(),
|
|
694
|
-
},
|
|
695
|
-
};
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// Detect payload format (Shopify JSON vs SFCC XML)
|
|
699
|
-
const isShopify = payload.order_id && payload.return_line_items;
|
|
700
|
-
const isSFCC = payload.ReturnRequest || payload.return;
|
|
701
|
-
|
|
702
|
-
if (!isShopify && !isSFCC) {
|
|
703
|
-
log.error('❌ [WEBHOOK] Invalid payload format');
|
|
704
|
-
return {
|
|
705
|
-
status: 400,
|
|
706
|
-
body: {
|
|
707
|
-
success: false,
|
|
708
|
-
error: 'INVALID_FORMAT',
|
|
709
|
-
message: 'Unsupported return request format',
|
|
710
|
-
timestamp: new Date().toISOString(),
|
|
711
|
-
},
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
log.info('✅ [WEBHOOK] Validation passed, starting background processing', {
|
|
716
|
-
source: isShopify ? 'Shopify' : 'SFCC',
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
// ✅ Fire-and-forget: Start background processing WITHOUT await
|
|
720
|
-
// The promise continues execution after we return the response
|
|
721
|
-
processRmaCreation(ctx, startTime)
|
|
722
|
-
.then(() => {
|
|
723
|
-
log.info('✅ [BACKGROUND] RMA creation processing completed successfully', {
|
|
724
|
-
orderId: payload?.order_id || payload?.orderId,
|
|
725
|
-
});
|
|
726
|
-
})
|
|
727
|
-
.catch((error: unknown) => {
|
|
728
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
729
|
-
log.error('❌ [BACKGROUND] RMA creation processing failed', {
|
|
730
|
-
orderId: payload?.order_id || payload?.orderId,
|
|
731
|
-
error: errorMessage,
|
|
732
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
733
|
-
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
734
|
-
});
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
// Return immediately (response sent with this return value)
|
|
738
|
-
return {
|
|
739
|
-
status: 200,
|
|
740
|
-
body: {
|
|
741
|
-
success: true,
|
|
742
|
-
message: 'RMA creation started in background',
|
|
743
|
-
timestamp: new Date().toISOString(),
|
|
744
|
-
},
|
|
745
|
-
};
|
|
746
|
-
});
|
|
747
|
-
|
|
748
|
-
/**
|
|
749
|
-
* WEBHOOK 2: Track Return Shipment
|
|
750
|
-
*
|
|
751
|
-
* Receives shipment tracking updates (from carrier or ShipStation)
|
|
752
|
-
* and updates RMA status in Fluent.
|
|
753
|
-
*/
|
|
754
|
-
export const trackReturnShipment = webhook('track-return-shipment', async (ctx) => {
|
|
755
|
-
const { log, activation } = ctx;
|
|
756
|
-
const payload = activation?.body || ctx.data;
|
|
757
|
-
const startTime = Date.now();
|
|
758
|
-
|
|
759
|
-
log.info('🚀 [RMA] Received shipment tracking update', {
|
|
760
|
-
trackingNumber: payload.tracking_number,
|
|
761
|
-
status: payload.status,
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
try {
|
|
765
|
-
const fluentClient = await createClient(ctx, { validateConnection: true });
|
|
766
|
-
const kvAdapter = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
767
|
-
|
|
768
|
-
// Find RMA by tracking number
|
|
769
|
-
const trackingNumber = payload.tracking_number || payload.trackingNumber;
|
|
770
|
-
|
|
771
|
-
// Query Fluent for RMA with this tracking number
|
|
772
|
-
const rmaResult = await fluentClient.graphql({
|
|
773
|
-
query: `query FindRMAByTracking($trackingNumber: String!) {
|
|
774
|
-
rmas(
|
|
775
|
-
first: 1
|
|
776
|
-
attributes: [
|
|
777
|
-
{ name: "returnTrackingNumber", value: $trackingNumber }
|
|
778
|
-
]
|
|
779
|
-
) {
|
|
780
|
-
edges {
|
|
781
|
-
node {
|
|
782
|
-
id
|
|
783
|
-
ref
|
|
784
|
-
status
|
|
785
|
-
orderRef
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
}`,
|
|
790
|
-
variables: { trackingNumber },
|
|
791
|
-
});
|
|
792
|
-
|
|
793
|
-
const rma = rmaResult.data?.rmas?.edges[0]?.node;
|
|
794
|
-
|
|
795
|
-
if (!rma) {
|
|
796
|
-
log.warn('⚠️ [RMA] No RMA found for tracking number', { trackingNumber });
|
|
797
|
-
return {
|
|
798
|
-
status: 404,
|
|
799
|
-
body: {
|
|
800
|
-
success: false,
|
|
801
|
-
error: 'RMA_NOT_FOUND',
|
|
802
|
-
message: `No RMA found with tracking number ${trackingNumber}`,
|
|
803
|
-
timestamp: new Date().toISOString(),
|
|
804
|
-
},
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
log.info('✅ [RMA] Found RMA for tracking update', {
|
|
809
|
-
rmaId: rma.id,
|
|
810
|
-
rmaRef: rma.ref,
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
// Map carrier status to RMA status
|
|
814
|
-
const carrierStatus = payload.status?.toUpperCase();
|
|
815
|
-
let rmaStatus = rma.status;
|
|
816
|
-
let eventType = 'TRACKING_UPDATE';
|
|
817
|
-
|
|
818
|
-
switch (carrierStatus) {
|
|
819
|
-
case 'IN_TRANSIT':
|
|
820
|
-
rmaStatus = 'IN_TRANSIT';
|
|
821
|
-
eventType = 'SHIPMENT_IN_TRANSIT';
|
|
822
|
-
break;
|
|
823
|
-
case 'OUT_FOR_DELIVERY':
|
|
824
|
-
rmaStatus = 'OUT_FOR_DELIVERY';
|
|
825
|
-
eventType = 'SHIPMENT_OUT_FOR_DELIVERY';
|
|
826
|
-
break;
|
|
827
|
-
case 'DELIVERED':
|
|
828
|
-
rmaStatus = 'RECEIVED';
|
|
829
|
-
eventType = 'SHIPMENT_DELIVERED';
|
|
830
|
-
break;
|
|
831
|
-
case 'EXCEPTION':
|
|
832
|
-
case 'FAILED':
|
|
833
|
-
rmaStatus = 'SHIPMENT_EXCEPTION';
|
|
834
|
-
eventType = 'SHIPMENT_EXCEPTION';
|
|
835
|
-
break;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// Update RMA status in Fluent
|
|
839
|
-
await fluentClient.graphql({
|
|
840
|
-
query: `mutation UpdateRMAStatus($id: ID!, $input: UpdateRMAInput!) {
|
|
841
|
-
updateRMA(id: $id, input: $input) {
|
|
842
|
-
id
|
|
843
|
-
ref
|
|
844
|
-
status
|
|
845
|
-
}
|
|
846
|
-
}`,
|
|
847
|
-
variables: {
|
|
848
|
-
id: rma.id,
|
|
849
|
-
input: {
|
|
850
|
-
status: rmaStatus,
|
|
851
|
-
attributes: [
|
|
852
|
-
{ name: 'lastTrackingUpdate', type: 'STRING', value: new Date().toISOString() },
|
|
853
|
-
{ name: 'carrierStatus', type: 'STRING', value: carrierStatus },
|
|
854
|
-
],
|
|
855
|
-
},
|
|
856
|
-
},
|
|
857
|
-
});
|
|
858
|
-
|
|
859
|
-
// Update KV state
|
|
860
|
-
const existingStateJson = await kvAdapter.get(`rma:${rma.ref}`);
|
|
861
|
-
if (existingStateJson) {
|
|
862
|
-
const existingState = JSON.parse(existingStateJson);
|
|
863
|
-
const updatedState = {
|
|
864
|
-
...existingState,
|
|
865
|
-
status: rmaStatus,
|
|
866
|
-
lastTrackingUpdate: new Date().toISOString(),
|
|
867
|
-
carrierStatus,
|
|
868
|
-
stage: eventType,
|
|
869
|
-
};
|
|
870
|
-
await kvAdapter.set(`rma:${rma.ref}`, JSON.stringify(updatedState));
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
const duration = Date.now() - startTime;
|
|
874
|
-
log.info('✅ [RMA] RMA status updated', {
|
|
875
|
-
rmaRef: rma.ref,
|
|
876
|
-
oldStatus: rma.status,
|
|
877
|
-
newStatus: rmaStatus,
|
|
878
|
-
duration: `${duration}ms`,
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
return {
|
|
882
|
-
status: 200,
|
|
883
|
-
body: {
|
|
884
|
-
success: true,
|
|
885
|
-
rmaRef: rma.ref,
|
|
886
|
-
status: rmaStatus,
|
|
887
|
-
eventType,
|
|
888
|
-
message: 'RMA status updated successfully',
|
|
889
|
-
duration: `${duration}ms`,
|
|
890
|
-
timestamp: new Date().toISOString(),
|
|
891
|
-
},
|
|
892
|
-
};
|
|
893
|
-
} catch (error: any) {
|
|
894
|
-
// ? Enhanced: Error logging with recommendations
|
|
895
|
-
const errorDetails = {
|
|
896
|
-
message: error instanceof Error ? error.message : String(error),
|
|
897
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
898
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
899
|
-
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
900
|
-
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
901
|
-
: error.message?.includes('not found') || error.message?.includes('missing')
|
|
902
|
-
? 'RMA not found - verify tracking number and RMA reference'
|
|
903
|
-
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
904
|
-
? 'Check network connectivity and Fluent Commerce API availability'
|
|
905
|
-
: 'Review error details and check tracking update payload structure',
|
|
906
|
-
};
|
|
907
|
-
log.error('[RMA] Failed to process tracking update', errorDetails);
|
|
908
|
-
return {
|
|
909
|
-
status: 500,
|
|
910
|
-
body: {
|
|
911
|
-
success: false,
|
|
912
|
-
error: error.message,
|
|
913
|
-
recommendation: errorDetails.recommendation,
|
|
914
|
-
timestamp: new Date().toISOString(),
|
|
915
|
-
},
|
|
916
|
-
};
|
|
917
|
-
}
|
|
918
|
-
});
|
|
919
|
-
|
|
920
|
-
/**
|
|
921
|
-
* WEBHOOK 3: Quality Inspection
|
|
922
|
-
*
|
|
923
|
-
* Warehouse receives return, inspects items, and updates RMA with inspection results.
|
|
924
|
-
* Determines if items can be restocked, need repair, or should be scrapped.
|
|
925
|
-
*/
|
|
926
|
-
export const qualityInspection = webhook('quality-inspection', async (ctx) => {
|
|
927
|
-
const { log, activation } = ctx;
|
|
928
|
-
const payload = activation?.body || ctx.data;
|
|
929
|
-
const startTime = Date.now();
|
|
930
|
-
|
|
931
|
-
log.info('🚀 [RMA] Processing quality inspection', {
|
|
932
|
-
rmaRef: payload.rmaRef,
|
|
933
|
-
itemCount: payload.items?.length || 0,
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
try {
|
|
937
|
-
const fluentClient = await createClient(ctx, { validateConnection: true });
|
|
938
|
-
const kvAdapter = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
939
|
-
|
|
940
|
-
// Validate payload
|
|
941
|
-
if (!payload.rmaRef || !payload.items || payload.items.length === 0) {
|
|
942
|
-
return {
|
|
943
|
-
status: 400,
|
|
944
|
-
body: {
|
|
945
|
-
success: false,
|
|
946
|
-
error: 'INVALID_PAYLOAD',
|
|
947
|
-
message: 'Invalid inspection payload: missing rmaRef or items',
|
|
948
|
-
timestamp: new Date().toISOString(),
|
|
949
|
-
},
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
// =================================================================
|
|
954
|
-
// STEP 1: RETRIEVE RMA FROM FLUENT
|
|
955
|
-
// =================================================================
|
|
956
|
-
const rmaResult = await fluentClient.graphql({
|
|
957
|
-
query: `query GetRMA($ref: String!) {
|
|
958
|
-
rma(ref: $ref) {
|
|
959
|
-
id
|
|
960
|
-
ref
|
|
961
|
-
status
|
|
962
|
-
orderRef
|
|
963
|
-
items {
|
|
964
|
-
id
|
|
965
|
-
productRef
|
|
966
|
-
quantity
|
|
967
|
-
returnReason
|
|
968
|
-
restockingFee
|
|
969
|
-
}
|
|
970
|
-
customer {
|
|
971
|
-
id
|
|
972
|
-
email
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
}`,
|
|
976
|
-
variables: { ref: payload.rmaRef },
|
|
977
|
-
});
|
|
978
|
-
|
|
979
|
-
const rma = rmaResult.data?.rma;
|
|
980
|
-
|
|
981
|
-
if (!rma) {
|
|
982
|
-
return {
|
|
983
|
-
status: 404,
|
|
984
|
-
body: {
|
|
985
|
-
success: false,
|
|
986
|
-
error: 'RMA_NOT_FOUND',
|
|
987
|
-
message: `RMA not found: ${payload.rmaRef}`,
|
|
988
|
-
timestamp: new Date().toISOString(),
|
|
989
|
-
},
|
|
990
|
-
};
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
log.info('✅ [RMA] RMA retrieved', {
|
|
994
|
-
rmaId: rma.id,
|
|
995
|
-
rmaRef: rma.ref,
|
|
996
|
-
currentStatus: rma.status,
|
|
997
|
-
});
|
|
998
|
-
|
|
999
|
-
// =================================================================
|
|
1000
|
-
// STEP 2: PROCESS INSPECTION RESULTS
|
|
1001
|
-
// =================================================================
|
|
1002
|
-
const inspectionResults = payload.items.map((item: any) => {
|
|
1003
|
-
const condition = item.condition?.toUpperCase();
|
|
1004
|
-
let disposition = 'RESTOCK';
|
|
1005
|
-
let restockable = true;
|
|
1006
|
-
|
|
1007
|
-
switch (condition) {
|
|
1008
|
-
case 'NEW':
|
|
1009
|
-
case 'LIKE_NEW':
|
|
1010
|
-
disposition = 'RESTOCK';
|
|
1011
|
-
restockable = true;
|
|
1012
|
-
break;
|
|
1013
|
-
case 'DAMAGED':
|
|
1014
|
-
case 'DEFECTIVE':
|
|
1015
|
-
disposition = 'SCRAP';
|
|
1016
|
-
restockable = false;
|
|
1017
|
-
break;
|
|
1018
|
-
case 'OPENED':
|
|
1019
|
-
case 'USED':
|
|
1020
|
-
disposition = 'OPEN_BOX';
|
|
1021
|
-
restockable = true;
|
|
1022
|
-
break;
|
|
1023
|
-
case 'MISSING_PARTS':
|
|
1024
|
-
disposition = 'REPAIR';
|
|
1025
|
-
restockable = false;
|
|
1026
|
-
break;
|
|
1027
|
-
default:
|
|
1028
|
-
disposition = 'MANUAL_REVIEW';
|
|
1029
|
-
restockable = false;
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
return {
|
|
1033
|
-
itemId: item.itemId,
|
|
1034
|
-
productRef: item.productRef || item.sku,
|
|
1035
|
-
condition,
|
|
1036
|
-
disposition,
|
|
1037
|
-
restockable,
|
|
1038
|
-
notes: item.notes || '',
|
|
1039
|
-
photoUrls: item.photoUrls || [],
|
|
1040
|
-
};
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
log.info('✅ [RMA] Inspection results processed', {
|
|
1044
|
-
total: inspectionResults.length,
|
|
1045
|
-
restockable: inspectionResults.filter(r => r.restockable).length,
|
|
1046
|
-
scrap: inspectionResults.filter(r => r.disposition === 'SCRAP').length,
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
|
-
// =================================================================
|
|
1050
|
-
// STEP 3: UPDATE RMA WITH INSPECTION RESULTS
|
|
1051
|
-
// =================================================================
|
|
1052
|
-
await fluentClient.graphql({
|
|
1053
|
-
query: `mutation UpdateRMAInspection($id: ID!, $input: UpdateRMAInput!) {
|
|
1054
|
-
updateRMA(id: $id, input: $input) {
|
|
1055
|
-
id
|
|
1056
|
-
ref
|
|
1057
|
-
status
|
|
1058
|
-
}
|
|
1059
|
-
}`,
|
|
1060
|
-
variables: {
|
|
1061
|
-
id: rma.id,
|
|
1062
|
-
input: {
|
|
1063
|
-
status: 'INSPECTED',
|
|
1064
|
-
attributes: [
|
|
1065
|
-
{ name: 'inspectionDate', type: 'STRING', value: new Date().toISOString() },
|
|
1066
|
-
{ name: 'inspectionResults', type: 'JSON', value: JSON.stringify(inspectionResults) },
|
|
1067
|
-
{ name: 'inspector', type: 'STRING', value: payload.inspector || 'SYSTEM' },
|
|
1068
|
-
],
|
|
1069
|
-
},
|
|
1070
|
-
},
|
|
1071
|
-
});
|
|
1072
|
-
|
|
1073
|
-
log.info('[RMA] RMA updated with inspection results', {
|
|
1074
|
-
rmaRef: rma.ref,
|
|
1075
|
-
status: 'INSPECTED',
|
|
1076
|
-
});
|
|
1077
|
-
|
|
1078
|
-
// =================================================================
|
|
1079
|
-
// STEP 4: UPDATE INVENTORY FOR RESTOCKABLE ITEMS
|
|
1080
|
-
// =================================================================
|
|
1081
|
-
const restockableItems = inspectionResults.filter(r => r.restockable);
|
|
1082
|
-
|
|
1083
|
-
if (restockableItems.length > 0) {
|
|
1084
|
-
log.info('[RMA] Restocking items', {
|
|
1085
|
-
count: restockableItems.length,
|
|
1086
|
-
});
|
|
1087
|
-
|
|
1088
|
-
for (const item of restockableItems) {
|
|
1089
|
-
// Increment inventory for restockable items
|
|
1090
|
-
await fluentClient.graphql({
|
|
1091
|
-
query: `mutation AdjustInventory($input: AdjustInventoryInput!) {
|
|
1092
|
-
adjustInventory(input: $input) {
|
|
1093
|
-
id
|
|
1094
|
-
ref
|
|
1095
|
-
quantity
|
|
1096
|
-
}
|
|
1097
|
-
}`,
|
|
1098
|
-
variables: {
|
|
1099
|
-
input: {
|
|
1100
|
-
productRef: item.productRef,
|
|
1101
|
-
locationRef: payload.locationRef || 'RETURNS_WAREHOUSE',
|
|
1102
|
-
adjustment: 1, // Increment by quantity returned
|
|
1103
|
-
reason: 'RMA_RESTOCK',
|
|
1104
|
-
ref: `${rma.ref}-${item.productRef}`,
|
|
1105
|
-
},
|
|
1106
|
-
},
|
|
1107
|
-
});
|
|
1108
|
-
|
|
1109
|
-
log.info('[RMA] Inventory adjusted', {
|
|
1110
|
-
productRef: item.productRef,
|
|
1111
|
-
disposition: item.disposition,
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
// =================================================================
|
|
1117
|
-
// STEP 5: UPDATE KV STATE
|
|
1118
|
-
// =================================================================
|
|
1119
|
-
const existingStateJson = await kvAdapter.get(`rma:${rma.ref}`);
|
|
1120
|
-
if (existingStateJson) {
|
|
1121
|
-
const existingState = JSON.parse(existingStateJson);
|
|
1122
|
-
const updatedState = {
|
|
1123
|
-
...existingState,
|
|
1124
|
-
status: 'INSPECTED',
|
|
1125
|
-
stage: 'INSPECTION_COMPLETE',
|
|
1126
|
-
inspectionResults,
|
|
1127
|
-
inspectionDate: new Date().toISOString(),
|
|
1128
|
-
};
|
|
1129
|
-
await kvAdapter.set(`rma:${rma.ref}`, JSON.stringify(updatedState));
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
// =================================================================
|
|
1133
|
-
// STEP 6: DETERMINE NEXT ACTION (REFUND OR EXCHANGE)
|
|
1134
|
-
// =================================================================
|
|
1135
|
-
const refundApproved = inspectionResults.every(r =>
|
|
1136
|
-
r.disposition === 'RESTOCK' || r.disposition === 'OPEN_BOX'
|
|
1137
|
-
);
|
|
1138
|
-
|
|
1139
|
-
const duration = Date.now() - startTime;
|
|
1140
|
-
log.info('✅ [RMA] Inspection complete', {
|
|
1141
|
-
rmaRef: rma.ref,
|
|
1142
|
-
refundApproved,
|
|
1143
|
-
nextAction: payload.returnType === 'exchange' ? 'CREATE_EXCHANGE_ORDER' : 'PROCESS_REFUND',
|
|
1144
|
-
duration: `${duration}ms`,
|
|
1145
|
-
});
|
|
1146
|
-
|
|
1147
|
-
return {
|
|
1148
|
-
status: 200,
|
|
1149
|
-
body: {
|
|
1150
|
-
success: true,
|
|
1151
|
-
rmaRef: rma.ref,
|
|
1152
|
-
status: 'INSPECTED',
|
|
1153
|
-
inspectionResults,
|
|
1154
|
-
refundApproved,
|
|
1155
|
-
nextAction: payload.returnType === 'exchange' ? 'CREATE_EXCHANGE_ORDER' : 'PROCESS_REFUND',
|
|
1156
|
-
message: 'Quality inspection completed successfully',
|
|
1157
|
-
duration: `${duration}ms`,
|
|
1158
|
-
timestamp: new Date().toISOString(),
|
|
1159
|
-
},
|
|
1160
|
-
};
|
|
1161
|
-
} catch (error: any) {
|
|
1162
|
-
// ? Enhanced: Error logging with recommendations
|
|
1163
|
-
const errorDetails = {
|
|
1164
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1165
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1166
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
1167
|
-
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
1168
|
-
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
1169
|
-
: error.message?.includes('not found') || error.message?.includes('missing')
|
|
1170
|
-
? 'RMA not found - verify RMA reference and inspection payload structure'
|
|
1171
|
-
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
1172
|
-
? 'Check network connectivity and Fluent Commerce API availability'
|
|
1173
|
-
: 'Review error details and check inspection payload structure',
|
|
1174
|
-
};
|
|
1175
|
-
log.error('[RMA] Failed to process inspection', errorDetails);
|
|
1176
|
-
return {
|
|
1177
|
-
status: 500,
|
|
1178
|
-
body: {
|
|
1179
|
-
success: false,
|
|
1180
|
-
error: error.message,
|
|
1181
|
-
recommendation: errorDetails.recommendation,
|
|
1182
|
-
timestamp: new Date().toISOString(),
|
|
1183
|
-
},
|
|
1184
|
-
};
|
|
1185
|
-
}
|
|
1186
|
-
});
|
|
1187
|
-
|
|
1188
|
-
/**
|
|
1189
|
-
* WEBHOOK 4: Process Refund or Exchange
|
|
1190
|
-
*
|
|
1191
|
-
* After inspection approval, processes refund via payment gateway
|
|
1192
|
-
* or creates exchange order.
|
|
1193
|
-
*/
|
|
1194
|
-
export const processRefundOrExchange = webhook('process-refund-exchange', async (ctx) => {
|
|
1195
|
-
const { log, activation } = ctx;
|
|
1196
|
-
const payload = activation?.body || ctx.data;
|
|
1197
|
-
const startTime = Date.now();
|
|
1198
|
-
|
|
1199
|
-
log.info('🚀 [RMA] Processing refund/exchange', {
|
|
1200
|
-
rmaRef: payload.rmaRef,
|
|
1201
|
-
actionType: payload.actionType, // 'refund' or 'exchange'
|
|
1202
|
-
});
|
|
1203
|
-
|
|
1204
|
-
try {
|
|
1205
|
-
const fluentClient = await createClient(ctx, { validateConnection: true });
|
|
1206
|
-
const kvAdapter = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
1207
|
-
|
|
1208
|
-
// Retrieve RMA
|
|
1209
|
-
const rmaResult = await fluentClient.graphql({
|
|
1210
|
-
query: `query GetRMA($ref: String!) {
|
|
1211
|
-
rma(ref: $ref) {
|
|
1212
|
-
id
|
|
1213
|
-
ref
|
|
1214
|
-
status
|
|
1215
|
-
orderRef
|
|
1216
|
-
items {
|
|
1217
|
-
id
|
|
1218
|
-
productRef
|
|
1219
|
-
quantity
|
|
1220
|
-
price
|
|
1221
|
-
restockingFee
|
|
1222
|
-
}
|
|
1223
|
-
customer {
|
|
1224
|
-
id
|
|
1225
|
-
email
|
|
1226
|
-
}
|
|
1227
|
-
attributes {
|
|
1228
|
-
name
|
|
1229
|
-
value
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
}`,
|
|
1233
|
-
variables: { ref: payload.rmaRef },
|
|
1234
|
-
});
|
|
1235
|
-
|
|
1236
|
-
const rma = rmaResult.data?.rma;
|
|
1237
|
-
|
|
1238
|
-
if (!rma) {
|
|
1239
|
-
return {
|
|
1240
|
-
status: 404,
|
|
1241
|
-
body: {
|
|
1242
|
-
success: false,
|
|
1243
|
-
error: 'RMA_NOT_FOUND',
|
|
1244
|
-
message: `RMA not found: ${payload.rmaRef}`,
|
|
1245
|
-
timestamp: new Date().toISOString(),
|
|
1246
|
-
},
|
|
1247
|
-
};
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
const actionType = payload.actionType?.toLowerCase();
|
|
1251
|
-
|
|
1252
|
-
// =================================================================
|
|
1253
|
-
// OPTION 1: PROCESS REFUND
|
|
1254
|
-
// =================================================================
|
|
1255
|
-
if (actionType === 'refund') {
|
|
1256
|
-
log.info('💰 [RMA] Processing refund', { rmaRef: rma.ref });
|
|
1257
|
-
|
|
1258
|
-
// Calculate refund amount (subtract restocking fees)
|
|
1259
|
-
const totalRefund = rma.items.reduce((sum: number, item: any) => {
|
|
1260
|
-
const itemTotal = item.price * item.quantity;
|
|
1261
|
-
const restockingFee = item.restockingFee || 0;
|
|
1262
|
-
return sum + (itemTotal - restockingFee);
|
|
1263
|
-
}, 0);
|
|
1264
|
-
|
|
1265
|
-
log.info('[RMA] Refund calculated', {
|
|
1266
|
-
totalRefund,
|
|
1267
|
-
itemCount: rma.items.length,
|
|
1268
|
-
});
|
|
1269
|
-
|
|
1270
|
-
// In production, integrate with payment gateway (Stripe, Adyen, etc.)
|
|
1271
|
-
const refundResponse = {
|
|
1272
|
-
refundId: `REF-${Date.now()}`,
|
|
1273
|
-
amount: totalRefund,
|
|
1274
|
-
currency: 'USD',
|
|
1275
|
-
status: 'PROCESSED',
|
|
1276
|
-
transactionId: `TXN-${Date.now()}`,
|
|
1277
|
-
};
|
|
1278
|
-
|
|
1279
|
-
// Update RMA status
|
|
1280
|
-
await fluentClient.graphql({
|
|
1281
|
-
query: `mutation UpdateRMARefund($id: ID!, $input: UpdateRMAInput!) {
|
|
1282
|
-
updateRMA(id: $id, input: $input) {
|
|
1283
|
-
id
|
|
1284
|
-
ref
|
|
1285
|
-
status
|
|
1286
|
-
}
|
|
1287
|
-
}`,
|
|
1288
|
-
variables: {
|
|
1289
|
-
id: rma.id,
|
|
1290
|
-
input: {
|
|
1291
|
-
status: 'REFUNDED',
|
|
1292
|
-
attributes: [
|
|
1293
|
-
{ name: 'refundId', type: 'STRING', value: refundResponse.refundId },
|
|
1294
|
-
{ name: 'refundAmount', type: 'STRING', value: totalRefund.toString() },
|
|
1295
|
-
{ name: 'refundDate', type: 'STRING', value: new Date().toISOString() },
|
|
1296
|
-
],
|
|
1297
|
-
},
|
|
1298
|
-
},
|
|
1299
|
-
});
|
|
1300
|
-
|
|
1301
|
-
// Update KV state
|
|
1302
|
-
const existingStateJson = await kvAdapter.get(`rma:${rma.ref}`);
|
|
1303
|
-
if (existingStateJson) {
|
|
1304
|
-
const existingState = JSON.parse(existingStateJson);
|
|
1305
|
-
const updatedState = {
|
|
1306
|
-
...existingState,
|
|
1307
|
-
status: 'REFUNDED',
|
|
1308
|
-
stage: 'REFUND_COMPLETE',
|
|
1309
|
-
refundAmount: totalRefund,
|
|
1310
|
-
refundId: refundResponse.refundId,
|
|
1311
|
-
refundDate: new Date().toISOString(),
|
|
1312
|
-
};
|
|
1313
|
-
await kvAdapter.set(`rma:${rma.ref}`, JSON.stringify(updatedState));
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
const duration = Date.now() - startTime;
|
|
1317
|
-
log.info('✅ [RMA] Refund processed successfully', {
|
|
1318
|
-
rmaRef: rma.ref,
|
|
1319
|
-
refundId: refundResponse.refundId,
|
|
1320
|
-
amount: totalRefund,
|
|
1321
|
-
duration: `${duration}ms`,
|
|
1322
|
-
});
|
|
1323
|
-
|
|
1324
|
-
return {
|
|
1325
|
-
status: 200,
|
|
1326
|
-
body: {
|
|
1327
|
-
success: true,
|
|
1328
|
-
rmaRef: rma.ref,
|
|
1329
|
-
actionType: 'refund',
|
|
1330
|
-
refund: refundResponse,
|
|
1331
|
-
message: `Refund of $${totalRefund} processed successfully`,
|
|
1332
|
-
duration: `${duration}ms`,
|
|
1333
|
-
timestamp: new Date().toISOString(),
|
|
1334
|
-
},
|
|
1335
|
-
};
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
// =================================================================
|
|
1339
|
-
// OPTION 2: CREATE EXCHANGE ORDER
|
|
1340
|
-
// =================================================================
|
|
1341
|
-
if (actionType === 'exchange') {
|
|
1342
|
-
log.info('🔄 [RMA] Creating exchange order', { rmaRef: rma.ref });
|
|
1343
|
-
|
|
1344
|
-
// Exchange items (from payload)
|
|
1345
|
-
const exchangeItems = payload.exchangeItems || [];
|
|
1346
|
-
|
|
1347
|
-
if (exchangeItems.length === 0) {
|
|
1348
|
-
return {
|
|
1349
|
-
status: 400,
|
|
1350
|
-
body: {
|
|
1351
|
-
success: false,
|
|
1352
|
-
error: 'INVALID_PAYLOAD',
|
|
1353
|
-
message: 'No exchange items provided',
|
|
1354
|
-
timestamp: new Date().toISOString(),
|
|
1355
|
-
},
|
|
1356
|
-
};
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
// Create new order in Fluent
|
|
1360
|
-
const exchangeOrderResult = await fluentClient.graphql({
|
|
1361
|
-
query: `mutation CreateExchangeOrder($input: CreateOrderInput!) {
|
|
1362
|
-
createOrder(input: $input) {
|
|
1363
|
-
id
|
|
1364
|
-
ref
|
|
1365
|
-
status
|
|
1366
|
-
items {
|
|
1367
|
-
id
|
|
1368
|
-
productRef
|
|
1369
|
-
quantity
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
}`,
|
|
1373
|
-
variables: {
|
|
1374
|
-
input: {
|
|
1375
|
-
ref: `EXCH-${rma.ref}`,
|
|
1376
|
-
type: 'EXCHANGE',
|
|
1377
|
-
customerId: rma.customer.id,
|
|
1378
|
-
items: exchangeItems.map((item: any) => ({
|
|
1379
|
-
productRef: item.productRef,
|
|
1380
|
-
quantity: item.quantity,
|
|
1381
|
-
})),
|
|
1382
|
-
attributes: [
|
|
1383
|
-
{ name: 'originalRMA', type: 'STRING', value: rma.ref },
|
|
1384
|
-
{ name: 'originalOrder', type: 'STRING', value: rma.orderRef },
|
|
1385
|
-
{ name: 'source', type: 'STRING', value: 'RMA_EXCHANGE' },
|
|
1386
|
-
],
|
|
1387
|
-
},
|
|
1388
|
-
},
|
|
1389
|
-
});
|
|
1390
|
-
|
|
1391
|
-
const exchangeOrder = exchangeOrderResult.data?.createOrder;
|
|
1392
|
-
|
|
1393
|
-
if (!exchangeOrder) {
|
|
1394
|
-
return {
|
|
1395
|
-
status: 502,
|
|
1396
|
-
body: {
|
|
1397
|
-
success: false,
|
|
1398
|
-
error: 'API_ERROR',
|
|
1399
|
-
message: 'Failed to create exchange order',
|
|
1400
|
-
timestamp: new Date().toISOString(),
|
|
1401
|
-
},
|
|
1402
|
-
};
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
// Update RMA with exchange order reference
|
|
1406
|
-
await fluentClient.graphql({
|
|
1407
|
-
query: `mutation UpdateRMAExchange($id: ID!, $input: UpdateRMAInput!) {
|
|
1408
|
-
updateRMA(id: $id, input: $input) {
|
|
1409
|
-
id
|
|
1410
|
-
ref
|
|
1411
|
-
status
|
|
1412
|
-
}
|
|
1413
|
-
}`,
|
|
1414
|
-
variables: {
|
|
1415
|
-
id: rma.id,
|
|
1416
|
-
input: {
|
|
1417
|
-
status: 'EXCHANGED',
|
|
1418
|
-
attributes: [
|
|
1419
|
-
{ name: 'exchangeOrderRef', type: 'STRING', value: exchangeOrder.ref },
|
|
1420
|
-
{ name: 'exchangeDate', type: 'STRING', value: new Date().toISOString() },
|
|
1421
|
-
],
|
|
1422
|
-
},
|
|
1423
|
-
},
|
|
1424
|
-
});
|
|
1425
|
-
|
|
1426
|
-
// Update KV state
|
|
1427
|
-
const existingStateJson = await kvAdapter.get(`rma:${rma.ref}`);
|
|
1428
|
-
if (existingStateJson) {
|
|
1429
|
-
const existingState = JSON.parse(existingStateJson);
|
|
1430
|
-
const updatedState = {
|
|
1431
|
-
...existingState,
|
|
1432
|
-
status: 'EXCHANGED',
|
|
1433
|
-
stage: 'EXCHANGE_COMPLETE',
|
|
1434
|
-
exchangeOrderRef: exchangeOrder.ref,
|
|
1435
|
-
exchangeDate: new Date().toISOString(),
|
|
1436
|
-
};
|
|
1437
|
-
await kvAdapter.set(`rma:${rma.ref}`, JSON.stringify(updatedState));
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
const duration = Date.now() - startTime;
|
|
1441
|
-
log.info('✅ [RMA] Exchange order created', {
|
|
1442
|
-
rmaRef: rma.ref,
|
|
1443
|
-
exchangeOrderRef: exchangeOrder.ref,
|
|
1444
|
-
duration: `${duration}ms`,
|
|
1445
|
-
});
|
|
1446
|
-
|
|
1447
|
-
return {
|
|
1448
|
-
status: 200,
|
|
1449
|
-
body: {
|
|
1450
|
-
success: true,
|
|
1451
|
-
rmaRef: rma.ref,
|
|
1452
|
-
actionType: 'exchange',
|
|
1453
|
-
exchangeOrder: {
|
|
1454
|
-
ref: exchangeOrder.ref,
|
|
1455
|
-
id: exchangeOrder.id,
|
|
1456
|
-
status: exchangeOrder.status,
|
|
1457
|
-
},
|
|
1458
|
-
message: `Exchange order ${exchangeOrder.ref} created successfully`,
|
|
1459
|
-
duration: `${duration}ms`,
|
|
1460
|
-
timestamp: new Date().toISOString(),
|
|
1461
|
-
},
|
|
1462
|
-
};
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
return {
|
|
1466
|
-
status: 400,
|
|
1467
|
-
body: {
|
|
1468
|
-
success: false,
|
|
1469
|
-
error: 'INVALID_ACTION_TYPE',
|
|
1470
|
-
message: `Invalid action type: ${actionType}`,
|
|
1471
|
-
timestamp: new Date().toISOString(),
|
|
1472
|
-
},
|
|
1473
|
-
};
|
|
1474
|
-
} catch (error: any) {
|
|
1475
|
-
// ? Enhanced: Error logging with recommendations
|
|
1476
|
-
const errorDetails = {
|
|
1477
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1478
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1479
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
1480
|
-
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
1481
|
-
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
1482
|
-
: error.message?.includes('payment') || error.message?.includes('gateway')
|
|
1483
|
-
? 'Check payment gateway configuration and refund processing credentials'
|
|
1484
|
-
: error.message?.includes('exchange') || error.message?.includes('order')
|
|
1485
|
-
? 'Check exchange order creation payload and product availability'
|
|
1486
|
-
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
1487
|
-
? 'Check network connectivity and Fluent Commerce API availability'
|
|
1488
|
-
: 'Review error details and check refund/exchange payload structure',
|
|
1489
|
-
};
|
|
1490
|
-
log.error('[RMA] Failed to process refund/exchange', errorDetails);
|
|
1491
|
-
return {
|
|
1492
|
-
status: 500,
|
|
1493
|
-
body: {
|
|
1494
|
-
success: false,
|
|
1495
|
-
error: error.message,
|
|
1496
|
-
recommendation: errorDetails.recommendation,
|
|
1497
|
-
timestamp: new Date().toISOString(),
|
|
1498
|
-
},
|
|
1499
|
-
};
|
|
1500
|
-
}
|
|
1501
|
-
});
|
|
1502
|
-
|
|
1503
|
-
/**
|
|
1504
|
-
* WEBHOOK 5: Get RMA Status
|
|
1505
|
-
*
|
|
1506
|
-
* Query endpoint to retrieve RMA status and history
|
|
1507
|
-
*/
|
|
1508
|
-
export const getRmaStatus = webhook('get-rma-status', async (ctx) => {
|
|
1509
|
-
const { log, activation } = ctx;
|
|
1510
|
-
const payload = activation?.body || ctx.data;
|
|
1511
|
-
const rmaRef = payload?.rmaRef || ctx.query?.rmaRef;
|
|
1512
|
-
const startTime = Date.now();
|
|
1513
|
-
|
|
1514
|
-
log.info('🚀 [RMA] Querying RMA status', { rmaRef });
|
|
1515
|
-
|
|
1516
|
-
if (!rmaRef) {
|
|
1517
|
-
return {
|
|
1518
|
-
status: 400,
|
|
1519
|
-
body: {
|
|
1520
|
-
success: false,
|
|
1521
|
-
error: 'MISSING_PARAMETER',
|
|
1522
|
-
message: 'Missing rmaRef parameter',
|
|
1523
|
-
timestamp: new Date().toISOString(),
|
|
1524
|
-
},
|
|
1525
|
-
};
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
try {
|
|
1529
|
-
const fluentClient = await createClient(ctx, { validateConnection: true });
|
|
1530
|
-
const kvAdapter = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
1531
|
-
|
|
1532
|
-
// Get RMA from Fluent
|
|
1533
|
-
const rmaResult = await fluentClient.graphql({
|
|
1534
|
-
query: `query GetRMA($ref: String!) {
|
|
1535
|
-
rma(ref: $ref) {
|
|
1536
|
-
id
|
|
1537
|
-
ref
|
|
1538
|
-
status
|
|
1539
|
-
orderRef
|
|
1540
|
-
items {
|
|
1541
|
-
id
|
|
1542
|
-
productRef
|
|
1543
|
-
quantity
|
|
1544
|
-
returnReason
|
|
1545
|
-
price
|
|
1546
|
-
restockingFee
|
|
1547
|
-
}
|
|
1548
|
-
customer {
|
|
1549
|
-
id
|
|
1550
|
-
firstName
|
|
1551
|
-
lastName
|
|
1552
|
-
email
|
|
1553
|
-
}
|
|
1554
|
-
createdOn
|
|
1555
|
-
updatedOn
|
|
1556
|
-
attributes {
|
|
1557
|
-
name
|
|
1558
|
-
value
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
}`,
|
|
1562
|
-
variables: { ref: rmaRef },
|
|
1563
|
-
});
|
|
1564
|
-
|
|
1565
|
-
const rma = rmaResult.data?.rma;
|
|
1566
|
-
|
|
1567
|
-
if (!rma) {
|
|
1568
|
-
return {
|
|
1569
|
-
status: 404,
|
|
1570
|
-
body: {
|
|
1571
|
-
success: false,
|
|
1572
|
-
error: 'RMA_NOT_FOUND',
|
|
1573
|
-
message: `RMA not found: ${rmaRef}`,
|
|
1574
|
-
timestamp: new Date().toISOString(),
|
|
1575
|
-
},
|
|
1576
|
-
};
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
// Get state from KV
|
|
1580
|
-
const rmaStateJson = await kvAdapter.get(`rma:${rmaRef}`);
|
|
1581
|
-
const rmaState = rmaStateJson ? JSON.parse(rmaStateJson) : null;
|
|
1582
|
-
|
|
1583
|
-
const duration = Date.now() - startTime;
|
|
1584
|
-
log.info('✅ [RMA] Status query complete', {
|
|
1585
|
-
rmaRef: rma.ref,
|
|
1586
|
-
status: rma.status,
|
|
1587
|
-
duration: `${duration}ms`,
|
|
1588
|
-
});
|
|
1589
|
-
|
|
1590
|
-
return {
|
|
1591
|
-
status: 200,
|
|
1592
|
-
body: {
|
|
1593
|
-
success: true,
|
|
1594
|
-
rma: {
|
|
1595
|
-
id: rma.id,
|
|
1596
|
-
ref: rma.ref,
|
|
1597
|
-
status: rma.status,
|
|
1598
|
-
orderRef: rma.orderRef,
|
|
1599
|
-
itemCount: rma.items.length,
|
|
1600
|
-
customer: rma.customer,
|
|
1601
|
-
createdOn: rma.createdOn,
|
|
1602
|
-
updatedOn: rma.updatedOn,
|
|
1603
|
-
},
|
|
1604
|
-
state: rmaState,
|
|
1605
|
-
attributes: rma.attributes,
|
|
1606
|
-
duration: `${duration}ms`,
|
|
1607
|
-
timestamp: new Date().toISOString(),
|
|
1608
|
-
},
|
|
1609
|
-
};
|
|
1610
|
-
} catch (error: any) {
|
|
1611
|
-
// ? Enhanced: Error logging with recommendations
|
|
1612
|
-
const errorDetails = {
|
|
1613
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1614
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1615
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
1616
|
-
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
1617
|
-
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
1618
|
-
: error.message?.includes('not found') || error.message?.includes('missing')
|
|
1619
|
-
? 'RMA not found - verify RMA reference and query parameters'
|
|
1620
|
-
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
1621
|
-
? 'Check network connectivity and Fluent Commerce API availability'
|
|
1622
|
-
: 'Review error details and check RMA status query parameters',
|
|
1623
|
-
};
|
|
1624
|
-
log.error('[RMA] Failed to query RMA', errorDetails);
|
|
1625
|
-
return {
|
|
1626
|
-
status: 500,
|
|
1627
|
-
body: {
|
|
1628
|
-
success: false,
|
|
1629
|
-
error: error.message,
|
|
1630
|
-
recommendation: errorDetails.recommendation,
|
|
1631
|
-
timestamp: new Date().toISOString(),
|
|
1632
|
-
},
|
|
1633
|
-
};
|
|
1634
|
-
}
|
|
1635
|
-
});
|
|
1636
|
-
```
|
|
1637
|
-
|
|
1638
|
-
---
|
|
1639
|
-
|
|
1640
|
-
## 2. RMA Mapping Configuration: `mappings/shopify-return-to-rma.json`
|
|
1641
|
-
|
|
1642
|
-
```json
|
|
1643
|
-
{
|
|
1644
|
-
"version": "1.0.0",
|
|
1645
|
-
"description": "Shopify return request to Fluent RMA mapping",
|
|
1646
|
-
"direction": "ingest",
|
|
1647
|
-
"sourceFormat": "json",
|
|
1648
|
-
"fields": {
|
|
1649
|
-
"ref": {
|
|
1650
|
-
"source": "order_id",
|
|
1651
|
-
"resolver": "custom.generateRmaRef",
|
|
1652
|
-
"required": true
|
|
1653
|
-
},
|
|
1654
|
-
"orderRef": {
|
|
1655
|
-
"source": "order_id",
|
|
1656
|
-
"required": true
|
|
1657
|
-
},
|
|
1658
|
-
"customerId": {
|
|
1659
|
-
"source": "customer.id",
|
|
1660
|
-
"required": true
|
|
1661
|
-
},
|
|
1662
|
-
"customerEmail": {
|
|
1663
|
-
"source": "customer.email",
|
|
1664
|
-
"resolver": "sdk.lowercase",
|
|
1665
|
-
"required": true
|
|
1666
|
-
},
|
|
1667
|
-
"eligible": {
|
|
1668
|
-
"source": "order.created_at",
|
|
1669
|
-
"resolver": "custom.isReturnEligible"
|
|
1670
|
-
},
|
|
1671
|
-
"returnMethod": {
|
|
1672
|
-
"source": "return_method",
|
|
1673
|
-
"defaultValue": "SHIP_BACK"
|
|
1674
|
-
},
|
|
1675
|
-
"primaryReason": {
|
|
1676
|
-
"source": "return_line_items[0].reason",
|
|
1677
|
-
"resolver": "custom.mapReturnReason"
|
|
1678
|
-
},
|
|
1679
|
-
"notes": {
|
|
1680
|
-
"source": "note",
|
|
1681
|
-
"required": false
|
|
1682
|
-
},
|
|
1683
|
-
"items": {
|
|
1684
|
-
"source": "return_line_items",
|
|
1685
|
-
"isArray": true,
|
|
1686
|
-
"fields": {
|
|
1687
|
-
"lineItemId": {
|
|
1688
|
-
"source": "line_item_id",
|
|
1689
|
-
"resolver": "sdk.toString"
|
|
1690
|
-
},
|
|
1691
|
-
"productRef": {
|
|
1692
|
-
"source": "sku",
|
|
1693
|
-
"required": true
|
|
1694
|
-
},
|
|
1695
|
-
"quantity": {
|
|
1696
|
-
"source": "quantity",
|
|
1697
|
-
"resolver": "sdk.parseInt",
|
|
1698
|
-
"required": true
|
|
1699
|
-
},
|
|
1700
|
-
"returnReason": {
|
|
1701
|
-
"source": "reason",
|
|
1702
|
-
"resolver": "custom.mapReturnReason",
|
|
1703
|
-
"required": true
|
|
1704
|
-
},
|
|
1705
|
-
"customerNotes": {
|
|
1706
|
-
"source": "customer_note",
|
|
1707
|
-
"required": false
|
|
1708
|
-
},
|
|
1709
|
-
"price": {
|
|
1710
|
-
"source": "price",
|
|
1711
|
-
"resolver": "sdk.parseFloat"
|
|
1712
|
-
},
|
|
1713
|
-
"restockingFee": {
|
|
1714
|
-
"resolver": "custom.calculateRestockingFee",
|
|
1715
|
-
"comment": "Resolver-only field - receives full item as sourceData"
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1721
|
-
```
|
|
1722
|
-
|
|
1723
|
-
---
|
|
1724
|
-
|
|
1725
|
-
## Key Patterns Explained
|
|
1726
|
-
|
|
1727
|
-
### Pattern 1: Return Reason Mapping
|
|
1728
|
-
|
|
1729
|
-
Map e-commerce return reasons to Fluent reason codes:
|
|
1730
|
-
|
|
1731
|
-
```typescript
|
|
1732
|
-
'custom.mapReturnReason': (reason: string) => {
|
|
1733
|
-
const reasonMap: Record<string, string> = {
|
|
1734
|
-
'changed_mind': 'CUSTOMER_REMORSE',
|
|
1735
|
-
'defective': 'DEFECTIVE',
|
|
1736
|
-
'wrong_item': 'WRONG_ITEM_SHIPPED',
|
|
1737
|
-
'damaged': 'DAMAGED_IN_TRANSIT',
|
|
1738
|
-
'not_as_described': 'NOT_AS_DESCRIBED',
|
|
1739
|
-
'sizing_issues': 'SIZE_FIT_ISSUE',
|
|
1740
|
-
'late_delivery': 'LATE_DELIVERY',
|
|
1741
|
-
'other': 'OTHER',
|
|
1742
|
-
};
|
|
1743
|
-
return reasonMap[reason?.toLowerCase()] || 'OTHER';
|
|
1744
|
-
}
|
|
1745
|
-
```
|
|
1746
|
-
|
|
1747
|
-
### Pattern 2: Restocking Fee Calculation
|
|
1748
|
-
|
|
1749
|
-
Calculate restocking fees based on return reason using resolver-only field:
|
|
1750
|
-
|
|
1751
|
-
```typescript
|
|
1752
|
-
// Resolver-only field pattern: receives (value, sourceData, helpers)
|
|
1753
|
-
// value = undefined (no source specified)
|
|
1754
|
-
// sourceData = current array item containing all fields
|
|
1755
|
-
'custom.calculateRestockingFee': (value: any, sourceData: any, helpers: any) => {
|
|
1756
|
-
const feeMap: Record<string, number> = {
|
|
1757
|
-
'CUSTOMER_REMORSE': 0.15, // 15% restocking fee
|
|
1758
|
-
'SIZE_FIT_ISSUE': 0.10, // 10% restocking fee
|
|
1759
|
-
'OTHER': 0.10,
|
|
1760
|
-
'DEFECTIVE': 0, // No fee for defective items
|
|
1761
|
-
'WRONG_ITEM_SHIPPED': 0, // No fee for our errors
|
|
1762
|
-
'DAMAGED_IN_TRANSIT': 0,
|
|
1763
|
-
};
|
|
1764
|
-
|
|
1765
|
-
// Extract fields from sourceData (the current array item)
|
|
1766
|
-
const reason = sourceData?.reason || 'OTHER';
|
|
1767
|
-
const price = helpers.parseFloatSafe(sourceData?.price, 0);
|
|
1768
|
-
|
|
1769
|
-
const feePercentage = feeMap[reason] || 0;
|
|
1770
|
-
return price * feePercentage;
|
|
1771
|
-
}
|
|
1772
|
-
```
|
|
1773
|
-
|
|
1774
|
-
**Mapping Configuration:**
|
|
1775
|
-
```json
|
|
1776
|
-
{
|
|
1777
|
-
"restockingFee": {
|
|
1778
|
-
"resolver": "custom.calculateRestockingFee",
|
|
1779
|
-
"comment": "Resolver-only field - no source, extracts from sourceData"
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
```
|
|
1783
|
-
|
|
1784
|
-
### Pattern 3: Return Eligibility Check
|
|
1785
|
-
|
|
1786
|
-
Validate returns are within policy window (e.g., 30 days):
|
|
1787
|
-
|
|
1788
|
-
```typescript
|
|
1789
|
-
'custom.isReturnEligible': (orderDate: string) => {
|
|
1790
|
-
const orderTime = new Date(orderDate).getTime();
|
|
1791
|
-
const now = Date.now();
|
|
1792
|
-
const daysSinceOrder = (now - orderTime) / (1000 * 60 * 60 * 24);
|
|
1793
|
-
return daysSinceOrder <= 30; // 30-day return window
|
|
1794
|
-
}
|
|
1795
|
-
```
|
|
1796
|
-
|
|
1797
|
-
### Pattern 4: Quality Inspection States
|
|
1798
|
-
|
|
1799
|
-
Map inspection condition to disposition:
|
|
1800
|
-
|
|
1801
|
-
```typescript
|
|
1802
|
-
const condition = item.condition?.toUpperCase();
|
|
1803
|
-
let disposition = 'RESTOCK';
|
|
1804
|
-
let restockable = true;
|
|
1805
|
-
|
|
1806
|
-
switch (condition) {
|
|
1807
|
-
case 'NEW':
|
|
1808
|
-
case 'LIKE_NEW':
|
|
1809
|
-
disposition = 'RESTOCK';
|
|
1810
|
-
restockable = true;
|
|
1811
|
-
break;
|
|
1812
|
-
case 'DAMAGED':
|
|
1813
|
-
case 'DEFECTIVE':
|
|
1814
|
-
disposition = 'SCRAP';
|
|
1815
|
-
restockable = false;
|
|
1816
|
-
break;
|
|
1817
|
-
case 'OPENED':
|
|
1818
|
-
case 'USED':
|
|
1819
|
-
disposition = 'OPEN_BOX';
|
|
1820
|
-
restockable = true;
|
|
1821
|
-
break;
|
|
1822
|
-
case 'MISSING_PARTS':
|
|
1823
|
-
disposition = 'REPAIR';
|
|
1824
|
-
restockable = false;
|
|
1825
|
-
break;
|
|
1826
|
-
default:
|
|
1827
|
-
disposition = 'MANUAL_REVIEW';
|
|
1828
|
-
restockable = false;
|
|
1829
|
-
}
|
|
1830
|
-
```
|
|
1831
|
-
|
|
1832
|
-
### Pattern 5: Refund vs Exchange Decision Tree
|
|
1833
|
-
|
|
1834
|
-
Determine next action based on inspection results:
|
|
1835
|
-
|
|
1836
|
-
```typescript
|
|
1837
|
-
// Check if all items passed inspection
|
|
1838
|
-
const refundApproved = inspectionResults.every(r =>
|
|
1839
|
-
r.disposition === 'RESTOCK' || r.disposition === 'OPEN_BOX'
|
|
1840
|
-
);
|
|
1841
|
-
|
|
1842
|
-
// Determine next action
|
|
1843
|
-
const nextAction = payload.returnType === 'exchange'
|
|
1844
|
-
? 'CREATE_EXCHANGE_ORDER'
|
|
1845
|
-
: 'PROCESS_REFUND';
|
|
1846
|
-
|
|
1847
|
-
log.info('[RMA] Inspection complete', {
|
|
1848
|
-
rmaRef: rma.ref,
|
|
1849
|
-
refundApproved,
|
|
1850
|
-
nextAction,
|
|
1851
|
-
});
|
|
1852
|
-
```
|
|
1853
|
-
|
|
1854
|
-
### Pattern 6: Partial Returns
|
|
1855
|
-
|
|
1856
|
-
Handle returns of some items from an order:
|
|
1857
|
-
|
|
1858
|
-
```json
|
|
1859
|
-
{
|
|
1860
|
-
"items": {
|
|
1861
|
-
"source": "return_line_items",
|
|
1862
|
-
"isArray": true,
|
|
1863
|
-
"fields": {
|
|
1864
|
-
"lineItemId": { "source": "line_item_id" },
|
|
1865
|
-
"productRef": { "source": "sku" },
|
|
1866
|
-
"quantity": { "source": "quantity", "resolver": "sdk.parseInt" }
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
```
|
|
1871
|
-
|
|
1872
|
-
**Key**: Use `isArray: true` to map each line item individually.
|
|
1873
|
-
|
|
1874
|
-
### Pattern 7: VersoriKV RMA Lifecycle Tracking
|
|
1875
|
-
|
|
1876
|
-
Track RMA state across all stages:
|
|
1877
|
-
|
|
1878
|
-
```typescript
|
|
1879
|
-
// Store initial RMA state
|
|
1880
|
-
const rmaState = {
|
|
1881
|
-
rmaId: rma.id,
|
|
1882
|
-
rmaRef: rma.ref,
|
|
1883
|
-
orderRef: rma.orderRef,
|
|
1884
|
-
status: rma.status,
|
|
1885
|
-
stage: 'RMA_CREATED',
|
|
1886
|
-
createdOn: rma.createdOn,
|
|
1887
|
-
};
|
|
1888
|
-
await kvAdapter.set(`rma:${rma.ref}`, rmaState);
|
|
1889
|
-
|
|
1890
|
-
// Update state at each stage
|
|
1891
|
-
const existingState = await kvAdapter.get(`rma:${rma.ref}`);
|
|
1892
|
-
if (existingState) {
|
|
1893
|
-
const updatedState = {
|
|
1894
|
-
...existingState,
|
|
1895
|
-
status: 'INSPECTED',
|
|
1896
|
-
stage: 'INSPECTION_COMPLETE',
|
|
1897
|
-
inspectionDate: new Date().toISOString(),
|
|
1898
|
-
};
|
|
1899
|
-
await kvAdapter.set(`rma:${rma.ref}`, updatedState);
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
// Query state
|
|
1903
|
-
const rmaStateJson = await kvAdapter.get(`rma:${rma.ref}`);
|
|
1904
|
-
const rmaState = rmaStateJson ? JSON.parse(rmaStateJson) : null;
|
|
1905
|
-
```
|
|
1906
|
-
|
|
1907
|
-
---
|
|
1908
|
-
|
|
1909
|
-
## Testing
|
|
1910
|
-
|
|
1911
|
-
### 1. Test RMA Creation (Shopify Format)
|
|
1912
|
-
|
|
1913
|
-
```bash
|
|
1914
|
-
curl -X POST https://your-workspace.versori.run/create-rma \
|
|
1915
|
-
-H "Content-Type: application/json" \
|
|
1916
|
-
-d '{
|
|
1917
|
-
"order_id": "ORD-12345",
|
|
1918
|
-
"customer": {
|
|
1919
|
-
"id": "CUST-789",
|
|
1920
|
-
"email": "customer@example.com"
|
|
1921
|
-
},
|
|
1922
|
-
"order": {
|
|
1923
|
-
"created_at": "2025-01-15T10:00:00Z"
|
|
1924
|
-
},
|
|
1925
|
-
"return_line_items": [
|
|
1926
|
-
{
|
|
1927
|
-
"line_item_id": "LI-001",
|
|
1928
|
-
"sku": "PROD-ABC123",
|
|
1929
|
-
"quantity": 1,
|
|
1930
|
-
"reason": "wrong_item",
|
|
1931
|
-
"customer_note": "Received wrong size",
|
|
1932
|
-
"price": 99.99
|
|
1933
|
-
}
|
|
1934
|
-
],
|
|
1935
|
-
"return_method": "SHIP_BACK",
|
|
1936
|
-
"note": "Customer wants exchange for correct size"
|
|
1937
|
-
}'
|
|
1938
|
-
|
|
1939
|
-
# Expected Response:
|
|
1940
|
-
{
|
|
1941
|
-
"status": 200,
|
|
1942
|
-
"body": {
|
|
1943
|
-
"success": true,
|
|
1944
|
-
"rmaId": "RMA-123",
|
|
1945
|
-
"rmaRef": "RMA-ORD-12345-456789",
|
|
1946
|
-
"status": "PENDING_APPROVAL",
|
|
1947
|
-
"shippingLabel": {
|
|
1948
|
-
"trackingNumber": "TRK-1234567890",
|
|
1949
|
-
"labelUrl": "https://labels.example.com/RMA-ORD-12345-456789.pdf",
|
|
1950
|
-
"carrier": "USPS",
|
|
1951
|
-
"service": "Priority Mail"
|
|
1952
|
-
},
|
|
1953
|
-
"message": "RMA created successfully. Return label sent to customer.",
|
|
1954
|
-
"timestamp": "2025-10-30T12:00:00.000Z"
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
```
|
|
1958
|
-
|
|
1959
|
-
### 2. Test Shipment Tracking Update
|
|
1960
|
-
|
|
1961
|
-
```bash
|
|
1962
|
-
curl -X POST https://your-workspace.versori.run/track-return-shipment \
|
|
1963
|
-
-H "Content-Type: application/json" \
|
|
1964
|
-
-d '{
|
|
1965
|
-
"tracking_number": "TRK-1234567890",
|
|
1966
|
-
"status": "DELIVERED",
|
|
1967
|
-
"carrier": "USPS",
|
|
1968
|
-
"timestamp": "2025-01-20T14:30:00Z"
|
|
1969
|
-
}'
|
|
1970
|
-
```
|
|
1971
|
-
|
|
1972
|
-
### 3. Test Quality Inspection
|
|
1973
|
-
|
|
1974
|
-
```bash
|
|
1975
|
-
curl -X POST https://your-workspace.versori.run/quality-inspection \
|
|
1976
|
-
-H "Content-Type: application/json" \
|
|
1977
|
-
-d '{
|
|
1978
|
-
"rmaRef": "RMA-ORD-12345-456789",
|
|
1979
|
-
"inspector": "John Doe",
|
|
1980
|
-
"locationRef": "RETURNS_WAREHOUSE",
|
|
1981
|
-
"returnType": "refund",
|
|
1982
|
-
"items": [
|
|
1983
|
-
{
|
|
1984
|
-
"itemId": "RMA-ITEM-001",
|
|
1985
|
-
"productRef": "PROD-ABC123",
|
|
1986
|
-
"condition": "NEW",
|
|
1987
|
-
"notes": "Item in original packaging, tags attached",
|
|
1988
|
-
"photoUrls": ["https://photos.example.com/item1.jpg"]
|
|
1989
|
-
}
|
|
1990
|
-
]
|
|
1991
|
-
}'
|
|
1992
|
-
```
|
|
1993
|
-
|
|
1994
|
-
### 4. Test Refund Processing
|
|
1995
|
-
|
|
1996
|
-
```bash
|
|
1997
|
-
curl -X POST https://your-workspace.versori.run/process-refund-exchange \
|
|
1998
|
-
-H "Content-Type: application/json" \
|
|
1999
|
-
-d '{
|
|
2000
|
-
"rmaRef": "RMA-ORD-12345-456789",
|
|
2001
|
-
"actionType": "refund"
|
|
2002
|
-
}'
|
|
2003
|
-
```
|
|
2004
|
-
|
|
2005
|
-
### 5. Test Exchange Order Creation
|
|
2006
|
-
|
|
2007
|
-
```bash
|
|
2008
|
-
curl -X POST https://your-workspace.versori.run/process-refund-exchange \
|
|
2009
|
-
-H "Content-Type: application/json" \
|
|
2010
|
-
-d '{
|
|
2011
|
-
"rmaRef": "RMA-ORD-12345-456789",
|
|
2012
|
-
"actionType": "exchange",
|
|
2013
|
-
"exchangeItems": [
|
|
2014
|
-
{
|
|
2015
|
-
"productRef": "PROD-ABC124",
|
|
2016
|
-
"quantity": 1
|
|
2017
|
-
}
|
|
2018
|
-
]
|
|
2019
|
-
}'
|
|
2020
|
-
```
|
|
2021
|
-
|
|
2022
|
-
### 6. Query RMA Status
|
|
2023
|
-
|
|
2024
|
-
```bash
|
|
2025
|
-
curl https://your-workspace.versori.run/get-rma-status?rmaRef=RMA-ORD-12345-456789
|
|
2026
|
-
```
|
|
2027
|
-
|
|
2028
|
-
---
|
|
2029
|
-
|
|
2030
|
-
## Common Issues and Solutions
|
|
2031
|
-
|
|
2032
|
-
### Issue 1: Return Request Rejected (Ineligible)
|
|
2033
|
-
|
|
2034
|
-
**Symptoms:**
|
|
2035
|
-
- RMA creation fails with "INELIGIBLE" error
|
|
2036
|
-
- Returns outside policy window
|
|
2037
|
-
|
|
2038
|
-
**Root Cause:**
|
|
2039
|
-
- Order is older than return window (30 days)
|
|
2040
|
-
|
|
2041
|
-
**Solution:**
|
|
2042
|
-
|
|
2043
|
-
```typescript
|
|
2044
|
-
// Adjust return window in custom resolver
|
|
2045
|
-
'custom.isReturnEligible': (orderDate: string) => {
|
|
2046
|
-
const orderTime = new Date(orderDate).getTime();
|
|
2047
|
-
const now = Date.now();
|
|
2048
|
-
const daysSinceOrder = (now - orderTime) / (1000 * 60 * 60 * 24);
|
|
2049
|
-
|
|
2050
|
-
// Change to 60 days or make configurable via activation variable
|
|
2051
|
-
return daysSinceOrder <= 60;
|
|
2052
|
-
}
|
|
2053
|
-
```
|
|
2054
|
-
|
|
2055
|
-
### Issue 2: Restocking Fee Not Calculated
|
|
2056
|
-
|
|
2057
|
-
**Symptoms:**
|
|
2058
|
-
- Restocking fee is 0 for all items
|
|
2059
|
-
- Refund amount is incorrect
|
|
2060
|
-
|
|
2061
|
-
**Root Cause:**
|
|
2062
|
-
- Custom resolver not receiving price parameter
|
|
2063
|
-
- Mapping configuration missing price field
|
|
2064
|
-
|
|
2065
|
-
**Solution:**
|
|
2066
|
-
|
|
2067
|
-
```json
|
|
2068
|
-
{
|
|
2069
|
-
"items": {
|
|
2070
|
-
"source": "return_line_items",
|
|
2071
|
-
"isArray": true,
|
|
2072
|
-
"fields": {
|
|
2073
|
-
"price": {
|
|
2074
|
-
"source": "price",
|
|
2075
|
-
"resolver": "sdk.parseFloat"
|
|
2076
|
-
},
|
|
2077
|
-
"restockingFee": {
|
|
2078
|
-
"resolver": "custom.calculateRestockingFee",
|
|
2079
|
-
"comment": "Resolver-only field - receives full item as sourceData parameter"
|
|
2080
|
-
}
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
```
|
|
2085
|
-
|
|
2086
|
-
### Issue 3: Inventory Not Restocked
|
|
2087
|
-
|
|
2088
|
-
**Symptoms:**
|
|
2089
|
-
- Quality inspection completes but inventory doesn't increase
|
|
2090
|
-
- Restockable items not showing in inventory
|
|
2091
|
-
|
|
2092
|
-
**Root Cause:**
|
|
2093
|
-
- Location reference incorrect
|
|
2094
|
-
- Inventory adjustment mutation failed
|
|
2095
|
-
|
|
2096
|
-
**Solution:**
|
|
2097
|
-
|
|
2098
|
-
```typescript
|
|
2099
|
-
// Verify location exists in Fluent
|
|
2100
|
-
const locationResult = await fluentClient.graphql({
|
|
2101
|
-
query: `query { location(ref: "${payload.locationRef}") { id ref } }`
|
|
2102
|
-
});
|
|
2103
|
-
|
|
2104
|
-
if (!locationResult.data?.location) {
|
|
2105
|
-
log.warn('Location not found, using default', {
|
|
2106
|
-
requestedLocation: payload.locationRef,
|
|
2107
|
-
defaultLocation: 'RETURNS_WAREHOUSE',
|
|
2108
|
-
});
|
|
2109
|
-
}
|
|
2110
|
-
|
|
2111
|
-
// Use validated location
|
|
2112
|
-
const locationRef = locationResult.data?.location?.ref || 'RETURNS_WAREHOUSE';
|
|
2113
|
-
```
|
|
2114
|
-
|
|
2115
|
-
### Issue 4: Duplicate RMA Creation
|
|
2116
|
-
|
|
2117
|
-
**Symptoms:**
|
|
2118
|
-
- Multiple RMAs created for same return request
|
|
2119
|
-
- Duplicate tracking numbers
|
|
2120
|
-
|
|
2121
|
-
**Root Cause:**
|
|
2122
|
-
- No duplicate detection
|
|
2123
|
-
- Webhook retries creating duplicates
|
|
2124
|
-
|
|
2125
|
-
**Solution:**
|
|
2126
|
-
|
|
2127
|
-
```typescript
|
|
2128
|
-
// Check if RMA already exists before creating
|
|
2129
|
-
const existingRma = await kvAdapter.get(`rma-creation:${payload.order_id}`);
|
|
2130
|
-
|
|
2131
|
-
if (existingRma) {
|
|
2132
|
-
log.info('[RMA] RMA already exists for this order', {
|
|
2133
|
-
orderId: payload.order_id,
|
|
2134
|
-
existingRmaRef: existingRma.rmaRef,
|
|
2135
|
-
});
|
|
2136
|
-
|
|
2137
|
-
return {
|
|
2138
|
-
success: true,
|
|
2139
|
-
rmaRef: existingRma.rmaRef,
|
|
2140
|
-
duplicate: true,
|
|
2141
|
-
message: 'RMA already exists for this order',
|
|
2142
|
-
};
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
// Create RMA...
|
|
2146
|
-
|
|
2147
|
-
// Store creation state
|
|
2148
|
-
const creationState = {
|
|
2149
|
-
rmaRef: rma.ref,
|
|
2150
|
-
createdAt: new Date().toISOString(),
|
|
2151
|
-
};
|
|
2152
|
-
await kvAdapter.set(`rma-creation:${payload.order_id}`, JSON.stringify(creationState));
|
|
2153
|
-
```
|
|
2154
|
-
|
|
2155
|
-
### Issue 5: Refund Amount Incorrect
|
|
2156
|
-
|
|
2157
|
-
**Symptoms:**
|
|
2158
|
-
- Refund amount doesn't match expected value
|
|
2159
|
-
- Restocking fees not deducted
|
|
2160
|
-
|
|
2161
|
-
**Root Cause:**
|
|
2162
|
-
- Calculation logic incorrect
|
|
2163
|
-
- Items missing price data
|
|
2164
|
-
|
|
2165
|
-
**Solution:**
|
|
2166
|
-
|
|
2167
|
-
```typescript
|
|
2168
|
-
// Detailed refund calculation with logging
|
|
2169
|
-
const refundDetails = rma.items.map((item: any) => {
|
|
2170
|
-
const itemTotal = item.price * item.quantity;
|
|
2171
|
-
const restockingFee = item.restockingFee || 0;
|
|
2172
|
-
const itemRefund = itemTotal - restockingFee;
|
|
2173
|
-
|
|
2174
|
-
log.debug('[RMA] Item refund calculated', {
|
|
2175
|
-
productRef: item.productRef,
|
|
2176
|
-
quantity: item.quantity,
|
|
2177
|
-
price: item.price,
|
|
2178
|
-
itemTotal,
|
|
2179
|
-
restockingFee,
|
|
2180
|
-
itemRefund,
|
|
2181
|
-
});
|
|
2182
|
-
|
|
2183
|
-
return {
|
|
2184
|
-
productRef: item.productRef,
|
|
2185
|
-
itemTotal,
|
|
2186
|
-
restockingFee,
|
|
2187
|
-
itemRefund,
|
|
2188
|
-
};
|
|
2189
|
-
});
|
|
2190
|
-
|
|
2191
|
-
const totalRefund = refundDetails.reduce((sum, detail) => sum + detail.itemRefund, 0);
|
|
2192
|
-
|
|
2193
|
-
log.info('[RMA] Total refund calculated', {
|
|
2194
|
-
totalRefund,
|
|
2195
|
-
itemCount: refundDetails.length,
|
|
2196
|
-
details: refundDetails,
|
|
2197
|
-
});
|
|
2198
|
-
```
|
|
2199
|
-
|
|
2200
|
-
---
|
|
2201
|
-
|
|
2202
|
-
## Related Guides
|
|
2203
|
-
|
|
2204
|
-
- **[Versori Webhook: XML Order Processing](./xml-order-ingestion.md)** - Similar webhook patterns
|
|
2205
|
-
- **[Versori Scheduled: CSV Inventory Sync](../workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md)** - State management patterns
|
|
2206
|
-
- **[KV State Management](../../../04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md)** - VersoriKV usage
|
|
2207
|
-
- **[Universal Mapping Guide](../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)** - Field mapping patterns
|
|
2208
|
-
- **[Custom Resolvers](../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)** - Advanced resolver patterns
|
|
2209
|
-
- **[Error Handling](../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)** - Error handling and retry logic
|
|
2210
|
-
|
|
2211
|
-
---
|
|
2212
|
-
|
|
2213
|
-
## Summary
|
|
2214
|
-
|
|
2215
|
-
**Volume:** Medium (5-10% of order volume, 50-500 returns/day)
|
|
2216
|
-
|
|
2217
|
-
**Latency:** Real-time (< 2 seconds per webhook)
|
|
2218
|
-
|
|
2219
|
-
**Complexity:** Medium (multiple stages, state tracking, conditional logic)
|
|
2220
|
-
|
|
2221
|
-
**Key Features:**
|
|
2222
|
-
- End-to-end return processing (initiation → refund/exchange)
|
|
2223
|
-
- Quality inspection workflow with condition tracking
|
|
2224
|
-
- Restocking fee calculation based on return reason
|
|
2225
|
-
- Inventory adjustments for restockable items
|
|
2226
|
-
- Exchange order creation
|
|
2227
|
-
- VersoriKV state management across RMA lifecycle
|
|
2228
|
-
- Email notifications at each stage
|
|
2229
|
-
- Support for partial returns
|
|
2230
|
-
- 30-day return policy enforcement
|
|
2231
|
-
|
|
2232
|
-
**Real-World Considerations:**
|
|
2233
|
-
- Integrate with actual payment gateway (Stripe, Adyen) for refunds
|
|
2234
|
-
- Integrate with shipping provider (ShipStation, EasyPost) for return labels
|
|
2235
|
-
- Add email/SMS notifications via SendGrid or Twilio
|
|
2236
|
-
- Implement return fraud detection
|
|
2237
|
-
- Add approval workflow for high-value returns
|
|
2238
|
-
- Track return trends and analytics
|
|
2239
|
-
- Support international returns with customs
|
|
2240
|
-
- Handle restocking for serialized/batch-tracked items
|
|
1
|
+
---
|
|
2
|
+
template_id: tpl-webhook-rma-returns-comprehensive
|
|
3
|
+
canonical_filename: template-webhook-rma-returns-comprehensive.md
|
|
4
|
+
version: 2.0.0
|
|
5
|
+
sdk_version: ^0.1.39
|
|
6
|
+
runtime: versori
|
|
7
|
+
direction: ingestion
|
|
8
|
+
source: webhook-json-xml-return
|
|
9
|
+
destination: fluent-graphql
|
|
10
|
+
entity: rma
|
|
11
|
+
format: json-xml
|
|
12
|
+
logging: versori
|
|
13
|
+
status: stable
|
|
14
|
+
features:
|
|
15
|
+
- webhook-signature-validation
|
|
16
|
+
- batched-events
|
|
17
|
+
- attribute-transformation
|
|
18
|
+
- memory-management
|
|
19
|
+
- enhanced-logging
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Template: Webhook - RMA Returns Processing
|
|
23
|
+
|
|
24
|
+
**Template Version:** 2.0.0
|
|
25
|
+
**SDK Version:** @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
26
|
+
**Last Updated:** 2025-01-24
|
|
27
|
+
**Deployment Target:** Versori Platform
|
|
28
|
+
|
|
29
|
+
**🆕 Version 2.0.0 Enhancements:**
|
|
30
|
+
- ✅ **Webhook Signature Validation** - Secure webhook verification with HMAC-SHA256
|
|
31
|
+
- ✅ **Batched Events** - Process events in optimized batches to reduce API calls
|
|
32
|
+
- ✅ **Attribute Transformation** - Handle nested arrays and complex data structures
|
|
33
|
+
- ✅ **Memory Management** - Clear large arrays after processing batches
|
|
34
|
+
- ✅ **Enhanced Logging** - Track batch processing and event submission with emoji indicators
|
|
35
|
+
|
|
36
|
+
**FC Connect SDK Use Case Guide**
|
|
37
|
+
|
|
38
|
+
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
39
|
+
> **Version**: Use latest - `npm install @fluentcommerce/fc-connect-sdk@latest`
|
|
40
|
+
|
|
41
|
+
**Context**: Handle customer returns end-to-end from initiation through refund/exchange
|
|
42
|
+
|
|
43
|
+
**Complexity**: Medium-High
|
|
44
|
+
|
|
45
|
+
**Runtime**: Versori Platform
|
|
46
|
+
|
|
47
|
+
**Estimated Lines**: ~950 lines (modular structure)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## STEP 1: Understand This Template
|
|
52
|
+
|
|
53
|
+
**What This Template Does:**
|
|
54
|
+
|
|
55
|
+
- Versori HTTP webhook for return requests
|
|
56
|
+
- Parse return payload from e-commerce (Shopify/SFCC)
|
|
57
|
+
- Create RMA in Fluent Commerce (custom mutation)
|
|
58
|
+
- Generate return shipping label (ShipStation/EasyPost)
|
|
59
|
+
- Track return shipment status
|
|
60
|
+
- Quality inspection workflow (receive → inspect → approve/reject)
|
|
61
|
+
- Process refund or exchange
|
|
62
|
+
- Inventory adjustment (return to stock vs damaged/scrap)
|
|
63
|
+
- Email notifications at each stage
|
|
64
|
+
- VersoriKV state tracking for RMA lifecycle
|
|
65
|
+
- **Sync + Fire-and-Forget Pattern**: Fast webhook response, background processing
|
|
66
|
+
|
|
67
|
+
**Key SDK Components:**
|
|
68
|
+
|
|
69
|
+
- `createClient()` - Universal client factory (auto-detects Versori context)
|
|
70
|
+
- `UniversalMapper` - Transform return payload with custom resolvers
|
|
71
|
+
- `GraphQLMutationMapper` - Map return data to GraphQL mutations
|
|
72
|
+
- `VersoriKVAdapter` - RMA state tracking (KV storage)
|
|
73
|
+
- Native Versori `log` - Use `log` from context
|
|
74
|
+
|
|
75
|
+
**Entity Type:**
|
|
76
|
+
|
|
77
|
+
- **RMA** - Fluent entity for return merchandise authorization
|
|
78
|
+
- **GraphQL Mutations** - Create RMA, update RMA status, process refunds
|
|
79
|
+
|
|
80
|
+
**Critical Patterns:**
|
|
81
|
+
|
|
82
|
+
- **Sync + Fire-and-Forget**: Webhook validates quickly, returns immediately, processes RMA in background
|
|
83
|
+
- **External JSON Config**: Return mapping configuration in separate JSON file (`config/return-mapping.json`)
|
|
84
|
+
- **Modular Architecture**: Separate services, workflows, config, types folders
|
|
85
|
+
- **Background Processing**: Long-running operations (RMA creation, label generation, refunds) happen asynchronously
|
|
86
|
+
- **State Management**: KV storage for RMA lifecycle tracking across multiple webhooks
|
|
87
|
+
- **Multi-Webhook Flow**: Multiple webhooks work together (create → track → inspect → refund)
|
|
88
|
+
|
|
89
|
+
**When to Use This Template:**
|
|
90
|
+
|
|
91
|
+
- ✅ End-to-end returns processing
|
|
92
|
+
- ✅ Multiple return sources (Shopify, SFCC, custom)
|
|
93
|
+
- ✅ Need fast webhook response (don't wait for RMA creation)
|
|
94
|
+
- ✅ Multi-stage returns workflow (create → track → inspect → refund)
|
|
95
|
+
- ✅ State tracking across multiple webhooks
|
|
96
|
+
|
|
97
|
+
**When NOT to Use:**
|
|
98
|
+
|
|
99
|
+
- ❌ Simple return processing (use single webhook)
|
|
100
|
+
- ❌ Bulk return processing (use Batch API or scheduled workflows)
|
|
101
|
+
- ❌ Need synchronous RMA creation (wait for result before responding)
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## STEP 2: Implementation Prompt for Claude Code
|
|
106
|
+
|
|
107
|
+
**Copy this prompt and send to Claude Code to generate the complete implementation:**
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
Create Versori webhook workflows for comprehensive RMA returns processing to Fluent Commerce.
|
|
111
|
+
|
|
112
|
+
REQUIREMENTS:
|
|
113
|
+
1. Runtime: Versori Platform (HTTP webhooks)
|
|
114
|
+
2. Source: Return data via HTTP POST webhooks (JSON or XML, Shopify/SFCC format)
|
|
115
|
+
3. Destination: Fluent Commerce GraphQL API (RMA mutations)
|
|
116
|
+
4. Format: JSON or XML (Shopify/SFCC compatible)
|
|
117
|
+
5. Entity: RMA (GraphQL mutations for lifecycle management)
|
|
118
|
+
|
|
119
|
+
KEY FEATURES:
|
|
120
|
+
- Sync + fire-and-forget pattern (fast webhook response, background processing)
|
|
121
|
+
- External JSON mapping configuration (config/return-mapping.json)
|
|
122
|
+
- Modular architecture (workflows/, services/, config/, types/)
|
|
123
|
+
- Multiple webhooks for RMA lifecycle (create → track → inspect → refund)
|
|
124
|
+
- UniversalMapper for return payload transformation
|
|
125
|
+
- KV storage for cross-webhook state tracking
|
|
126
|
+
- Comprehensive error handling
|
|
127
|
+
|
|
128
|
+
CRITICAL REQUIREMENTS:
|
|
129
|
+
1. Webhook Mode: response: { mode: 'sync' } (fast response)
|
|
130
|
+
2. Background Processing: Fire-and-forget pattern (no await on long operations)
|
|
131
|
+
3. Mapping Config: External JSON file (config/return-mapping.json)
|
|
132
|
+
4. Modular Structure: Separate services/, config/, types/ folders
|
|
133
|
+
5. Native Logging: Use log from context (no LoggingService)
|
|
134
|
+
6. State Management: VersoriKVAdapter for RMA lifecycle tracking
|
|
135
|
+
|
|
136
|
+
SDK METHODS TO USE:
|
|
137
|
+
- createClient({ ...ctx, log }) - Pass full Versori context
|
|
138
|
+
- new UniversalMapper(mappingConfig) - Transform return payload
|
|
139
|
+
- new GraphQLMutationMapper(mappingConfig, log, { fluentClient: client }) - Map to GraphQL
|
|
140
|
+
- new VersoriKVAdapter(openKv(':project:')) - RMA state storage
|
|
141
|
+
- client.graphql({ query, variables }) - Execute GraphQL mutations
|
|
142
|
+
|
|
143
|
+
FORBIDDEN PATTERNS:
|
|
144
|
+
- ❌ Inline mapping config (use external JSON)
|
|
145
|
+
- ❌ await on background processing (use fire-and-forget)
|
|
146
|
+
- ❌ LoggingService (use native log from context)
|
|
147
|
+
- ❌ All code in one file (use modular structure)
|
|
148
|
+
- ❌ async mode webhook (use sync + fire-and-forget)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## STEP 3: Detailed Flow Documentation
|
|
154
|
+
|
|
155
|
+
### Complete Processing Flow
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
159
|
+
│ 1. WEBHOOK RECEIVED (Create RMA) │
|
|
160
|
+
│ POST https://{workspace}.versori.run/create-rma │
|
|
161
|
+
│ Content-Type: application/json │
|
|
162
|
+
│ Body: { order_id: "...", return_line_items: [...] } │
|
|
163
|
+
└────────────────────┬────────────────────────────────────────┘
|
|
164
|
+
│
|
|
165
|
+
▼
|
|
166
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
167
|
+
│ 2. QUICK VALIDATION (Synchronous, ~10-50ms) │
|
|
168
|
+
│ - Check fluent_commerce connection exists │
|
|
169
|
+
│ - Validate return payload present │
|
|
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. Transform Return Payload │ │
|
|
182
|
+
│ │ - UniversalMapper with custom resolvers │ │
|
|
183
|
+
│ │ - Map return reasons, calculate fees │ │
|
|
184
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
185
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
186
|
+
│ │ 3c. Create RMA in Fluent │ │
|
|
187
|
+
│ │ - GraphQL mutation to create RMA │ │
|
|
188
|
+
│ │ - Store RMA state in KV │ │
|
|
189
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
190
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
191
|
+
│ │ 3d. Generate Return Shipping Label │ │
|
|
192
|
+
│ │ - Call shipping provider API │ │
|
|
193
|
+
│ │ - Update RMA with label URL │ │
|
|
194
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
195
|
+
└─────────────────────────────────────────────────────────────┘
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Response Timing
|
|
199
|
+
|
|
200
|
+
| Stage | Timing | Blocking |
|
|
201
|
+
|-------|--------|----------|
|
|
202
|
+
| **Webhook Validation** | ~10-50ms | ✅ Yes (blocks response) |
|
|
203
|
+
| **Background Processing** | ~2000-5000ms | ❌ No (fire-and-forget) |
|
|
204
|
+
| **Total Response Time** | ~10-50ms | ✅ Fast response |
|
|
205
|
+
|
|
206
|
+
**Key Benefit**: Webhook caller receives immediate acknowledgment (~50ms) while RMA creation happens in background (~2-5s).
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## STEP 4: Production Modular Structure
|
|
211
|
+
|
|
212
|
+
> **✅ This section shows the COMPLETE production-ready modular structure.**
|
|
213
|
+
> All files are shown with proper imports/exports and folder organization.
|
|
214
|
+
|
|
215
|
+
### Complete Project Structure
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
rma-returns-processing/
|
|
219
|
+
├── package.json # Dependencies and Versori config
|
|
220
|
+
├── index.ts # Entry point - exports all workflows
|
|
221
|
+
└── src/
|
|
222
|
+
├── workflows/
|
|
223
|
+
│ └── webhook/
|
|
224
|
+
│ ├── create-rma.ts # Webhook: Create RMA from return
|
|
225
|
+
│ ├── track-shipment.ts # Webhook: Track return shipment
|
|
226
|
+
│ ├── quality-inspection.ts # Webhook: Quality inspection
|
|
227
|
+
│ └── process-refund-exchange.ts # Webhook: Process refund/exchange
|
|
228
|
+
│
|
|
229
|
+
├── services/
|
|
230
|
+
│ └── return-processing.service.ts # Shared orchestration logic (reusable)
|
|
231
|
+
│
|
|
232
|
+
├── resolvers/
|
|
233
|
+
│ └── return-resolvers.ts # Custom resolvers for transformations
|
|
234
|
+
│
|
|
235
|
+
├── config/
|
|
236
|
+
│ ├── return-mapping.json # Mapping configuration (external JSON)
|
|
237
|
+
│ └── inspection-mapping.json # Inspection mapping (external JSON)
|
|
238
|
+
│
|
|
239
|
+
└── types/
|
|
240
|
+
└── rma.types.ts # TypeScript interfaces
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Why This Structure?**
|
|
244
|
+
|
|
245
|
+
- ✅ **Clear separation**: Multiple webhook handlers vs business logic
|
|
246
|
+
- ✅ **Reusable services**: Return processing logic can be reused
|
|
247
|
+
- ✅ **External config**: Mapping changes don't require code changes
|
|
248
|
+
- ✅ **Custom resolvers**: Separate file for complex transformations
|
|
249
|
+
- ✅ **Type safety**: TypeScript interfaces for better IDE support
|
|
250
|
+
- ✅ **Scalable**: Easy to add new return sources or processing steps
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## SDK Methods Used
|
|
255
|
+
|
|
256
|
+
- `webhook(name, handler)` - HTTP webhook endpoint (from `@versori/run`)
|
|
257
|
+
- `fn(name, handler)` - Function handler (from `@versori/run`)
|
|
258
|
+
- `createClient(ctx)` - Auto-detects Versori context, creates FluentClient
|
|
259
|
+
- `UniversalMapper(config, { customResolvers })` - Transform return payload with custom resolvers
|
|
260
|
+
- `client.graphql({ query, variables })` - Execute GraphQL queries/mutations
|
|
261
|
+
- `VersoriKVAdapter(ctx.openKv(':project:'))` - Versori KV storage adapter (:project: scope for cross-webhook state)
|
|
262
|
+
- `kvAdapter.get(key)` - Retrieve state from KV (returns JSON string)
|
|
263
|
+
- `kvAdapter.set(key, value)` - Store state in KV (stores JSON string)
|
|
264
|
+
- Custom resolvers for return reason mapping, restocking fees, eligibility checks
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Versori Workflows Structure
|
|
269
|
+
|
|
270
|
+
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
271
|
+
|
|
272
|
+
**Trigger Types:**
|
|
273
|
+
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
274
|
+
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
275
|
+
- **`http()`** → External API calls (chained from webhook/schedule)
|
|
276
|
+
- **`fn()`** → Internal processing (chained from webhook/schedule)
|
|
277
|
+
|
|
278
|
+
### Recommended Project Structure
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
rma-returns-processing/
|
|
282
|
+
├── index.ts # Entry point - exports all workflows
|
|
283
|
+
└── src/
|
|
284
|
+
├── workflows/
|
|
285
|
+
│ └── webhook/
|
|
286
|
+
│ └── return-processing.ts # Webhook: Process return requests
|
|
287
|
+
│
|
|
288
|
+
├── services/
|
|
289
|
+
│ └── return-processing.service.ts # Shared orchestration logic (reusable)
|
|
290
|
+
│
|
|
291
|
+
└── config/
|
|
292
|
+
└── return-mapping.json # Mapping configuration
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**Benefits:**
|
|
296
|
+
- ✅ Clear trigger separation (`webhook/` vs `scheduled/`)
|
|
297
|
+
- ✅ Descriptive file names (easy to browse and understand)
|
|
298
|
+
- ✅ Scalable (add new workflows without cluttering)
|
|
299
|
+
- ✅ Reusable code in `services/` (DRY principle)
|
|
300
|
+
- ✅ Easy to modify individual workflows without affecting others
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Complete Working Code
|
|
305
|
+
|
|
306
|
+
### 1. Main Workflow File: `index.ts`
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
/**
|
|
310
|
+
* RMA Returns Processing Workflow
|
|
311
|
+
*
|
|
312
|
+
* Complete returns management from customer initiation through refund/exchange.
|
|
313
|
+
* Supports Shopify and SFCC webhook payloads.
|
|
314
|
+
*
|
|
315
|
+
* Flow:
|
|
316
|
+
* 1. Receive return request webhook
|
|
317
|
+
* 2. Create RMA in Fluent
|
|
318
|
+
* 3. Generate return shipping label
|
|
319
|
+
* 4. Track shipment
|
|
320
|
+
* 5. Quality inspection
|
|
321
|
+
* 6. Process refund/exchange
|
|
322
|
+
* 7. Adjust inventory
|
|
323
|
+
*/
|
|
324
|
+
|
|
325
|
+
import { webhook, http, fn } from '@versori/run';
|
|
326
|
+
import {
|
|
327
|
+
createClient,
|
|
328
|
+
GraphQLMutationMapper,
|
|
329
|
+
UniversalMapper,
|
|
330
|
+
VersoriKVAdapter,
|
|
331
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
332
|
+
|
|
333
|
+
// Import mapping configurations
|
|
334
|
+
import rmaCreationMapping from './mappings/shopify-return-to-rma.json' with { type: 'json' };
|
|
335
|
+
import inspectionMapping from './mappings/inspection-update.json' with { type: 'json' };
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* WEBHOOK 1: Create RMA from Return Request
|
|
339
|
+
*
|
|
340
|
+
* Receives return request from e-commerce platform, creates RMA in Fluent,
|
|
341
|
+
* and generates return shipping label.
|
|
342
|
+
*/
|
|
343
|
+
/**
|
|
344
|
+
* Background processing function for RMA creation
|
|
345
|
+
* Handles all long-running operations (transformation, RMA creation, label generation)
|
|
346
|
+
*/
|
|
347
|
+
async function processRmaCreation(ctx: any, startTime: number): Promise<void> {
|
|
348
|
+
const { log, activation } = ctx;
|
|
349
|
+
const payload = activation?.body || ctx.data;
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
log.info('🔄 [BACKGROUND] Starting background RMA creation processing', {
|
|
353
|
+
orderId: payload?.order_id || payload?.orderId,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// =================================================================
|
|
357
|
+
// STEP 1: PARSE AND VALIDATE RETURN REQUEST
|
|
358
|
+
// =================================================================
|
|
359
|
+
const fluentClient = await createClient(ctx, { validateConnection: true });
|
|
360
|
+
|
|
361
|
+
if (!ctx.connections || !ctx.connections.fluent_commerce) {
|
|
362
|
+
log.error('❌ [BACKGROUND] Missing fluent_commerce connection');
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Use :project: scope for cross-webhook RMA state sharing
|
|
367
|
+
// - createRmaFromReturn stores initial state
|
|
368
|
+
// - trackReturnShipment updates shipping status
|
|
369
|
+
// - qualityInspection adds inspection results
|
|
370
|
+
// - processRefundOrExchange marks completion
|
|
371
|
+
const kvAdapter = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
372
|
+
|
|
373
|
+
// Detect payload format (Shopify JSON vs SFCC XML)
|
|
374
|
+
const isShopify = payload.order_id && payload.return_line_items;
|
|
375
|
+
const isSFCC = payload.ReturnRequest || payload.return;
|
|
376
|
+
|
|
377
|
+
if (!isShopify && !isSFCC) {
|
|
378
|
+
log.error('❌ [BACKGROUND] Invalid payload format');
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
log.info(`🔍 [BACKGROUND] Detected source: ${isShopify ? 'Shopify' : 'SFCC'}`);
|
|
383
|
+
|
|
384
|
+
// =================================================================
|
|
385
|
+
// STEP 2: TRANSFORM TO FLUENT RMA FORMAT
|
|
386
|
+
// =================================================================
|
|
387
|
+
// Custom resolvers for return reason mapping
|
|
388
|
+
const customResolvers = {
|
|
389
|
+
// Map e-commerce return reasons to Fluent reason codes
|
|
390
|
+
'custom.mapReturnReason': (reason: string) => {
|
|
391
|
+
const reasonMap: Record<string, string> = {
|
|
392
|
+
'changed_mind': 'CUSTOMER_REMORSE',
|
|
393
|
+
'defective': 'DEFECTIVE',
|
|
394
|
+
'wrong_item': 'WRONG_ITEM_SHIPPED',
|
|
395
|
+
'damaged': 'DAMAGED_IN_TRANSIT',
|
|
396
|
+
'not_as_described': 'NOT_AS_DESCRIBED',
|
|
397
|
+
'sizing_issues': 'SIZE_FIT_ISSUE',
|
|
398
|
+
'late_delivery': 'LATE_DELIVERY',
|
|
399
|
+
'other': 'OTHER',
|
|
400
|
+
};
|
|
401
|
+
return reasonMap[reason?.toLowerCase()] || 'OTHER';
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
// Calculate restocking fee based on reason
|
|
405
|
+
// Resolver-only field: receives (value, sourceData, helpers)
|
|
406
|
+
// value = undefined (no source), sourceData = current array item
|
|
407
|
+
'custom.calculateRestockingFee': (value: any, sourceData: any, helpers: any) => {
|
|
408
|
+
const feeMap: Record<string, number> = {
|
|
409
|
+
'CUSTOMER_REMORSE': 0.15, // 15% restocking fee
|
|
410
|
+
'SIZE_FIT_ISSUE': 0.10, // 10% restocking fee
|
|
411
|
+
'OTHER': 0.10,
|
|
412
|
+
'DEFECTIVE': 0, // No fee for defective items
|
|
413
|
+
'WRONG_ITEM_SHIPPED': 0, // No fee for our errors
|
|
414
|
+
'DAMAGED_IN_TRANSIT': 0,
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// Extract fields from sourceData (the current item)
|
|
418
|
+
const reason = sourceData?.reason || 'OTHER';
|
|
419
|
+
const price = helpers.parseFloatSafe(sourceData?.price, 0);
|
|
420
|
+
|
|
421
|
+
const feePercentage = feeMap[reason] || 0;
|
|
422
|
+
return price * feePercentage;
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
// Generate RMA reference
|
|
426
|
+
'custom.generateRmaRef': (orderId: string) => {
|
|
427
|
+
const timestamp = Date.now().toString().slice(-6);
|
|
428
|
+
return `RMA-${orderId}-${timestamp}`;
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
// Determine if return is eligible (30-day window)
|
|
432
|
+
'custom.isReturnEligible': (orderDate: string) => {
|
|
433
|
+
const orderTime = new Date(orderDate).getTime();
|
|
434
|
+
const now = Date.now();
|
|
435
|
+
const daysSinceOrder = (now - orderTime) / (1000 * 60 * 60 * 24);
|
|
436
|
+
return daysSinceOrder <= 30; // 30-day return window
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// Transform return request to RMA format
|
|
441
|
+
const mapper = new UniversalMapper(rmaCreationMapping, {
|
|
442
|
+
customResolvers,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const transformResult = await mapper.map(payload);
|
|
446
|
+
|
|
447
|
+
if (!transformResult.success) {
|
|
448
|
+
log.error('❌ [BACKGROUND] Transformation failed', {
|
|
449
|
+
errors: transformResult.errors,
|
|
450
|
+
});
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const rmaData = transformResult.data;
|
|
455
|
+
|
|
456
|
+
log.info('✅ [RMA] Return request transformed', {
|
|
457
|
+
rmaRef: rmaData.ref,
|
|
458
|
+
itemCount: rmaData.items?.length || 0,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// =================================================================
|
|
462
|
+
// STEP 3: CHECK RETURN ELIGIBILITY
|
|
463
|
+
// =================================================================
|
|
464
|
+
if (!rmaData.eligible) {
|
|
465
|
+
log.warn('⚠️ [BACKGROUND] Return request ineligible', {
|
|
466
|
+
orderId: rmaData.orderRef,
|
|
467
|
+
reason: 'Outside return window',
|
|
468
|
+
});
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// =================================================================
|
|
473
|
+
// STEP 4: CHECK FOR DUPLICATE RMA
|
|
474
|
+
// =================================================================
|
|
475
|
+
const existingRmaJson = await kvAdapter.get(`rma-creation:${payload.order_id}`);
|
|
476
|
+
|
|
477
|
+
if (existingRmaJson) {
|
|
478
|
+
const existingRma = JSON.parse(existingRmaJson);
|
|
479
|
+
log.info('🔍 [BACKGROUND] RMA already exists for this order', {
|
|
480
|
+
orderId: payload.order_id,
|
|
481
|
+
existingRmaRef: existingRma.rmaRef,
|
|
482
|
+
});
|
|
483
|
+
return; // Already exists, exit early
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// =================================================================
|
|
487
|
+
// STEP 5: CREATE RMA IN FLUENT COMMERCE
|
|
488
|
+
// =================================================================
|
|
489
|
+
log.info('📝 [RMA] Creating RMA in Fluent', {
|
|
490
|
+
rmaRef: rmaData.ref,
|
|
491
|
+
orderRef: rmaData.orderRef,
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Create RMA using custom mutation
|
|
495
|
+
const rmaResult = await fluentClient.graphql({
|
|
496
|
+
query: `mutation CreateRMA($input: CreateRMAInput!) {
|
|
497
|
+
createRMA(input: $input) {
|
|
498
|
+
id
|
|
499
|
+
ref
|
|
500
|
+
status
|
|
501
|
+
orderRef
|
|
502
|
+
items {
|
|
503
|
+
id
|
|
504
|
+
productRef
|
|
505
|
+
quantity
|
|
506
|
+
returnReason
|
|
507
|
+
restockingFee
|
|
508
|
+
}
|
|
509
|
+
customer {
|
|
510
|
+
id
|
|
511
|
+
firstName
|
|
512
|
+
lastName
|
|
513
|
+
email
|
|
514
|
+
}
|
|
515
|
+
createdOn
|
|
516
|
+
attributes {
|
|
517
|
+
name
|
|
518
|
+
value
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}`,
|
|
522
|
+
variables: {
|
|
523
|
+
input: {
|
|
524
|
+
ref: rmaData.ref,
|
|
525
|
+
orderRef: rmaData.orderRef,
|
|
526
|
+
customerId: rmaData.customerId,
|
|
527
|
+
items: rmaData.items,
|
|
528
|
+
status: 'PENDING_APPROVAL',
|
|
529
|
+
returnMethod: rmaData.returnMethod || 'SHIP_BACK',
|
|
530
|
+
attributes: [
|
|
531
|
+
{ name: 'source', type: 'STRING', value: isShopify ? 'Shopify' : 'SFCC' },
|
|
532
|
+
{ name: 'returnReason', type: 'STRING', value: rmaData.primaryReason },
|
|
533
|
+
{ name: 'customerNotes', type: 'STRING', value: rmaData.notes || '' },
|
|
534
|
+
],
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
if (!rmaResult.data?.createRMA) {
|
|
540
|
+
log.error('❌ [BACKGROUND] RMA creation failed', {
|
|
541
|
+
errors: rmaResult.errors,
|
|
542
|
+
});
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const rma = rmaResult.data.createRMA;
|
|
547
|
+
|
|
548
|
+
log.info('✅ [RMA] RMA created successfully', {
|
|
549
|
+
rmaId: rma.id,
|
|
550
|
+
rmaRef: rma.ref,
|
|
551
|
+
status: rma.status,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// =================================================================
|
|
555
|
+
// STEP 6: STORE RMA STATE IN VERSORI KV
|
|
556
|
+
// =================================================================
|
|
557
|
+
const rmaState = {
|
|
558
|
+
rmaId: rma.id,
|
|
559
|
+
rmaRef: rma.ref,
|
|
560
|
+
orderRef: rma.orderRef,
|
|
561
|
+
status: rma.status,
|
|
562
|
+
customerId: rma.customer.id,
|
|
563
|
+
customerEmail: rma.customer.email,
|
|
564
|
+
itemCount: rma.items.length,
|
|
565
|
+
createdOn: rma.createdOn,
|
|
566
|
+
source: isShopify ? 'Shopify' : 'SFCC',
|
|
567
|
+
stage: 'RMA_CREATED',
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
await kvAdapter.set(`rma:${rma.ref}`, JSON.stringify(rmaState));
|
|
571
|
+
|
|
572
|
+
// Store creation timestamp to prevent duplicates
|
|
573
|
+
const creationState = {
|
|
574
|
+
rmaRef: rma.ref,
|
|
575
|
+
createdAt: new Date().toISOString(),
|
|
576
|
+
};
|
|
577
|
+
await kvAdapter.set(`rma-creation:${payload.order_id}`, JSON.stringify(creationState));
|
|
578
|
+
|
|
579
|
+
log.info('[RMA] State stored in KV', { rmaRef: rma.ref });
|
|
580
|
+
|
|
581
|
+
// =================================================================
|
|
582
|
+
// STEP 7: GENERATE RETURN SHIPPING LABEL (MOCK)
|
|
583
|
+
// =================================================================
|
|
584
|
+
log.info('[RMA] Generating return shipping label', {
|
|
585
|
+
rmaRef: rma.ref,
|
|
586
|
+
customerEmail: rma.customer.email,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// In production, integrate with ShipStation, EasyPost, or carrier API
|
|
590
|
+
const shippingLabel = {
|
|
591
|
+
trackingNumber: `TRK-${Date.now()}`,
|
|
592
|
+
labelUrl: `https://labels.example.com/${rma.ref}.pdf`,
|
|
593
|
+
carrier: 'USPS',
|
|
594
|
+
service: 'Priority Mail',
|
|
595
|
+
estimatedDelivery: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// Update RMA with shipping label info
|
|
599
|
+
await fluentClient.graphql({
|
|
600
|
+
query: `mutation UpdateRMA($id: ID!, $input: UpdateRMAInput!) {
|
|
601
|
+
updateRMA(id: $id, input: $input) {
|
|
602
|
+
id
|
|
603
|
+
ref
|
|
604
|
+
attributes {
|
|
605
|
+
name
|
|
606
|
+
value
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}`,
|
|
610
|
+
variables: {
|
|
611
|
+
id: rma.id,
|
|
612
|
+
input: {
|
|
613
|
+
attributes: [
|
|
614
|
+
{ name: 'returnTrackingNumber', type: 'STRING', value: shippingLabel.trackingNumber },
|
|
615
|
+
{ name: 'returnLabelUrl', type: 'STRING', value: shippingLabel.labelUrl },
|
|
616
|
+
{ name: 'returnCarrier', type: 'STRING', value: shippingLabel.carrier },
|
|
617
|
+
],
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
log.info('[RMA] Shipping label generated', {
|
|
623
|
+
trackingNumber: shippingLabel.trackingNumber,
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// =================================================================
|
|
627
|
+
// STEP 8: SEND EMAIL NOTIFICATION
|
|
628
|
+
// =================================================================
|
|
629
|
+
// In production, integrate with SendGrid, AWS SES, etc.
|
|
630
|
+
log.info('[RMA] Sending email notification', {
|
|
631
|
+
to: rma.customer.email,
|
|
632
|
+
rmaRef: rma.ref,
|
|
633
|
+
trackingNumber: shippingLabel.trackingNumber,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// =================================================================
|
|
637
|
+
// STEP 9: LOG SUCCESS
|
|
638
|
+
// =================================================================
|
|
639
|
+
const duration = Date.now() - startTime;
|
|
640
|
+
log.info('✅ [BACKGROUND] RMA creation completed successfully', {
|
|
641
|
+
rmaRef: rma.ref,
|
|
642
|
+
rmaId: rma.id,
|
|
643
|
+
duration: `${duration}ms`,
|
|
644
|
+
});
|
|
645
|
+
} catch (error: any) {
|
|
646
|
+
// ? Enhanced: Error logging with recommendations
|
|
647
|
+
const errorDetails = {
|
|
648
|
+
message: error instanceof Error ? error.message : String(error),
|
|
649
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
650
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
651
|
+
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
652
|
+
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
653
|
+
: error.message?.includes('mapping') || error.message?.includes('field')
|
|
654
|
+
? 'Check mapping configuration JSON and verify source paths match incoming return request structure'
|
|
655
|
+
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
656
|
+
? 'Check network connectivity and Fluent Commerce API availability'
|
|
657
|
+
: error.message?.includes('duplicate') || error.message?.includes('already exists')
|
|
658
|
+
? 'RMA may already exist for this order - check existing RMAs'
|
|
659
|
+
: error.message?.includes('eligible') || error.message?.includes('return window')
|
|
660
|
+
? 'Return request is outside the return window - verify order date'
|
|
661
|
+
: 'Review error details and check return request payload structure',
|
|
662
|
+
};
|
|
663
|
+
log.error('❌ [BACKGROUND] RMA creation failed', errorDetails);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* RMA Creation Webhook Handler
|
|
669
|
+
* Uses sync + fire-and-forget pattern for fast response
|
|
670
|
+
*/
|
|
671
|
+
export const createRmaFromReturn = webhook('create-rma', {
|
|
672
|
+
response: { mode: 'sync' }, // ✅ Sync mode: response sent when handler returns
|
|
673
|
+
}, async (ctx) => {
|
|
674
|
+
const { log, activation } = ctx;
|
|
675
|
+
const startTime = Date.now();
|
|
676
|
+
const payload = activation?.body || ctx.data;
|
|
677
|
+
|
|
678
|
+
log.info('🚀 [WEBHOOK] Received RMA creation webhook', {
|
|
679
|
+
source: payload?.source || 'unknown',
|
|
680
|
+
orderId: payload?.order_id || payload?.orderId,
|
|
681
|
+
timestamp: new Date().toISOString(),
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// Quick validation
|
|
685
|
+
if (!ctx.connections || !ctx.connections.fluent_commerce) {
|
|
686
|
+
log.error('❌ [WEBHOOK] Missing fluent_commerce connection');
|
|
687
|
+
return {
|
|
688
|
+
status: 500,
|
|
689
|
+
body: {
|
|
690
|
+
success: false,
|
|
691
|
+
error: 'Missing fluent_commerce connection',
|
|
692
|
+
recommendation: 'Configure fluent_commerce connection in Connections section with OAuth2 credentials',
|
|
693
|
+
timestamp: new Date().toISOString(),
|
|
694
|
+
},
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Detect payload format (Shopify JSON vs SFCC XML)
|
|
699
|
+
const isShopify = payload.order_id && payload.return_line_items;
|
|
700
|
+
const isSFCC = payload.ReturnRequest || payload.return;
|
|
701
|
+
|
|
702
|
+
if (!isShopify && !isSFCC) {
|
|
703
|
+
log.error('❌ [WEBHOOK] Invalid payload format');
|
|
704
|
+
return {
|
|
705
|
+
status: 400,
|
|
706
|
+
body: {
|
|
707
|
+
success: false,
|
|
708
|
+
error: 'INVALID_FORMAT',
|
|
709
|
+
message: 'Unsupported return request format',
|
|
710
|
+
timestamp: new Date().toISOString(),
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
log.info('✅ [WEBHOOK] Validation passed, starting background processing', {
|
|
716
|
+
source: isShopify ? 'Shopify' : 'SFCC',
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
// ✅ Fire-and-forget: Start background processing WITHOUT await
|
|
720
|
+
// The promise continues execution after we return the response
|
|
721
|
+
processRmaCreation(ctx, startTime)
|
|
722
|
+
.then(() => {
|
|
723
|
+
log.info('✅ [BACKGROUND] RMA creation processing completed successfully', {
|
|
724
|
+
orderId: payload?.order_id || payload?.orderId,
|
|
725
|
+
});
|
|
726
|
+
})
|
|
727
|
+
.catch((error: unknown) => {
|
|
728
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
729
|
+
log.error('❌ [BACKGROUND] RMA creation processing failed', {
|
|
730
|
+
orderId: payload?.order_id || payload?.orderId,
|
|
731
|
+
error: errorMessage,
|
|
732
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
733
|
+
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// Return immediately (response sent with this return value)
|
|
738
|
+
return {
|
|
739
|
+
status: 200,
|
|
740
|
+
body: {
|
|
741
|
+
success: true,
|
|
742
|
+
message: 'RMA creation started in background',
|
|
743
|
+
timestamp: new Date().toISOString(),
|
|
744
|
+
},
|
|
745
|
+
};
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* WEBHOOK 2: Track Return Shipment
|
|
750
|
+
*
|
|
751
|
+
* Receives shipment tracking updates (from carrier or ShipStation)
|
|
752
|
+
* and updates RMA status in Fluent.
|
|
753
|
+
*/
|
|
754
|
+
export const trackReturnShipment = webhook('track-return-shipment', async (ctx) => {
|
|
755
|
+
const { log, activation } = ctx;
|
|
756
|
+
const payload = activation?.body || ctx.data;
|
|
757
|
+
const startTime = Date.now();
|
|
758
|
+
|
|
759
|
+
log.info('🚀 [RMA] Received shipment tracking update', {
|
|
760
|
+
trackingNumber: payload.tracking_number,
|
|
761
|
+
status: payload.status,
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
try {
|
|
765
|
+
const fluentClient = await createClient(ctx, { validateConnection: true });
|
|
766
|
+
const kvAdapter = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
767
|
+
|
|
768
|
+
// Find RMA by tracking number
|
|
769
|
+
const trackingNumber = payload.tracking_number || payload.trackingNumber;
|
|
770
|
+
|
|
771
|
+
// Query Fluent for RMA with this tracking number
|
|
772
|
+
const rmaResult = await fluentClient.graphql({
|
|
773
|
+
query: `query FindRMAByTracking($trackingNumber: String!) {
|
|
774
|
+
rmas(
|
|
775
|
+
first: 1
|
|
776
|
+
attributes: [
|
|
777
|
+
{ name: "returnTrackingNumber", value: $trackingNumber }
|
|
778
|
+
]
|
|
779
|
+
) {
|
|
780
|
+
edges {
|
|
781
|
+
node {
|
|
782
|
+
id
|
|
783
|
+
ref
|
|
784
|
+
status
|
|
785
|
+
orderRef
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}`,
|
|
790
|
+
variables: { trackingNumber },
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
const rma = rmaResult.data?.rmas?.edges[0]?.node;
|
|
794
|
+
|
|
795
|
+
if (!rma) {
|
|
796
|
+
log.warn('⚠️ [RMA] No RMA found for tracking number', { trackingNumber });
|
|
797
|
+
return {
|
|
798
|
+
status: 404,
|
|
799
|
+
body: {
|
|
800
|
+
success: false,
|
|
801
|
+
error: 'RMA_NOT_FOUND',
|
|
802
|
+
message: `No RMA found with tracking number ${trackingNumber}`,
|
|
803
|
+
timestamp: new Date().toISOString(),
|
|
804
|
+
},
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
log.info('✅ [RMA] Found RMA for tracking update', {
|
|
809
|
+
rmaId: rma.id,
|
|
810
|
+
rmaRef: rma.ref,
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
// Map carrier status to RMA status
|
|
814
|
+
const carrierStatus = payload.status?.toUpperCase();
|
|
815
|
+
let rmaStatus = rma.status;
|
|
816
|
+
let eventType = 'TRACKING_UPDATE';
|
|
817
|
+
|
|
818
|
+
switch (carrierStatus) {
|
|
819
|
+
case 'IN_TRANSIT':
|
|
820
|
+
rmaStatus = 'IN_TRANSIT';
|
|
821
|
+
eventType = 'SHIPMENT_IN_TRANSIT';
|
|
822
|
+
break;
|
|
823
|
+
case 'OUT_FOR_DELIVERY':
|
|
824
|
+
rmaStatus = 'OUT_FOR_DELIVERY';
|
|
825
|
+
eventType = 'SHIPMENT_OUT_FOR_DELIVERY';
|
|
826
|
+
break;
|
|
827
|
+
case 'DELIVERED':
|
|
828
|
+
rmaStatus = 'RECEIVED';
|
|
829
|
+
eventType = 'SHIPMENT_DELIVERED';
|
|
830
|
+
break;
|
|
831
|
+
case 'EXCEPTION':
|
|
832
|
+
case 'FAILED':
|
|
833
|
+
rmaStatus = 'SHIPMENT_EXCEPTION';
|
|
834
|
+
eventType = 'SHIPMENT_EXCEPTION';
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Update RMA status in Fluent
|
|
839
|
+
await fluentClient.graphql({
|
|
840
|
+
query: `mutation UpdateRMAStatus($id: ID!, $input: UpdateRMAInput!) {
|
|
841
|
+
updateRMA(id: $id, input: $input) {
|
|
842
|
+
id
|
|
843
|
+
ref
|
|
844
|
+
status
|
|
845
|
+
}
|
|
846
|
+
}`,
|
|
847
|
+
variables: {
|
|
848
|
+
id: rma.id,
|
|
849
|
+
input: {
|
|
850
|
+
status: rmaStatus,
|
|
851
|
+
attributes: [
|
|
852
|
+
{ name: 'lastTrackingUpdate', type: 'STRING', value: new Date().toISOString() },
|
|
853
|
+
{ name: 'carrierStatus', type: 'STRING', value: carrierStatus },
|
|
854
|
+
],
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
// Update KV state
|
|
860
|
+
const existingStateJson = await kvAdapter.get(`rma:${rma.ref}`);
|
|
861
|
+
if (existingStateJson) {
|
|
862
|
+
const existingState = JSON.parse(existingStateJson);
|
|
863
|
+
const updatedState = {
|
|
864
|
+
...existingState,
|
|
865
|
+
status: rmaStatus,
|
|
866
|
+
lastTrackingUpdate: new Date().toISOString(),
|
|
867
|
+
carrierStatus,
|
|
868
|
+
stage: eventType,
|
|
869
|
+
};
|
|
870
|
+
await kvAdapter.set(`rma:${rma.ref}`, JSON.stringify(updatedState));
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const duration = Date.now() - startTime;
|
|
874
|
+
log.info('✅ [RMA] RMA status updated', {
|
|
875
|
+
rmaRef: rma.ref,
|
|
876
|
+
oldStatus: rma.status,
|
|
877
|
+
newStatus: rmaStatus,
|
|
878
|
+
duration: `${duration}ms`,
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
return {
|
|
882
|
+
status: 200,
|
|
883
|
+
body: {
|
|
884
|
+
success: true,
|
|
885
|
+
rmaRef: rma.ref,
|
|
886
|
+
status: rmaStatus,
|
|
887
|
+
eventType,
|
|
888
|
+
message: 'RMA status updated successfully',
|
|
889
|
+
duration: `${duration}ms`,
|
|
890
|
+
timestamp: new Date().toISOString(),
|
|
891
|
+
},
|
|
892
|
+
};
|
|
893
|
+
} catch (error: any) {
|
|
894
|
+
// ? Enhanced: Error logging with recommendations
|
|
895
|
+
const errorDetails = {
|
|
896
|
+
message: error instanceof Error ? error.message : String(error),
|
|
897
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
898
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
899
|
+
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
900
|
+
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
901
|
+
: error.message?.includes('not found') || error.message?.includes('missing')
|
|
902
|
+
? 'RMA not found - verify tracking number and RMA reference'
|
|
903
|
+
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
904
|
+
? 'Check network connectivity and Fluent Commerce API availability'
|
|
905
|
+
: 'Review error details and check tracking update payload structure',
|
|
906
|
+
};
|
|
907
|
+
log.error('[RMA] Failed to process tracking update', errorDetails);
|
|
908
|
+
return {
|
|
909
|
+
status: 500,
|
|
910
|
+
body: {
|
|
911
|
+
success: false,
|
|
912
|
+
error: error.message,
|
|
913
|
+
recommendation: errorDetails.recommendation,
|
|
914
|
+
timestamp: new Date().toISOString(),
|
|
915
|
+
},
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* WEBHOOK 3: Quality Inspection
|
|
922
|
+
*
|
|
923
|
+
* Warehouse receives return, inspects items, and updates RMA with inspection results.
|
|
924
|
+
* Determines if items can be restocked, need repair, or should be scrapped.
|
|
925
|
+
*/
|
|
926
|
+
export const qualityInspection = webhook('quality-inspection', async (ctx) => {
|
|
927
|
+
const { log, activation } = ctx;
|
|
928
|
+
const payload = activation?.body || ctx.data;
|
|
929
|
+
const startTime = Date.now();
|
|
930
|
+
|
|
931
|
+
log.info('🚀 [RMA] Processing quality inspection', {
|
|
932
|
+
rmaRef: payload.rmaRef,
|
|
933
|
+
itemCount: payload.items?.length || 0,
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
try {
|
|
937
|
+
const fluentClient = await createClient(ctx, { validateConnection: true });
|
|
938
|
+
const kvAdapter = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
939
|
+
|
|
940
|
+
// Validate payload
|
|
941
|
+
if (!payload.rmaRef || !payload.items || payload.items.length === 0) {
|
|
942
|
+
return {
|
|
943
|
+
status: 400,
|
|
944
|
+
body: {
|
|
945
|
+
success: false,
|
|
946
|
+
error: 'INVALID_PAYLOAD',
|
|
947
|
+
message: 'Invalid inspection payload: missing rmaRef or items',
|
|
948
|
+
timestamp: new Date().toISOString(),
|
|
949
|
+
},
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// =================================================================
|
|
954
|
+
// STEP 1: RETRIEVE RMA FROM FLUENT
|
|
955
|
+
// =================================================================
|
|
956
|
+
const rmaResult = await fluentClient.graphql({
|
|
957
|
+
query: `query GetRMA($ref: String!) {
|
|
958
|
+
rma(ref: $ref) {
|
|
959
|
+
id
|
|
960
|
+
ref
|
|
961
|
+
status
|
|
962
|
+
orderRef
|
|
963
|
+
items {
|
|
964
|
+
id
|
|
965
|
+
productRef
|
|
966
|
+
quantity
|
|
967
|
+
returnReason
|
|
968
|
+
restockingFee
|
|
969
|
+
}
|
|
970
|
+
customer {
|
|
971
|
+
id
|
|
972
|
+
email
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}`,
|
|
976
|
+
variables: { ref: payload.rmaRef },
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
const rma = rmaResult.data?.rma;
|
|
980
|
+
|
|
981
|
+
if (!rma) {
|
|
982
|
+
return {
|
|
983
|
+
status: 404,
|
|
984
|
+
body: {
|
|
985
|
+
success: false,
|
|
986
|
+
error: 'RMA_NOT_FOUND',
|
|
987
|
+
message: `RMA not found: ${payload.rmaRef}`,
|
|
988
|
+
timestamp: new Date().toISOString(),
|
|
989
|
+
},
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
log.info('✅ [RMA] RMA retrieved', {
|
|
994
|
+
rmaId: rma.id,
|
|
995
|
+
rmaRef: rma.ref,
|
|
996
|
+
currentStatus: rma.status,
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// =================================================================
|
|
1000
|
+
// STEP 2: PROCESS INSPECTION RESULTS
|
|
1001
|
+
// =================================================================
|
|
1002
|
+
const inspectionResults = payload.items.map((item: any) => {
|
|
1003
|
+
const condition = item.condition?.toUpperCase();
|
|
1004
|
+
let disposition = 'RESTOCK';
|
|
1005
|
+
let restockable = true;
|
|
1006
|
+
|
|
1007
|
+
switch (condition) {
|
|
1008
|
+
case 'NEW':
|
|
1009
|
+
case 'LIKE_NEW':
|
|
1010
|
+
disposition = 'RESTOCK';
|
|
1011
|
+
restockable = true;
|
|
1012
|
+
break;
|
|
1013
|
+
case 'DAMAGED':
|
|
1014
|
+
case 'DEFECTIVE':
|
|
1015
|
+
disposition = 'SCRAP';
|
|
1016
|
+
restockable = false;
|
|
1017
|
+
break;
|
|
1018
|
+
case 'OPENED':
|
|
1019
|
+
case 'USED':
|
|
1020
|
+
disposition = 'OPEN_BOX';
|
|
1021
|
+
restockable = true;
|
|
1022
|
+
break;
|
|
1023
|
+
case 'MISSING_PARTS':
|
|
1024
|
+
disposition = 'REPAIR';
|
|
1025
|
+
restockable = false;
|
|
1026
|
+
break;
|
|
1027
|
+
default:
|
|
1028
|
+
disposition = 'MANUAL_REVIEW';
|
|
1029
|
+
restockable = false;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
return {
|
|
1033
|
+
itemId: item.itemId,
|
|
1034
|
+
productRef: item.productRef || item.sku,
|
|
1035
|
+
condition,
|
|
1036
|
+
disposition,
|
|
1037
|
+
restockable,
|
|
1038
|
+
notes: item.notes || '',
|
|
1039
|
+
photoUrls: item.photoUrls || [],
|
|
1040
|
+
};
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
log.info('✅ [RMA] Inspection results processed', {
|
|
1044
|
+
total: inspectionResults.length,
|
|
1045
|
+
restockable: inspectionResults.filter(r => r.restockable).length,
|
|
1046
|
+
scrap: inspectionResults.filter(r => r.disposition === 'SCRAP').length,
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
// =================================================================
|
|
1050
|
+
// STEP 3: UPDATE RMA WITH INSPECTION RESULTS
|
|
1051
|
+
// =================================================================
|
|
1052
|
+
await fluentClient.graphql({
|
|
1053
|
+
query: `mutation UpdateRMAInspection($id: ID!, $input: UpdateRMAInput!) {
|
|
1054
|
+
updateRMA(id: $id, input: $input) {
|
|
1055
|
+
id
|
|
1056
|
+
ref
|
|
1057
|
+
status
|
|
1058
|
+
}
|
|
1059
|
+
}`,
|
|
1060
|
+
variables: {
|
|
1061
|
+
id: rma.id,
|
|
1062
|
+
input: {
|
|
1063
|
+
status: 'INSPECTED',
|
|
1064
|
+
attributes: [
|
|
1065
|
+
{ name: 'inspectionDate', type: 'STRING', value: new Date().toISOString() },
|
|
1066
|
+
{ name: 'inspectionResults', type: 'JSON', value: JSON.stringify(inspectionResults) },
|
|
1067
|
+
{ name: 'inspector', type: 'STRING', value: payload.inspector || 'SYSTEM' },
|
|
1068
|
+
],
|
|
1069
|
+
},
|
|
1070
|
+
},
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
log.info('[RMA] RMA updated with inspection results', {
|
|
1074
|
+
rmaRef: rma.ref,
|
|
1075
|
+
status: 'INSPECTED',
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
// =================================================================
|
|
1079
|
+
// STEP 4: UPDATE INVENTORY FOR RESTOCKABLE ITEMS
|
|
1080
|
+
// =================================================================
|
|
1081
|
+
const restockableItems = inspectionResults.filter(r => r.restockable);
|
|
1082
|
+
|
|
1083
|
+
if (restockableItems.length > 0) {
|
|
1084
|
+
log.info('[RMA] Restocking items', {
|
|
1085
|
+
count: restockableItems.length,
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
for (const item of restockableItems) {
|
|
1089
|
+
// Increment inventory for restockable items
|
|
1090
|
+
await fluentClient.graphql({
|
|
1091
|
+
query: `mutation AdjustInventory($input: AdjustInventoryInput!) {
|
|
1092
|
+
adjustInventory(input: $input) {
|
|
1093
|
+
id
|
|
1094
|
+
ref
|
|
1095
|
+
quantity
|
|
1096
|
+
}
|
|
1097
|
+
}`,
|
|
1098
|
+
variables: {
|
|
1099
|
+
input: {
|
|
1100
|
+
productRef: item.productRef,
|
|
1101
|
+
locationRef: payload.locationRef || 'RETURNS_WAREHOUSE',
|
|
1102
|
+
adjustment: 1, // Increment by quantity returned
|
|
1103
|
+
reason: 'RMA_RESTOCK',
|
|
1104
|
+
ref: `${rma.ref}-${item.productRef}`,
|
|
1105
|
+
},
|
|
1106
|
+
},
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
log.info('[RMA] Inventory adjusted', {
|
|
1110
|
+
productRef: item.productRef,
|
|
1111
|
+
disposition: item.disposition,
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// =================================================================
|
|
1117
|
+
// STEP 5: UPDATE KV STATE
|
|
1118
|
+
// =================================================================
|
|
1119
|
+
const existingStateJson = await kvAdapter.get(`rma:${rma.ref}`);
|
|
1120
|
+
if (existingStateJson) {
|
|
1121
|
+
const existingState = JSON.parse(existingStateJson);
|
|
1122
|
+
const updatedState = {
|
|
1123
|
+
...existingState,
|
|
1124
|
+
status: 'INSPECTED',
|
|
1125
|
+
stage: 'INSPECTION_COMPLETE',
|
|
1126
|
+
inspectionResults,
|
|
1127
|
+
inspectionDate: new Date().toISOString(),
|
|
1128
|
+
};
|
|
1129
|
+
await kvAdapter.set(`rma:${rma.ref}`, JSON.stringify(updatedState));
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// =================================================================
|
|
1133
|
+
// STEP 6: DETERMINE NEXT ACTION (REFUND OR EXCHANGE)
|
|
1134
|
+
// =================================================================
|
|
1135
|
+
const refundApproved = inspectionResults.every(r =>
|
|
1136
|
+
r.disposition === 'RESTOCK' || r.disposition === 'OPEN_BOX'
|
|
1137
|
+
);
|
|
1138
|
+
|
|
1139
|
+
const duration = Date.now() - startTime;
|
|
1140
|
+
log.info('✅ [RMA] Inspection complete', {
|
|
1141
|
+
rmaRef: rma.ref,
|
|
1142
|
+
refundApproved,
|
|
1143
|
+
nextAction: payload.returnType === 'exchange' ? 'CREATE_EXCHANGE_ORDER' : 'PROCESS_REFUND',
|
|
1144
|
+
duration: `${duration}ms`,
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
return {
|
|
1148
|
+
status: 200,
|
|
1149
|
+
body: {
|
|
1150
|
+
success: true,
|
|
1151
|
+
rmaRef: rma.ref,
|
|
1152
|
+
status: 'INSPECTED',
|
|
1153
|
+
inspectionResults,
|
|
1154
|
+
refundApproved,
|
|
1155
|
+
nextAction: payload.returnType === 'exchange' ? 'CREATE_EXCHANGE_ORDER' : 'PROCESS_REFUND',
|
|
1156
|
+
message: 'Quality inspection completed successfully',
|
|
1157
|
+
duration: `${duration}ms`,
|
|
1158
|
+
timestamp: new Date().toISOString(),
|
|
1159
|
+
},
|
|
1160
|
+
};
|
|
1161
|
+
} catch (error: any) {
|
|
1162
|
+
// ? Enhanced: Error logging with recommendations
|
|
1163
|
+
const errorDetails = {
|
|
1164
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1165
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1166
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
1167
|
+
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
1168
|
+
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
1169
|
+
: error.message?.includes('not found') || error.message?.includes('missing')
|
|
1170
|
+
? 'RMA not found - verify RMA reference and inspection payload structure'
|
|
1171
|
+
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
1172
|
+
? 'Check network connectivity and Fluent Commerce API availability'
|
|
1173
|
+
: 'Review error details and check inspection payload structure',
|
|
1174
|
+
};
|
|
1175
|
+
log.error('[RMA] Failed to process inspection', errorDetails);
|
|
1176
|
+
return {
|
|
1177
|
+
status: 500,
|
|
1178
|
+
body: {
|
|
1179
|
+
success: false,
|
|
1180
|
+
error: error.message,
|
|
1181
|
+
recommendation: errorDetails.recommendation,
|
|
1182
|
+
timestamp: new Date().toISOString(),
|
|
1183
|
+
},
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* WEBHOOK 4: Process Refund or Exchange
|
|
1190
|
+
*
|
|
1191
|
+
* After inspection approval, processes refund via payment gateway
|
|
1192
|
+
* or creates exchange order.
|
|
1193
|
+
*/
|
|
1194
|
+
export const processRefundOrExchange = webhook('process-refund-exchange', async (ctx) => {
|
|
1195
|
+
const { log, activation } = ctx;
|
|
1196
|
+
const payload = activation?.body || ctx.data;
|
|
1197
|
+
const startTime = Date.now();
|
|
1198
|
+
|
|
1199
|
+
log.info('🚀 [RMA] Processing refund/exchange', {
|
|
1200
|
+
rmaRef: payload.rmaRef,
|
|
1201
|
+
actionType: payload.actionType, // 'refund' or 'exchange'
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
try {
|
|
1205
|
+
const fluentClient = await createClient(ctx, { validateConnection: true });
|
|
1206
|
+
const kvAdapter = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
1207
|
+
|
|
1208
|
+
// Retrieve RMA
|
|
1209
|
+
const rmaResult = await fluentClient.graphql({
|
|
1210
|
+
query: `query GetRMA($ref: String!) {
|
|
1211
|
+
rma(ref: $ref) {
|
|
1212
|
+
id
|
|
1213
|
+
ref
|
|
1214
|
+
status
|
|
1215
|
+
orderRef
|
|
1216
|
+
items {
|
|
1217
|
+
id
|
|
1218
|
+
productRef
|
|
1219
|
+
quantity
|
|
1220
|
+
price
|
|
1221
|
+
restockingFee
|
|
1222
|
+
}
|
|
1223
|
+
customer {
|
|
1224
|
+
id
|
|
1225
|
+
email
|
|
1226
|
+
}
|
|
1227
|
+
attributes {
|
|
1228
|
+
name
|
|
1229
|
+
value
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}`,
|
|
1233
|
+
variables: { ref: payload.rmaRef },
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
const rma = rmaResult.data?.rma;
|
|
1237
|
+
|
|
1238
|
+
if (!rma) {
|
|
1239
|
+
return {
|
|
1240
|
+
status: 404,
|
|
1241
|
+
body: {
|
|
1242
|
+
success: false,
|
|
1243
|
+
error: 'RMA_NOT_FOUND',
|
|
1244
|
+
message: `RMA not found: ${payload.rmaRef}`,
|
|
1245
|
+
timestamp: new Date().toISOString(),
|
|
1246
|
+
},
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const actionType = payload.actionType?.toLowerCase();
|
|
1251
|
+
|
|
1252
|
+
// =================================================================
|
|
1253
|
+
// OPTION 1: PROCESS REFUND
|
|
1254
|
+
// =================================================================
|
|
1255
|
+
if (actionType === 'refund') {
|
|
1256
|
+
log.info('💰 [RMA] Processing refund', { rmaRef: rma.ref });
|
|
1257
|
+
|
|
1258
|
+
// Calculate refund amount (subtract restocking fees)
|
|
1259
|
+
const totalRefund = rma.items.reduce((sum: number, item: any) => {
|
|
1260
|
+
const itemTotal = item.price * item.quantity;
|
|
1261
|
+
const restockingFee = item.restockingFee || 0;
|
|
1262
|
+
return sum + (itemTotal - restockingFee);
|
|
1263
|
+
}, 0);
|
|
1264
|
+
|
|
1265
|
+
log.info('[RMA] Refund calculated', {
|
|
1266
|
+
totalRefund,
|
|
1267
|
+
itemCount: rma.items.length,
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
// In production, integrate with payment gateway (Stripe, Adyen, etc.)
|
|
1271
|
+
const refundResponse = {
|
|
1272
|
+
refundId: `REF-${Date.now()}`,
|
|
1273
|
+
amount: totalRefund,
|
|
1274
|
+
currency: 'USD',
|
|
1275
|
+
status: 'PROCESSED',
|
|
1276
|
+
transactionId: `TXN-${Date.now()}`,
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
// Update RMA status
|
|
1280
|
+
await fluentClient.graphql({
|
|
1281
|
+
query: `mutation UpdateRMARefund($id: ID!, $input: UpdateRMAInput!) {
|
|
1282
|
+
updateRMA(id: $id, input: $input) {
|
|
1283
|
+
id
|
|
1284
|
+
ref
|
|
1285
|
+
status
|
|
1286
|
+
}
|
|
1287
|
+
}`,
|
|
1288
|
+
variables: {
|
|
1289
|
+
id: rma.id,
|
|
1290
|
+
input: {
|
|
1291
|
+
status: 'REFUNDED',
|
|
1292
|
+
attributes: [
|
|
1293
|
+
{ name: 'refundId', type: 'STRING', value: refundResponse.refundId },
|
|
1294
|
+
{ name: 'refundAmount', type: 'STRING', value: totalRefund.toString() },
|
|
1295
|
+
{ name: 'refundDate', type: 'STRING', value: new Date().toISOString() },
|
|
1296
|
+
],
|
|
1297
|
+
},
|
|
1298
|
+
},
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
// Update KV state
|
|
1302
|
+
const existingStateJson = await kvAdapter.get(`rma:${rma.ref}`);
|
|
1303
|
+
if (existingStateJson) {
|
|
1304
|
+
const existingState = JSON.parse(existingStateJson);
|
|
1305
|
+
const updatedState = {
|
|
1306
|
+
...existingState,
|
|
1307
|
+
status: 'REFUNDED',
|
|
1308
|
+
stage: 'REFUND_COMPLETE',
|
|
1309
|
+
refundAmount: totalRefund,
|
|
1310
|
+
refundId: refundResponse.refundId,
|
|
1311
|
+
refundDate: new Date().toISOString(),
|
|
1312
|
+
};
|
|
1313
|
+
await kvAdapter.set(`rma:${rma.ref}`, JSON.stringify(updatedState));
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const duration = Date.now() - startTime;
|
|
1317
|
+
log.info('✅ [RMA] Refund processed successfully', {
|
|
1318
|
+
rmaRef: rma.ref,
|
|
1319
|
+
refundId: refundResponse.refundId,
|
|
1320
|
+
amount: totalRefund,
|
|
1321
|
+
duration: `${duration}ms`,
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
return {
|
|
1325
|
+
status: 200,
|
|
1326
|
+
body: {
|
|
1327
|
+
success: true,
|
|
1328
|
+
rmaRef: rma.ref,
|
|
1329
|
+
actionType: 'refund',
|
|
1330
|
+
refund: refundResponse,
|
|
1331
|
+
message: `Refund of $${totalRefund} processed successfully`,
|
|
1332
|
+
duration: `${duration}ms`,
|
|
1333
|
+
timestamp: new Date().toISOString(),
|
|
1334
|
+
},
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// =================================================================
|
|
1339
|
+
// OPTION 2: CREATE EXCHANGE ORDER
|
|
1340
|
+
// =================================================================
|
|
1341
|
+
if (actionType === 'exchange') {
|
|
1342
|
+
log.info('🔄 [RMA] Creating exchange order', { rmaRef: rma.ref });
|
|
1343
|
+
|
|
1344
|
+
// Exchange items (from payload)
|
|
1345
|
+
const exchangeItems = payload.exchangeItems || [];
|
|
1346
|
+
|
|
1347
|
+
if (exchangeItems.length === 0) {
|
|
1348
|
+
return {
|
|
1349
|
+
status: 400,
|
|
1350
|
+
body: {
|
|
1351
|
+
success: false,
|
|
1352
|
+
error: 'INVALID_PAYLOAD',
|
|
1353
|
+
message: 'No exchange items provided',
|
|
1354
|
+
timestamp: new Date().toISOString(),
|
|
1355
|
+
},
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// Create new order in Fluent
|
|
1360
|
+
const exchangeOrderResult = await fluentClient.graphql({
|
|
1361
|
+
query: `mutation CreateExchangeOrder($input: CreateOrderInput!) {
|
|
1362
|
+
createOrder(input: $input) {
|
|
1363
|
+
id
|
|
1364
|
+
ref
|
|
1365
|
+
status
|
|
1366
|
+
items {
|
|
1367
|
+
id
|
|
1368
|
+
productRef
|
|
1369
|
+
quantity
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}`,
|
|
1373
|
+
variables: {
|
|
1374
|
+
input: {
|
|
1375
|
+
ref: `EXCH-${rma.ref}`,
|
|
1376
|
+
type: 'EXCHANGE',
|
|
1377
|
+
customerId: rma.customer.id,
|
|
1378
|
+
items: exchangeItems.map((item: any) => ({
|
|
1379
|
+
productRef: item.productRef,
|
|
1380
|
+
quantity: item.quantity,
|
|
1381
|
+
})),
|
|
1382
|
+
attributes: [
|
|
1383
|
+
{ name: 'originalRMA', type: 'STRING', value: rma.ref },
|
|
1384
|
+
{ name: 'originalOrder', type: 'STRING', value: rma.orderRef },
|
|
1385
|
+
{ name: 'source', type: 'STRING', value: 'RMA_EXCHANGE' },
|
|
1386
|
+
],
|
|
1387
|
+
},
|
|
1388
|
+
},
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
const exchangeOrder = exchangeOrderResult.data?.createOrder;
|
|
1392
|
+
|
|
1393
|
+
if (!exchangeOrder) {
|
|
1394
|
+
return {
|
|
1395
|
+
status: 502,
|
|
1396
|
+
body: {
|
|
1397
|
+
success: false,
|
|
1398
|
+
error: 'API_ERROR',
|
|
1399
|
+
message: 'Failed to create exchange order',
|
|
1400
|
+
timestamp: new Date().toISOString(),
|
|
1401
|
+
},
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Update RMA with exchange order reference
|
|
1406
|
+
await fluentClient.graphql({
|
|
1407
|
+
query: `mutation UpdateRMAExchange($id: ID!, $input: UpdateRMAInput!) {
|
|
1408
|
+
updateRMA(id: $id, input: $input) {
|
|
1409
|
+
id
|
|
1410
|
+
ref
|
|
1411
|
+
status
|
|
1412
|
+
}
|
|
1413
|
+
}`,
|
|
1414
|
+
variables: {
|
|
1415
|
+
id: rma.id,
|
|
1416
|
+
input: {
|
|
1417
|
+
status: 'EXCHANGED',
|
|
1418
|
+
attributes: [
|
|
1419
|
+
{ name: 'exchangeOrderRef', type: 'STRING', value: exchangeOrder.ref },
|
|
1420
|
+
{ name: 'exchangeDate', type: 'STRING', value: new Date().toISOString() },
|
|
1421
|
+
],
|
|
1422
|
+
},
|
|
1423
|
+
},
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
// Update KV state
|
|
1427
|
+
const existingStateJson = await kvAdapter.get(`rma:${rma.ref}`);
|
|
1428
|
+
if (existingStateJson) {
|
|
1429
|
+
const existingState = JSON.parse(existingStateJson);
|
|
1430
|
+
const updatedState = {
|
|
1431
|
+
...existingState,
|
|
1432
|
+
status: 'EXCHANGED',
|
|
1433
|
+
stage: 'EXCHANGE_COMPLETE',
|
|
1434
|
+
exchangeOrderRef: exchangeOrder.ref,
|
|
1435
|
+
exchangeDate: new Date().toISOString(),
|
|
1436
|
+
};
|
|
1437
|
+
await kvAdapter.set(`rma:${rma.ref}`, JSON.stringify(updatedState));
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
const duration = Date.now() - startTime;
|
|
1441
|
+
log.info('✅ [RMA] Exchange order created', {
|
|
1442
|
+
rmaRef: rma.ref,
|
|
1443
|
+
exchangeOrderRef: exchangeOrder.ref,
|
|
1444
|
+
duration: `${duration}ms`,
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
return {
|
|
1448
|
+
status: 200,
|
|
1449
|
+
body: {
|
|
1450
|
+
success: true,
|
|
1451
|
+
rmaRef: rma.ref,
|
|
1452
|
+
actionType: 'exchange',
|
|
1453
|
+
exchangeOrder: {
|
|
1454
|
+
ref: exchangeOrder.ref,
|
|
1455
|
+
id: exchangeOrder.id,
|
|
1456
|
+
status: exchangeOrder.status,
|
|
1457
|
+
},
|
|
1458
|
+
message: `Exchange order ${exchangeOrder.ref} created successfully`,
|
|
1459
|
+
duration: `${duration}ms`,
|
|
1460
|
+
timestamp: new Date().toISOString(),
|
|
1461
|
+
},
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
return {
|
|
1466
|
+
status: 400,
|
|
1467
|
+
body: {
|
|
1468
|
+
success: false,
|
|
1469
|
+
error: 'INVALID_ACTION_TYPE',
|
|
1470
|
+
message: `Invalid action type: ${actionType}`,
|
|
1471
|
+
timestamp: new Date().toISOString(),
|
|
1472
|
+
},
|
|
1473
|
+
};
|
|
1474
|
+
} catch (error: any) {
|
|
1475
|
+
// ? Enhanced: Error logging with recommendations
|
|
1476
|
+
const errorDetails = {
|
|
1477
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1478
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1479
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
1480
|
+
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
1481
|
+
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
1482
|
+
: error.message?.includes('payment') || error.message?.includes('gateway')
|
|
1483
|
+
? 'Check payment gateway configuration and refund processing credentials'
|
|
1484
|
+
: error.message?.includes('exchange') || error.message?.includes('order')
|
|
1485
|
+
? 'Check exchange order creation payload and product availability'
|
|
1486
|
+
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
1487
|
+
? 'Check network connectivity and Fluent Commerce API availability'
|
|
1488
|
+
: 'Review error details and check refund/exchange payload structure',
|
|
1489
|
+
};
|
|
1490
|
+
log.error('[RMA] Failed to process refund/exchange', errorDetails);
|
|
1491
|
+
return {
|
|
1492
|
+
status: 500,
|
|
1493
|
+
body: {
|
|
1494
|
+
success: false,
|
|
1495
|
+
error: error.message,
|
|
1496
|
+
recommendation: errorDetails.recommendation,
|
|
1497
|
+
timestamp: new Date().toISOString(),
|
|
1498
|
+
},
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
/**
|
|
1504
|
+
* WEBHOOK 5: Get RMA Status
|
|
1505
|
+
*
|
|
1506
|
+
* Query endpoint to retrieve RMA status and history
|
|
1507
|
+
*/
|
|
1508
|
+
export const getRmaStatus = webhook('get-rma-status', async (ctx) => {
|
|
1509
|
+
const { log, activation } = ctx;
|
|
1510
|
+
const payload = activation?.body || ctx.data;
|
|
1511
|
+
const rmaRef = payload?.rmaRef || ctx.query?.rmaRef;
|
|
1512
|
+
const startTime = Date.now();
|
|
1513
|
+
|
|
1514
|
+
log.info('🚀 [RMA] Querying RMA status', { rmaRef });
|
|
1515
|
+
|
|
1516
|
+
if (!rmaRef) {
|
|
1517
|
+
return {
|
|
1518
|
+
status: 400,
|
|
1519
|
+
body: {
|
|
1520
|
+
success: false,
|
|
1521
|
+
error: 'MISSING_PARAMETER',
|
|
1522
|
+
message: 'Missing rmaRef parameter',
|
|
1523
|
+
timestamp: new Date().toISOString(),
|
|
1524
|
+
},
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
try {
|
|
1529
|
+
const fluentClient = await createClient(ctx, { validateConnection: true });
|
|
1530
|
+
const kvAdapter = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
1531
|
+
|
|
1532
|
+
// Get RMA from Fluent
|
|
1533
|
+
const rmaResult = await fluentClient.graphql({
|
|
1534
|
+
query: `query GetRMA($ref: String!) {
|
|
1535
|
+
rma(ref: $ref) {
|
|
1536
|
+
id
|
|
1537
|
+
ref
|
|
1538
|
+
status
|
|
1539
|
+
orderRef
|
|
1540
|
+
items {
|
|
1541
|
+
id
|
|
1542
|
+
productRef
|
|
1543
|
+
quantity
|
|
1544
|
+
returnReason
|
|
1545
|
+
price
|
|
1546
|
+
restockingFee
|
|
1547
|
+
}
|
|
1548
|
+
customer {
|
|
1549
|
+
id
|
|
1550
|
+
firstName
|
|
1551
|
+
lastName
|
|
1552
|
+
email
|
|
1553
|
+
}
|
|
1554
|
+
createdOn
|
|
1555
|
+
updatedOn
|
|
1556
|
+
attributes {
|
|
1557
|
+
name
|
|
1558
|
+
value
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}`,
|
|
1562
|
+
variables: { ref: rmaRef },
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
const rma = rmaResult.data?.rma;
|
|
1566
|
+
|
|
1567
|
+
if (!rma) {
|
|
1568
|
+
return {
|
|
1569
|
+
status: 404,
|
|
1570
|
+
body: {
|
|
1571
|
+
success: false,
|
|
1572
|
+
error: 'RMA_NOT_FOUND',
|
|
1573
|
+
message: `RMA not found: ${rmaRef}`,
|
|
1574
|
+
timestamp: new Date().toISOString(),
|
|
1575
|
+
},
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Get state from KV
|
|
1580
|
+
const rmaStateJson = await kvAdapter.get(`rma:${rmaRef}`);
|
|
1581
|
+
const rmaState = rmaStateJson ? JSON.parse(rmaStateJson) : null;
|
|
1582
|
+
|
|
1583
|
+
const duration = Date.now() - startTime;
|
|
1584
|
+
log.info('✅ [RMA] Status query complete', {
|
|
1585
|
+
rmaRef: rma.ref,
|
|
1586
|
+
status: rma.status,
|
|
1587
|
+
duration: `${duration}ms`,
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
return {
|
|
1591
|
+
status: 200,
|
|
1592
|
+
body: {
|
|
1593
|
+
success: true,
|
|
1594
|
+
rma: {
|
|
1595
|
+
id: rma.id,
|
|
1596
|
+
ref: rma.ref,
|
|
1597
|
+
status: rma.status,
|
|
1598
|
+
orderRef: rma.orderRef,
|
|
1599
|
+
itemCount: rma.items.length,
|
|
1600
|
+
customer: rma.customer,
|
|
1601
|
+
createdOn: rma.createdOn,
|
|
1602
|
+
updatedOn: rma.updatedOn,
|
|
1603
|
+
},
|
|
1604
|
+
state: rmaState,
|
|
1605
|
+
attributes: rma.attributes,
|
|
1606
|
+
duration: `${duration}ms`,
|
|
1607
|
+
timestamp: new Date().toISOString(),
|
|
1608
|
+
},
|
|
1609
|
+
};
|
|
1610
|
+
} catch (error: any) {
|
|
1611
|
+
// ? Enhanced: Error logging with recommendations
|
|
1612
|
+
const errorDetails = {
|
|
1613
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1614
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1615
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
1616
|
+
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
1617
|
+
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
1618
|
+
: error.message?.includes('not found') || error.message?.includes('missing')
|
|
1619
|
+
? 'RMA not found - verify RMA reference and query parameters'
|
|
1620
|
+
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
1621
|
+
? 'Check network connectivity and Fluent Commerce API availability'
|
|
1622
|
+
: 'Review error details and check RMA status query parameters',
|
|
1623
|
+
};
|
|
1624
|
+
log.error('[RMA] Failed to query RMA', errorDetails);
|
|
1625
|
+
return {
|
|
1626
|
+
status: 500,
|
|
1627
|
+
body: {
|
|
1628
|
+
success: false,
|
|
1629
|
+
error: error.message,
|
|
1630
|
+
recommendation: errorDetails.recommendation,
|
|
1631
|
+
timestamp: new Date().toISOString(),
|
|
1632
|
+
},
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
});
|
|
1636
|
+
```
|
|
1637
|
+
|
|
1638
|
+
---
|
|
1639
|
+
|
|
1640
|
+
## 2. RMA Mapping Configuration: `mappings/shopify-return-to-rma.json`
|
|
1641
|
+
|
|
1642
|
+
```json
|
|
1643
|
+
{
|
|
1644
|
+
"version": "1.0.0",
|
|
1645
|
+
"description": "Shopify return request to Fluent RMA mapping",
|
|
1646
|
+
"direction": "ingest",
|
|
1647
|
+
"sourceFormat": "json",
|
|
1648
|
+
"fields": {
|
|
1649
|
+
"ref": {
|
|
1650
|
+
"source": "order_id",
|
|
1651
|
+
"resolver": "custom.generateRmaRef",
|
|
1652
|
+
"required": true
|
|
1653
|
+
},
|
|
1654
|
+
"orderRef": {
|
|
1655
|
+
"source": "order_id",
|
|
1656
|
+
"required": true
|
|
1657
|
+
},
|
|
1658
|
+
"customerId": {
|
|
1659
|
+
"source": "customer.id",
|
|
1660
|
+
"required": true
|
|
1661
|
+
},
|
|
1662
|
+
"customerEmail": {
|
|
1663
|
+
"source": "customer.email",
|
|
1664
|
+
"resolver": "sdk.lowercase",
|
|
1665
|
+
"required": true
|
|
1666
|
+
},
|
|
1667
|
+
"eligible": {
|
|
1668
|
+
"source": "order.created_at",
|
|
1669
|
+
"resolver": "custom.isReturnEligible"
|
|
1670
|
+
},
|
|
1671
|
+
"returnMethod": {
|
|
1672
|
+
"source": "return_method",
|
|
1673
|
+
"defaultValue": "SHIP_BACK"
|
|
1674
|
+
},
|
|
1675
|
+
"primaryReason": {
|
|
1676
|
+
"source": "return_line_items[0].reason",
|
|
1677
|
+
"resolver": "custom.mapReturnReason"
|
|
1678
|
+
},
|
|
1679
|
+
"notes": {
|
|
1680
|
+
"source": "note",
|
|
1681
|
+
"required": false
|
|
1682
|
+
},
|
|
1683
|
+
"items": {
|
|
1684
|
+
"source": "return_line_items",
|
|
1685
|
+
"isArray": true,
|
|
1686
|
+
"fields": {
|
|
1687
|
+
"lineItemId": {
|
|
1688
|
+
"source": "line_item_id",
|
|
1689
|
+
"resolver": "sdk.toString"
|
|
1690
|
+
},
|
|
1691
|
+
"productRef": {
|
|
1692
|
+
"source": "sku",
|
|
1693
|
+
"required": true
|
|
1694
|
+
},
|
|
1695
|
+
"quantity": {
|
|
1696
|
+
"source": "quantity",
|
|
1697
|
+
"resolver": "sdk.parseInt",
|
|
1698
|
+
"required": true
|
|
1699
|
+
},
|
|
1700
|
+
"returnReason": {
|
|
1701
|
+
"source": "reason",
|
|
1702
|
+
"resolver": "custom.mapReturnReason",
|
|
1703
|
+
"required": true
|
|
1704
|
+
},
|
|
1705
|
+
"customerNotes": {
|
|
1706
|
+
"source": "customer_note",
|
|
1707
|
+
"required": false
|
|
1708
|
+
},
|
|
1709
|
+
"price": {
|
|
1710
|
+
"source": "price",
|
|
1711
|
+
"resolver": "sdk.parseFloat"
|
|
1712
|
+
},
|
|
1713
|
+
"restockingFee": {
|
|
1714
|
+
"resolver": "custom.calculateRestockingFee",
|
|
1715
|
+
"comment": "Resolver-only field - receives full item as sourceData"
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
```
|
|
1722
|
+
|
|
1723
|
+
---
|
|
1724
|
+
|
|
1725
|
+
## Key Patterns Explained
|
|
1726
|
+
|
|
1727
|
+
### Pattern 1: Return Reason Mapping
|
|
1728
|
+
|
|
1729
|
+
Map e-commerce return reasons to Fluent reason codes:
|
|
1730
|
+
|
|
1731
|
+
```typescript
|
|
1732
|
+
'custom.mapReturnReason': (reason: string) => {
|
|
1733
|
+
const reasonMap: Record<string, string> = {
|
|
1734
|
+
'changed_mind': 'CUSTOMER_REMORSE',
|
|
1735
|
+
'defective': 'DEFECTIVE',
|
|
1736
|
+
'wrong_item': 'WRONG_ITEM_SHIPPED',
|
|
1737
|
+
'damaged': 'DAMAGED_IN_TRANSIT',
|
|
1738
|
+
'not_as_described': 'NOT_AS_DESCRIBED',
|
|
1739
|
+
'sizing_issues': 'SIZE_FIT_ISSUE',
|
|
1740
|
+
'late_delivery': 'LATE_DELIVERY',
|
|
1741
|
+
'other': 'OTHER',
|
|
1742
|
+
};
|
|
1743
|
+
return reasonMap[reason?.toLowerCase()] || 'OTHER';
|
|
1744
|
+
}
|
|
1745
|
+
```
|
|
1746
|
+
|
|
1747
|
+
### Pattern 2: Restocking Fee Calculation
|
|
1748
|
+
|
|
1749
|
+
Calculate restocking fees based on return reason using resolver-only field:
|
|
1750
|
+
|
|
1751
|
+
```typescript
|
|
1752
|
+
// Resolver-only field pattern: receives (value, sourceData, helpers)
|
|
1753
|
+
// value = undefined (no source specified)
|
|
1754
|
+
// sourceData = current array item containing all fields
|
|
1755
|
+
'custom.calculateRestockingFee': (value: any, sourceData: any, helpers: any) => {
|
|
1756
|
+
const feeMap: Record<string, number> = {
|
|
1757
|
+
'CUSTOMER_REMORSE': 0.15, // 15% restocking fee
|
|
1758
|
+
'SIZE_FIT_ISSUE': 0.10, // 10% restocking fee
|
|
1759
|
+
'OTHER': 0.10,
|
|
1760
|
+
'DEFECTIVE': 0, // No fee for defective items
|
|
1761
|
+
'WRONG_ITEM_SHIPPED': 0, // No fee for our errors
|
|
1762
|
+
'DAMAGED_IN_TRANSIT': 0,
|
|
1763
|
+
};
|
|
1764
|
+
|
|
1765
|
+
// Extract fields from sourceData (the current array item)
|
|
1766
|
+
const reason = sourceData?.reason || 'OTHER';
|
|
1767
|
+
const price = helpers.parseFloatSafe(sourceData?.price, 0);
|
|
1768
|
+
|
|
1769
|
+
const feePercentage = feeMap[reason] || 0;
|
|
1770
|
+
return price * feePercentage;
|
|
1771
|
+
}
|
|
1772
|
+
```
|
|
1773
|
+
|
|
1774
|
+
**Mapping Configuration:**
|
|
1775
|
+
```json
|
|
1776
|
+
{
|
|
1777
|
+
"restockingFee": {
|
|
1778
|
+
"resolver": "custom.calculateRestockingFee",
|
|
1779
|
+
"comment": "Resolver-only field - no source, extracts from sourceData"
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
```
|
|
1783
|
+
|
|
1784
|
+
### Pattern 3: Return Eligibility Check
|
|
1785
|
+
|
|
1786
|
+
Validate returns are within policy window (e.g., 30 days):
|
|
1787
|
+
|
|
1788
|
+
```typescript
|
|
1789
|
+
'custom.isReturnEligible': (orderDate: string) => {
|
|
1790
|
+
const orderTime = new Date(orderDate).getTime();
|
|
1791
|
+
const now = Date.now();
|
|
1792
|
+
const daysSinceOrder = (now - orderTime) / (1000 * 60 * 60 * 24);
|
|
1793
|
+
return daysSinceOrder <= 30; // 30-day return window
|
|
1794
|
+
}
|
|
1795
|
+
```
|
|
1796
|
+
|
|
1797
|
+
### Pattern 4: Quality Inspection States
|
|
1798
|
+
|
|
1799
|
+
Map inspection condition to disposition:
|
|
1800
|
+
|
|
1801
|
+
```typescript
|
|
1802
|
+
const condition = item.condition?.toUpperCase();
|
|
1803
|
+
let disposition = 'RESTOCK';
|
|
1804
|
+
let restockable = true;
|
|
1805
|
+
|
|
1806
|
+
switch (condition) {
|
|
1807
|
+
case 'NEW':
|
|
1808
|
+
case 'LIKE_NEW':
|
|
1809
|
+
disposition = 'RESTOCK';
|
|
1810
|
+
restockable = true;
|
|
1811
|
+
break;
|
|
1812
|
+
case 'DAMAGED':
|
|
1813
|
+
case 'DEFECTIVE':
|
|
1814
|
+
disposition = 'SCRAP';
|
|
1815
|
+
restockable = false;
|
|
1816
|
+
break;
|
|
1817
|
+
case 'OPENED':
|
|
1818
|
+
case 'USED':
|
|
1819
|
+
disposition = 'OPEN_BOX';
|
|
1820
|
+
restockable = true;
|
|
1821
|
+
break;
|
|
1822
|
+
case 'MISSING_PARTS':
|
|
1823
|
+
disposition = 'REPAIR';
|
|
1824
|
+
restockable = false;
|
|
1825
|
+
break;
|
|
1826
|
+
default:
|
|
1827
|
+
disposition = 'MANUAL_REVIEW';
|
|
1828
|
+
restockable = false;
|
|
1829
|
+
}
|
|
1830
|
+
```
|
|
1831
|
+
|
|
1832
|
+
### Pattern 5: Refund vs Exchange Decision Tree
|
|
1833
|
+
|
|
1834
|
+
Determine next action based on inspection results:
|
|
1835
|
+
|
|
1836
|
+
```typescript
|
|
1837
|
+
// Check if all items passed inspection
|
|
1838
|
+
const refundApproved = inspectionResults.every(r =>
|
|
1839
|
+
r.disposition === 'RESTOCK' || r.disposition === 'OPEN_BOX'
|
|
1840
|
+
);
|
|
1841
|
+
|
|
1842
|
+
// Determine next action
|
|
1843
|
+
const nextAction = payload.returnType === 'exchange'
|
|
1844
|
+
? 'CREATE_EXCHANGE_ORDER'
|
|
1845
|
+
: 'PROCESS_REFUND';
|
|
1846
|
+
|
|
1847
|
+
log.info('[RMA] Inspection complete', {
|
|
1848
|
+
rmaRef: rma.ref,
|
|
1849
|
+
refundApproved,
|
|
1850
|
+
nextAction,
|
|
1851
|
+
});
|
|
1852
|
+
```
|
|
1853
|
+
|
|
1854
|
+
### Pattern 6: Partial Returns
|
|
1855
|
+
|
|
1856
|
+
Handle returns of some items from an order:
|
|
1857
|
+
|
|
1858
|
+
```json
|
|
1859
|
+
{
|
|
1860
|
+
"items": {
|
|
1861
|
+
"source": "return_line_items",
|
|
1862
|
+
"isArray": true,
|
|
1863
|
+
"fields": {
|
|
1864
|
+
"lineItemId": { "source": "line_item_id" },
|
|
1865
|
+
"productRef": { "source": "sku" },
|
|
1866
|
+
"quantity": { "source": "quantity", "resolver": "sdk.parseInt" }
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
```
|
|
1871
|
+
|
|
1872
|
+
**Key**: Use `isArray: true` to map each line item individually.
|
|
1873
|
+
|
|
1874
|
+
### Pattern 7: VersoriKV RMA Lifecycle Tracking
|
|
1875
|
+
|
|
1876
|
+
Track RMA state across all stages:
|
|
1877
|
+
|
|
1878
|
+
```typescript
|
|
1879
|
+
// Store initial RMA state
|
|
1880
|
+
const rmaState = {
|
|
1881
|
+
rmaId: rma.id,
|
|
1882
|
+
rmaRef: rma.ref,
|
|
1883
|
+
orderRef: rma.orderRef,
|
|
1884
|
+
status: rma.status,
|
|
1885
|
+
stage: 'RMA_CREATED',
|
|
1886
|
+
createdOn: rma.createdOn,
|
|
1887
|
+
};
|
|
1888
|
+
await kvAdapter.set(`rma:${rma.ref}`, rmaState);
|
|
1889
|
+
|
|
1890
|
+
// Update state at each stage
|
|
1891
|
+
const existingState = await kvAdapter.get(`rma:${rma.ref}`);
|
|
1892
|
+
if (existingState) {
|
|
1893
|
+
const updatedState = {
|
|
1894
|
+
...existingState,
|
|
1895
|
+
status: 'INSPECTED',
|
|
1896
|
+
stage: 'INSPECTION_COMPLETE',
|
|
1897
|
+
inspectionDate: new Date().toISOString(),
|
|
1898
|
+
};
|
|
1899
|
+
await kvAdapter.set(`rma:${rma.ref}`, updatedState);
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
// Query state
|
|
1903
|
+
const rmaStateJson = await kvAdapter.get(`rma:${rma.ref}`);
|
|
1904
|
+
const rmaState = rmaStateJson ? JSON.parse(rmaStateJson) : null;
|
|
1905
|
+
```
|
|
1906
|
+
|
|
1907
|
+
---
|
|
1908
|
+
|
|
1909
|
+
## Testing
|
|
1910
|
+
|
|
1911
|
+
### 1. Test RMA Creation (Shopify Format)
|
|
1912
|
+
|
|
1913
|
+
```bash
|
|
1914
|
+
curl -X POST https://your-workspace.versori.run/create-rma \
|
|
1915
|
+
-H "Content-Type: application/json" \
|
|
1916
|
+
-d '{
|
|
1917
|
+
"order_id": "ORD-12345",
|
|
1918
|
+
"customer": {
|
|
1919
|
+
"id": "CUST-789",
|
|
1920
|
+
"email": "customer@example.com"
|
|
1921
|
+
},
|
|
1922
|
+
"order": {
|
|
1923
|
+
"created_at": "2025-01-15T10:00:00Z"
|
|
1924
|
+
},
|
|
1925
|
+
"return_line_items": [
|
|
1926
|
+
{
|
|
1927
|
+
"line_item_id": "LI-001",
|
|
1928
|
+
"sku": "PROD-ABC123",
|
|
1929
|
+
"quantity": 1,
|
|
1930
|
+
"reason": "wrong_item",
|
|
1931
|
+
"customer_note": "Received wrong size",
|
|
1932
|
+
"price": 99.99
|
|
1933
|
+
}
|
|
1934
|
+
],
|
|
1935
|
+
"return_method": "SHIP_BACK",
|
|
1936
|
+
"note": "Customer wants exchange for correct size"
|
|
1937
|
+
}'
|
|
1938
|
+
|
|
1939
|
+
# Expected Response:
|
|
1940
|
+
{
|
|
1941
|
+
"status": 200,
|
|
1942
|
+
"body": {
|
|
1943
|
+
"success": true,
|
|
1944
|
+
"rmaId": "RMA-123",
|
|
1945
|
+
"rmaRef": "RMA-ORD-12345-456789",
|
|
1946
|
+
"status": "PENDING_APPROVAL",
|
|
1947
|
+
"shippingLabel": {
|
|
1948
|
+
"trackingNumber": "TRK-1234567890",
|
|
1949
|
+
"labelUrl": "https://labels.example.com/RMA-ORD-12345-456789.pdf",
|
|
1950
|
+
"carrier": "USPS",
|
|
1951
|
+
"service": "Priority Mail"
|
|
1952
|
+
},
|
|
1953
|
+
"message": "RMA created successfully. Return label sent to customer.",
|
|
1954
|
+
"timestamp": "2025-10-30T12:00:00.000Z"
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
```
|
|
1958
|
+
|
|
1959
|
+
### 2. Test Shipment Tracking Update
|
|
1960
|
+
|
|
1961
|
+
```bash
|
|
1962
|
+
curl -X POST https://your-workspace.versori.run/track-return-shipment \
|
|
1963
|
+
-H "Content-Type: application/json" \
|
|
1964
|
+
-d '{
|
|
1965
|
+
"tracking_number": "TRK-1234567890",
|
|
1966
|
+
"status": "DELIVERED",
|
|
1967
|
+
"carrier": "USPS",
|
|
1968
|
+
"timestamp": "2025-01-20T14:30:00Z"
|
|
1969
|
+
}'
|
|
1970
|
+
```
|
|
1971
|
+
|
|
1972
|
+
### 3. Test Quality Inspection
|
|
1973
|
+
|
|
1974
|
+
```bash
|
|
1975
|
+
curl -X POST https://your-workspace.versori.run/quality-inspection \
|
|
1976
|
+
-H "Content-Type: application/json" \
|
|
1977
|
+
-d '{
|
|
1978
|
+
"rmaRef": "RMA-ORD-12345-456789",
|
|
1979
|
+
"inspector": "John Doe",
|
|
1980
|
+
"locationRef": "RETURNS_WAREHOUSE",
|
|
1981
|
+
"returnType": "refund",
|
|
1982
|
+
"items": [
|
|
1983
|
+
{
|
|
1984
|
+
"itemId": "RMA-ITEM-001",
|
|
1985
|
+
"productRef": "PROD-ABC123",
|
|
1986
|
+
"condition": "NEW",
|
|
1987
|
+
"notes": "Item in original packaging, tags attached",
|
|
1988
|
+
"photoUrls": ["https://photos.example.com/item1.jpg"]
|
|
1989
|
+
}
|
|
1990
|
+
]
|
|
1991
|
+
}'
|
|
1992
|
+
```
|
|
1993
|
+
|
|
1994
|
+
### 4. Test Refund Processing
|
|
1995
|
+
|
|
1996
|
+
```bash
|
|
1997
|
+
curl -X POST https://your-workspace.versori.run/process-refund-exchange \
|
|
1998
|
+
-H "Content-Type: application/json" \
|
|
1999
|
+
-d '{
|
|
2000
|
+
"rmaRef": "RMA-ORD-12345-456789",
|
|
2001
|
+
"actionType": "refund"
|
|
2002
|
+
}'
|
|
2003
|
+
```
|
|
2004
|
+
|
|
2005
|
+
### 5. Test Exchange Order Creation
|
|
2006
|
+
|
|
2007
|
+
```bash
|
|
2008
|
+
curl -X POST https://your-workspace.versori.run/process-refund-exchange \
|
|
2009
|
+
-H "Content-Type: application/json" \
|
|
2010
|
+
-d '{
|
|
2011
|
+
"rmaRef": "RMA-ORD-12345-456789",
|
|
2012
|
+
"actionType": "exchange",
|
|
2013
|
+
"exchangeItems": [
|
|
2014
|
+
{
|
|
2015
|
+
"productRef": "PROD-ABC124",
|
|
2016
|
+
"quantity": 1
|
|
2017
|
+
}
|
|
2018
|
+
]
|
|
2019
|
+
}'
|
|
2020
|
+
```
|
|
2021
|
+
|
|
2022
|
+
### 6. Query RMA Status
|
|
2023
|
+
|
|
2024
|
+
```bash
|
|
2025
|
+
curl https://your-workspace.versori.run/get-rma-status?rmaRef=RMA-ORD-12345-456789
|
|
2026
|
+
```
|
|
2027
|
+
|
|
2028
|
+
---
|
|
2029
|
+
|
|
2030
|
+
## Common Issues and Solutions
|
|
2031
|
+
|
|
2032
|
+
### Issue 1: Return Request Rejected (Ineligible)
|
|
2033
|
+
|
|
2034
|
+
**Symptoms:**
|
|
2035
|
+
- RMA creation fails with "INELIGIBLE" error
|
|
2036
|
+
- Returns outside policy window
|
|
2037
|
+
|
|
2038
|
+
**Root Cause:**
|
|
2039
|
+
- Order is older than return window (30 days)
|
|
2040
|
+
|
|
2041
|
+
**Solution:**
|
|
2042
|
+
|
|
2043
|
+
```typescript
|
|
2044
|
+
// Adjust return window in custom resolver
|
|
2045
|
+
'custom.isReturnEligible': (orderDate: string) => {
|
|
2046
|
+
const orderTime = new Date(orderDate).getTime();
|
|
2047
|
+
const now = Date.now();
|
|
2048
|
+
const daysSinceOrder = (now - orderTime) / (1000 * 60 * 60 * 24);
|
|
2049
|
+
|
|
2050
|
+
// Change to 60 days or make configurable via activation variable
|
|
2051
|
+
return daysSinceOrder <= 60;
|
|
2052
|
+
}
|
|
2053
|
+
```
|
|
2054
|
+
|
|
2055
|
+
### Issue 2: Restocking Fee Not Calculated
|
|
2056
|
+
|
|
2057
|
+
**Symptoms:**
|
|
2058
|
+
- Restocking fee is 0 for all items
|
|
2059
|
+
- Refund amount is incorrect
|
|
2060
|
+
|
|
2061
|
+
**Root Cause:**
|
|
2062
|
+
- Custom resolver not receiving price parameter
|
|
2063
|
+
- Mapping configuration missing price field
|
|
2064
|
+
|
|
2065
|
+
**Solution:**
|
|
2066
|
+
|
|
2067
|
+
```json
|
|
2068
|
+
{
|
|
2069
|
+
"items": {
|
|
2070
|
+
"source": "return_line_items",
|
|
2071
|
+
"isArray": true,
|
|
2072
|
+
"fields": {
|
|
2073
|
+
"price": {
|
|
2074
|
+
"source": "price",
|
|
2075
|
+
"resolver": "sdk.parseFloat"
|
|
2076
|
+
},
|
|
2077
|
+
"restockingFee": {
|
|
2078
|
+
"resolver": "custom.calculateRestockingFee",
|
|
2079
|
+
"comment": "Resolver-only field - receives full item as sourceData parameter"
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
```
|
|
2085
|
+
|
|
2086
|
+
### Issue 3: Inventory Not Restocked
|
|
2087
|
+
|
|
2088
|
+
**Symptoms:**
|
|
2089
|
+
- Quality inspection completes but inventory doesn't increase
|
|
2090
|
+
- Restockable items not showing in inventory
|
|
2091
|
+
|
|
2092
|
+
**Root Cause:**
|
|
2093
|
+
- Location reference incorrect
|
|
2094
|
+
- Inventory adjustment mutation failed
|
|
2095
|
+
|
|
2096
|
+
**Solution:**
|
|
2097
|
+
|
|
2098
|
+
```typescript
|
|
2099
|
+
// Verify location exists in Fluent
|
|
2100
|
+
const locationResult = await fluentClient.graphql({
|
|
2101
|
+
query: `query { location(ref: "${payload.locationRef}") { id ref } }`
|
|
2102
|
+
});
|
|
2103
|
+
|
|
2104
|
+
if (!locationResult.data?.location) {
|
|
2105
|
+
log.warn('Location not found, using default', {
|
|
2106
|
+
requestedLocation: payload.locationRef,
|
|
2107
|
+
defaultLocation: 'RETURNS_WAREHOUSE',
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// Use validated location
|
|
2112
|
+
const locationRef = locationResult.data?.location?.ref || 'RETURNS_WAREHOUSE';
|
|
2113
|
+
```
|
|
2114
|
+
|
|
2115
|
+
### Issue 4: Duplicate RMA Creation
|
|
2116
|
+
|
|
2117
|
+
**Symptoms:**
|
|
2118
|
+
- Multiple RMAs created for same return request
|
|
2119
|
+
- Duplicate tracking numbers
|
|
2120
|
+
|
|
2121
|
+
**Root Cause:**
|
|
2122
|
+
- No duplicate detection
|
|
2123
|
+
- Webhook retries creating duplicates
|
|
2124
|
+
|
|
2125
|
+
**Solution:**
|
|
2126
|
+
|
|
2127
|
+
```typescript
|
|
2128
|
+
// Check if RMA already exists before creating
|
|
2129
|
+
const existingRma = await kvAdapter.get(`rma-creation:${payload.order_id}`);
|
|
2130
|
+
|
|
2131
|
+
if (existingRma) {
|
|
2132
|
+
log.info('[RMA] RMA already exists for this order', {
|
|
2133
|
+
orderId: payload.order_id,
|
|
2134
|
+
existingRmaRef: existingRma.rmaRef,
|
|
2135
|
+
});
|
|
2136
|
+
|
|
2137
|
+
return {
|
|
2138
|
+
success: true,
|
|
2139
|
+
rmaRef: existingRma.rmaRef,
|
|
2140
|
+
duplicate: true,
|
|
2141
|
+
message: 'RMA already exists for this order',
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
// Create RMA...
|
|
2146
|
+
|
|
2147
|
+
// Store creation state
|
|
2148
|
+
const creationState = {
|
|
2149
|
+
rmaRef: rma.ref,
|
|
2150
|
+
createdAt: new Date().toISOString(),
|
|
2151
|
+
};
|
|
2152
|
+
await kvAdapter.set(`rma-creation:${payload.order_id}`, JSON.stringify(creationState));
|
|
2153
|
+
```
|
|
2154
|
+
|
|
2155
|
+
### Issue 5: Refund Amount Incorrect
|
|
2156
|
+
|
|
2157
|
+
**Symptoms:**
|
|
2158
|
+
- Refund amount doesn't match expected value
|
|
2159
|
+
- Restocking fees not deducted
|
|
2160
|
+
|
|
2161
|
+
**Root Cause:**
|
|
2162
|
+
- Calculation logic incorrect
|
|
2163
|
+
- Items missing price data
|
|
2164
|
+
|
|
2165
|
+
**Solution:**
|
|
2166
|
+
|
|
2167
|
+
```typescript
|
|
2168
|
+
// Detailed refund calculation with logging
|
|
2169
|
+
const refundDetails = rma.items.map((item: any) => {
|
|
2170
|
+
const itemTotal = item.price * item.quantity;
|
|
2171
|
+
const restockingFee = item.restockingFee || 0;
|
|
2172
|
+
const itemRefund = itemTotal - restockingFee;
|
|
2173
|
+
|
|
2174
|
+
log.debug('[RMA] Item refund calculated', {
|
|
2175
|
+
productRef: item.productRef,
|
|
2176
|
+
quantity: item.quantity,
|
|
2177
|
+
price: item.price,
|
|
2178
|
+
itemTotal,
|
|
2179
|
+
restockingFee,
|
|
2180
|
+
itemRefund,
|
|
2181
|
+
});
|
|
2182
|
+
|
|
2183
|
+
return {
|
|
2184
|
+
productRef: item.productRef,
|
|
2185
|
+
itemTotal,
|
|
2186
|
+
restockingFee,
|
|
2187
|
+
itemRefund,
|
|
2188
|
+
};
|
|
2189
|
+
});
|
|
2190
|
+
|
|
2191
|
+
const totalRefund = refundDetails.reduce((sum, detail) => sum + detail.itemRefund, 0);
|
|
2192
|
+
|
|
2193
|
+
log.info('[RMA] Total refund calculated', {
|
|
2194
|
+
totalRefund,
|
|
2195
|
+
itemCount: refundDetails.length,
|
|
2196
|
+
details: refundDetails,
|
|
2197
|
+
});
|
|
2198
|
+
```
|
|
2199
|
+
|
|
2200
|
+
---
|
|
2201
|
+
|
|
2202
|
+
## Related Guides
|
|
2203
|
+
|
|
2204
|
+
- **[Versori Webhook: XML Order Processing](./xml-order-ingestion.md)** - Similar webhook patterns
|
|
2205
|
+
- **[Versori Scheduled: CSV Inventory Sync](../workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md)** - State management patterns
|
|
2206
|
+
- **[KV State Management](../../../04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md)** - VersoriKV usage
|
|
2207
|
+
- **[Universal Mapping Guide](../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)** - Field mapping patterns
|
|
2208
|
+
- **[Custom Resolvers](../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)** - Advanced resolver patterns
|
|
2209
|
+
- **[Error Handling](../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)** - Error handling and retry logic
|
|
2210
|
+
|
|
2211
|
+
---
|
|
2212
|
+
|
|
2213
|
+
## Summary
|
|
2214
|
+
|
|
2215
|
+
**Volume:** Medium (5-10% of order volume, 50-500 returns/day)
|
|
2216
|
+
|
|
2217
|
+
**Latency:** Real-time (< 2 seconds per webhook)
|
|
2218
|
+
|
|
2219
|
+
**Complexity:** Medium (multiple stages, state tracking, conditional logic)
|
|
2220
|
+
|
|
2221
|
+
**Key Features:**
|
|
2222
|
+
- End-to-end return processing (initiation → refund/exchange)
|
|
2223
|
+
- Quality inspection workflow with condition tracking
|
|
2224
|
+
- Restocking fee calculation based on return reason
|
|
2225
|
+
- Inventory adjustments for restockable items
|
|
2226
|
+
- Exchange order creation
|
|
2227
|
+
- VersoriKV state management across RMA lifecycle
|
|
2228
|
+
- Email notifications at each stage
|
|
2229
|
+
- Support for partial returns
|
|
2230
|
+
- 30-day return policy enforcement
|
|
2231
|
+
|
|
2232
|
+
**Real-World Considerations:**
|
|
2233
|
+
- Integrate with actual payment gateway (Stripe, Adyen) for refunds
|
|
2234
|
+
- Integrate with shipping provider (ShipStation, EasyPost) for return labels
|
|
2235
|
+
- Add email/SMS notifications via SendGrid or Twilio
|
|
2236
|
+
- Implement return fraud detection
|
|
2237
|
+
- Add approval workflow for high-value returns
|
|
2238
|
+
- Track return trends and analytics
|
|
2239
|
+
- Support international returns with customs
|
|
2240
|
+
- Handle restocking for serialized/batch-tracked items
|