Browse Source

Merge branch 'master' into minor

Michael Bromley 1 year ago
parent
commit
7e3e1ab2d3
56 changed files with 2218 additions and 391 deletions
  1. 20 0
      CHANGELOG.md
  2. 1 1
      lerna.json
  3. 56 56
      package-lock.json
  4. 4 4
      packages/admin-ui-plugin/package.json
  5. 2 2
      packages/admin-ui/package.json
  6. 7 6
      packages/admin-ui/src/lib/catalog/src/catalog.module.ts
  7. 1 1
      packages/admin-ui/src/lib/core/src/common/version.ts
  8. 28 1
      packages/admin-ui/src/lib/core/src/shared/components/dropdown/dropdown-menu.component.ts
  9. 5 2
      packages/admin-ui/src/lib/core/src/shared/components/zone-selector/zone-selector.component.ts
  10. 1 1
      packages/admin-ui/src/lib/marketing/src/marketing.module.ts
  11. 21 2
      packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.ts
  12. 3 3
      packages/asset-server-plugin/package.json
  13. 3 3
      packages/cli/package.json
  14. 4 0
      packages/cli/src/commands/add/ui-extensions/add-ui-extensions.ts
  15. 1 1
      packages/common/package.json
  16. 179 23
      packages/core/e2e/custom-field-relations.e2e-spec.ts
  17. 42 0
      packages/core/e2e/graphql/admin-definitions.ts
  18. 192 0
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  19. 61 2
      packages/core/e2e/order-modification.e2e-spec.ts
  20. 249 0
      packages/core/e2e/order-multi-vendor.e2e-spec.ts
  21. 12 22
      packages/core/e2e/order-multiple-shipping.e2e-spec.ts
  22. 2 2
      packages/core/package.json
  23. 11 0
      packages/core/src/api/resolvers/entity/order-entity.resolver.ts
  24. 0 1
      packages/core/src/config/fulfillment/default-fulfillment-process.ts
  25. 12 2
      packages/core/src/service/helpers/custom-field-relation/custom-field-relation.service.ts
  26. 2 49
      packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts
  27. 45 0
      packages/core/src/service/helpers/entity-hydrator/merge-deep.spec.ts
  28. 44 0
      packages/core/src/service/helpers/entity-hydrator/merge-deep.ts
  29. 18 0
      packages/core/src/service/helpers/order-modifier/order-modifier.ts
  30. 4 3
      packages/core/src/service/helpers/order-splitter/order-splitter.ts
  31. 1 1
      packages/core/src/service/services/order.service.ts
  32. 3 3
      packages/core/src/service/services/shipping-method.service.ts
  33. 3 3
      packages/create/package.json
  34. 4 1
      packages/create/src/helpers.ts
  35. 11 0
      packages/create/templates/readme.hbs
  36. 9 9
      packages/dev-server/package.json
  37. 3 3
      packages/elasticsearch-plugin/package.json
  38. 3 3
      packages/email-plugin/package.json
  39. 3 3
      packages/harden-plugin/package.json
  40. 3 3
      packages/job-queue-plugin/package.json
  41. 11 0
      packages/payments-plugin/e2e/graphql/admin-queries.ts
  42. 65 0
      packages/payments-plugin/e2e/graphql/generated-admin-types.ts
  43. 794 63
      packages/payments-plugin/e2e/graphql/generated-shop-types.ts
  44. 26 11
      packages/payments-plugin/e2e/graphql/shop-queries.ts
  45. 19 50
      packages/payments-plugin/e2e/mollie-dev-server.ts
  46. 44 2
      packages/payments-plugin/e2e/mollie-payment.e2e-spec.ts
  47. 113 13
      packages/payments-plugin/e2e/payment-helpers.ts
  48. 4 4
      packages/payments-plugin/package.json
  49. 10 4
      packages/payments-plugin/src/mollie/mollie.controller.ts
  50. 9 4
      packages/payments-plugin/src/mollie/mollie.handler.ts
  51. 36 11
      packages/payments-plugin/src/mollie/mollie.service.ts
  52. 3 3
      packages/sentry-plugin/package.json
  53. 3 3
      packages/stellate-plugin/package.json
  54. 3 3
      packages/testing/package.json
  55. 4 4
      packages/ui-devkit/package.json
  56. 1 0
      scripts/codegen/generate-graphql-types.ts

+ 20 - 0
CHANGELOG.md

@@ -1,3 +1,23 @@
+## <small>2.2.5 (2024-06-03)</small>
+
+
+#### Fixes
+
+* **admin-ui** Add scrollbar to dropdowns that go out of the viewport ([8a78a70](https://github.com/vendure-ecommerce/vendure/commit/8a78a70))
+* **admin-ui** Fix order of bulk actions in dropdown ([d917874](https://github.com/vendure-ecommerce/vendure/commit/d917874))
+* **admin-ui** Fix stale values in zone selector component ([232ecbb](https://github.com/vendure-ecommerce/vendure/commit/232ecbb)), closes [#2867](https://github.com/vendure-ecommerce/vendure/issues/2867)
+* **admin-ui** Fix update of Channel when removing default currency/lang ([1e0c96f](https://github.com/vendure-ecommerce/vendure/commit/1e0c96f)), closes [#2825](https://github.com/vendure-ecommerce/vendure/issues/2825)
+* **cli** Install React typings when adding ui extensions ([94b6dcf](https://github.com/vendure-ecommerce/vendure/commit/94b6dcf)), closes [#2857](https://github.com/vendure-ecommerce/vendure/issues/2857)
+* **core** Add field resolver for Order.shippingLines ([84ec0aa](https://github.com/vendure-ecommerce/vendure/commit/84ec0aa)), closes [#2859](https://github.com/vendure-ecommerce/vendure/issues/2859)
+* **core** Apply price strategies when modifying order lines ([61fdbbd](https://github.com/vendure-ecommerce/vendure/commit/61fdbbd)), closes [#2870](https://github.com/vendure-ecommerce/vendure/issues/2870)
+* **core** Fix nulling of primitive custom fields when updating relation ([96f0410](https://github.com/vendure-ecommerce/vendure/commit/96f0410)), closes [#2840](https://github.com/vendure-ecommerce/vendure/issues/2840)
+* **core** Fix regression in updating of Order custom fields ([2744068](https://github.com/vendure-ecommerce/vendure/commit/2744068))
+* **core** Fix splitting of shippingLines on multivendor orders ([9112dd8](https://github.com/vendure-ecommerce/vendure/commit/9112dd8)), closes [#2859](https://github.com/vendure-ecommerce/vendure/issues/2859)
+* **core** Keep order of nested relations during hydration (#2864) (#2865) ([b325a83](https://github.com/vendure-ecommerce/vendure/commit/b325a83)), closes [#2864](https://github.com/vendure-ecommerce/vendure/issues/2864) [#2865](https://github.com/vendure-ecommerce/vendure/issues/2865)
+* **create** Move `typescript` to dev dependencies (#2816) ([b3c29e7](https://github.com/vendure-ecommerce/vendure/commit/b3c29e7)), closes [#2816](https://github.com/vendure-ecommerce/vendure/issues/2816)
+* **payments-plugin** Allow mollie orders with $0 (#2855) ([b54c210](https://github.com/vendure-ecommerce/vendure/commit/b54c210)), closes [#2855](https://github.com/vendure-ecommerce/vendure/issues/2855)
+* **payments-plugin** Events triggered by Mollie webhook include request ([4ca4593](https://github.com/vendure-ecommerce/vendure/commit/4ca4593)), closes [#2872](https://github.com/vendure-ecommerce/vendure/issues/2872)
+
 ## <small>2.2.4 (2024-05-08)</small>
 
 

+ 1 - 1
lerna.json

@@ -1,6 +1,6 @@
 {
     "packages": ["packages/*"],
-    "version": "2.2.4",
+    "version": "2.2.5",
     "npmClient": "npm",
     "command": {
         "version": {

+ 56 - 56
package-lock.json

@@ -31735,7 +31735,7 @@
         },
         "packages/admin-ui": {
             "name": "@vendure/admin-ui",
-            "version": "2.2.4",
+            "version": "2.2.5",
             "license": "MIT",
             "dependencies": {
                 "@angular/animations": "^17.2.4",
@@ -31758,7 +31758,7 @@
                 "@ng-select/ng-select": "^12.0.7",
                 "@ngx-translate/core": "^15.0.0",
                 "@ngx-translate/http-loader": "^8.0.0",
-                "@vendure/common": "^2.2.4",
+                "@vendure/common": "^2.2.5",
                 "@webcomponents/custom-elements": "^1.6.0",
                 "apollo-angular": "^6.0.0",
                 "apollo-upload-client": "^18.0.1",
@@ -31829,7 +31829,7 @@
         },
         "packages/admin-ui-plugin": {
             "name": "@vendure/admin-ui-plugin",
-            "version": "2.2.4",
+            "version": "2.2.5",
             "license": "MIT",
             "dependencies": {
                 "date-fns": "^2.30.0",
@@ -31838,9 +31838,9 @@
             "devDependencies": {
                 "@types/express": "^4.17.21",
                 "@types/fs-extra": "^11.0.4",
-                "@vendure/admin-ui": "^2.2.4",
-                "@vendure/common": "^2.2.4",
-                "@vendure/core": "^2.2.4",
+                "@vendure/admin-ui": "^2.2.5",
+                "@vendure/common": "^2.2.5",
+                "@vendure/core": "^2.2.5",
                 "express": "^4.18.3",
                 "rimraf": "^5.0.5",
                 "typescript": "5.4.2"
@@ -31871,7 +31871,7 @@
         },
         "packages/asset-server-plugin": {
             "name": "@vendure/asset-server-plugin",
-            "version": "2.2.4",
+            "version": "2.2.5",
             "license": "MIT",
             "dependencies": {
                 "file-type": "^19.0.0",
@@ -31884,8 +31884,8 @@
                 "@types/express": "^4.17.21",
                 "@types/fs-extra": "^11.0.4",
                 "@types/node-fetch": "^2.6.11",
-                "@vendure/common": "^2.2.4",
-                "@vendure/core": "^2.2.4",
+                "@vendure/common": "^2.2.5",
+                "@vendure/core": "^2.2.5",
                 "express": "^4.18.3",
                 "node-fetch": "^2.7.0",
                 "rimraf": "^5.0.5",
@@ -31897,11 +31897,11 @@
         },
         "packages/cli": {
             "name": "@vendure/cli",
-            "version": "2.2.4",
+            "version": "2.2.5",
             "license": "MIT",
             "dependencies": {
                 "@clack/prompts": "^0.7.0",
-                "@vendure/common": "^2.2.4",
+                "@vendure/common": "^2.2.5",
                 "change-case": "^4.1.2",
                 "commander": "^11.0.0",
                 "dotenv": "^16.4.5",
@@ -31915,7 +31915,7 @@
                 "vendure": "dist/cli.js"
             },
             "devDependencies": {
-                "@vendure/core": "^2.2.4",
+                "@vendure/core": "^2.2.5",
                 "typescript": "5.3.3"
             },
             "funding": {
@@ -31952,7 +31952,7 @@
         },
         "packages/common": {
             "name": "@vendure/common",
-            "version": "2.2.4",
+            "version": "2.2.5",
             "license": "MIT",
             "devDependencies": {
                 "rimraf": "^5.0.5",
@@ -31964,7 +31964,7 @@
         },
         "packages/core": {
             "name": "@vendure/core",
-            "version": "2.2.4",
+            "version": "2.2.5",
             "license": "MIT",
             "dependencies": {
                 "@apollo/server": "^4.10.1",
@@ -31978,7 +31978,7 @@
                 "@nestjs/testing": "10.3.3",
                 "@nestjs/typeorm": "10.0.2",
                 "@types/fs-extra": "^9.0.1",
-                "@vendure/common": "^2.2.4",
+                "@vendure/common": "^2.2.5",
                 "bcrypt": "^5.1.1",
                 "body-parser": "^1.20.2",
                 "cookie-session": "^2.1.0",
@@ -32114,11 +32114,11 @@
         },
         "packages/create": {
             "name": "@vendure/create",
-            "version": "2.2.4",
+            "version": "2.2.5",
             "license": "MIT",
             "dependencies": {
                 "@clack/prompts": "^0.7.0",
-                "@vendure/common": "^2.2.4",
+                "@vendure/common": "^2.2.5",
                 "commander": "^11.0.0",
                 "cross-spawn": "^7.0.3",
                 "detect-port": "^1.5.1",
@@ -32137,7 +32137,7 @@
                 "@types/fs-extra": "^11.0.4",
                 "@types/handlebars": "^4.1.0",
                 "@types/semver": "^7.5.8",
-                "@vendure/core": "^2.2.4",
+                "@vendure/core": "^2.2.5",
                 "rimraf": "^5.0.5",
                 "ts-node": "^10.9.2",
                 "typescript": "5.3.3"
@@ -32154,21 +32154,21 @@
             }
         },
         "packages/dev-server": {
-            "version": "2.2.4",
+            "version": "2.2.5",
             "license": "MIT",
             "dependencies": {
                 "@nestjs/axios": "^3.0.2",
-                "@vendure/admin-ui-plugin": "^2.2.4",
-                "@vendure/asset-server-plugin": "^2.2.4",
-                "@vendure/common": "^2.2.4",
-                "@vendure/core": "^2.2.4",
-                "@vendure/elasticsearch-plugin": "^2.2.4",
-                "@vendure/email-plugin": "^2.2.4",
+                "@vendure/admin-ui-plugin": "^2.2.5",
+                "@vendure/asset-server-plugin": "^2.2.5",
+                "@vendure/common": "^2.2.5",
+                "@vendure/core": "^2.2.5",
+                "@vendure/elasticsearch-plugin": "^2.2.5",
+                "@vendure/email-plugin": "^2.2.5",
                 "typescript": "5.3.3"
             },
             "devDependencies": {
-                "@vendure/testing": "^2.2.4",
-                "@vendure/ui-devkit": "^2.2.4",
+                "@vendure/testing": "^2.2.5",
+                "@vendure/ui-devkit": "^2.2.5",
                 "commander": "^12.0.0",
                 "concurrently": "^8.2.2",
                 "csv-stringify": "^6.4.6",
@@ -32186,7 +32186,7 @@
         },
         "packages/elasticsearch-plugin": {
             "name": "@vendure/elasticsearch-plugin",
-            "version": "2.2.4",
+            "version": "2.2.5",
             "license": "MIT",
             "dependencies": {
                 "@elastic/elasticsearch": "~7.9.1",
@@ -32194,8 +32194,8 @@
                 "fast-deep-equal": "^3.1.3"
             },
             "devDependencies": {
-                "@vendure/common": "^2.2.4",
-                "@vendure/core": "^2.2.4",
+                "@vendure/common": "^2.2.5",
+                "@vendure/core": "^2.2.5",
                 "rimraf": "^5.0.5",
                 "typescript": "5.3.3"
             },
@@ -32205,7 +32205,7 @@
         },
         "packages/email-plugin": {
             "name": "@vendure/email-plugin",
-            "version": "2.2.4",
+            "version": "2.2.5",
             "license": "MIT",
             "dependencies": {
                 "@types/nodemailer": "^6.4.9",
@@ -32221,8 +32221,8 @@
                 "@types/express": "^4.17.21",
                 "@types/fs-extra": "^11.0.4",
                 "@types/mjml": "^4.7.4",
-                "@vendure/common": "^2.2.4",
-                "@vendure/core": "^2.2.4",
+                "@vendure/common": "^2.2.5",
+                "@vendure/core": "^2.2.5",
                 "rimraf": "^5.0.5",
                 "typescript": "5.3.3"
             },
@@ -32232,14 +32232,14 @@
         },
         "packages/harden-plugin": {
             "name": "@vendure/harden-plugin",
-            "version": "2.2.4",
+            "version": "2.2.5",
             "license": "MIT",
             "dependencies": {
                 "graphql-query-complexity": "^0.12.0"
             },
             "devDependencies": {
-                "@vendure/common": "^2.2.4",
-                "@vendure/core": "^2.2.4"
+                "@vendure/common": "^2.2.5",
+                "@vendure/core": "^2.2.5"
             },
             "funding": {
                 "url": "https://github.com/sponsors/michaelbromley"
@@ -32247,12 +32247,12 @@
         },
         "packages/job-queue-plugin": {
             "name": "@vendure/job-queue-plugin",
-            "version": "2.2.4",
+            "version": "2.2.5",
             "license": "MIT",
             "devDependencies": {
                 "@google-cloud/pubsub": "^2.8.0",
-                "@vendure/common": "^2.2.4",
-                "@vendure/core": "^2.2.4",
+                "@vendure/common": "^2.2.5",
+                "@vendure/core": "^2.2.5",
                 "bullmq": "^5.4.2",
                 "ioredis": "^5.3.2",
                 "rimraf": "^5.0.5",
@@ -32264,7 +32264,7 @@
         },
         "packages/payments-plugin": {
             "name": "@vendure/payments-plugin",
-            "version": "2.2.4",
+            "version": "2.2.5",
             "license": "MIT",
             "dependencies": {
                 "currency.js": "2.0.4"
@@ -32273,9 +32273,9 @@
                 "@mollie/api-client": "^3.7.0",
                 "@types/braintree": "^3.3.11",
                 "@types/localtunnel": "2.0.4",
-                "@vendure/common": "^2.2.4",
-                "@vendure/core": "^2.2.4",
-                "@vendure/testing": "^2.2.4",
+                "@vendure/common": "^2.2.5",
+                "@vendure/core": "^2.2.5",
+                "@vendure/testing": "^2.2.5",
                 "braintree": "^3.22.0",
                 "localtunnel": "2.0.2",
                 "nock": "^13.1.4",
@@ -32331,12 +32331,12 @@
         },
         "packages/sentry-plugin": {
             "name": "@vendure/sentry-plugin",
-            "version": "2.2.4",
+            "version": "2.2.5",
             "license": "MIT",
             "devDependencies": {
                 "@sentry/node": "^7.106.1",
-                "@vendure/common": "^2.2.4",
-                "@vendure/core": "^2.2.4"
+                "@vendure/common": "^2.2.5",
+                "@vendure/core": "^2.2.5"
             },
             "funding": {
                 "url": "https://github.com/sponsors/michaelbromley"
@@ -32347,14 +32347,14 @@
         },
         "packages/stellate-plugin": {
             "name": "@vendure/stellate-plugin",
-            "version": "2.2.4",
+            "version": "2.2.5",
             "license": "MIT",
             "dependencies": {
                 "node-fetch": "^2.7.0"
             },
             "devDependencies": {
-                "@vendure/common": "^2.2.4",
-                "@vendure/core": "^2.2.4"
+                "@vendure/common": "^2.2.5",
+                "@vendure/core": "^2.2.5"
             },
             "funding": {
                 "url": "https://github.com/sponsors/michaelbromley"
@@ -32362,11 +32362,11 @@
         },
         "packages/testing": {
             "name": "@vendure/testing",
-            "version": "2.2.4",
+            "version": "2.2.5",
             "license": "MIT",
             "dependencies": {
                 "@graphql-typed-document-node/core": "^3.2.0",
-                "@vendure/common": "^2.2.4",
+                "@vendure/common": "^2.2.5",
                 "faker": "^4.1.0",
                 "form-data": "^4.0.0",
                 "graphql": "16.8.1",
@@ -32379,7 +32379,7 @@
                 "@types/mysql": "^2.15.26",
                 "@types/node-fetch": "^2.6.4",
                 "@types/pg": "^8.11.2",
-                "@vendure/core": "^2.2.4",
+                "@vendure/core": "^2.2.5",
                 "mysql": "^2.18.1",
                 "pg": "^8.11.3",
                 "rimraf": "^5.0.5",
@@ -32395,15 +32395,15 @@
         },
         "packages/ui-devkit": {
             "name": "@vendure/ui-devkit",
-            "version": "2.2.4",
+            "version": "2.2.5",
             "license": "MIT",
             "dependencies": {
                 "@angular-devkit/build-angular": "^17.2.3",
                 "@angular/cli": "^17.2.3",
                 "@angular/compiler": "^17.2.4",
                 "@angular/compiler-cli": "^17.2.4",
-                "@vendure/admin-ui": "^2.2.4",
-                "@vendure/common": "^2.2.4",
+                "@vendure/admin-ui": "^2.2.5",
+                "@vendure/common": "^2.2.5",
                 "chalk": "^4.1.0",
                 "chokidar": "^3.6.0",
                 "fs-extra": "^11.2.0",
@@ -32414,7 +32414,7 @@
                 "@rollup/plugin-node-resolve": "^15.2.3",
                 "@rollup/plugin-terser": "^0.4.4",
                 "@types/fs-extra": "^11.0.4",
-                "@vendure/core": "^2.2.4",
+                "@vendure/core": "^2.2.5",
                 "react": "^18.2.0",
                 "react-dom": "^18.2.0",
                 "rimraf": "^5.0.5",

+ 4 - 4
packages/admin-ui-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/admin-ui-plugin",
-    "version": "2.2.4",
+    "version": "2.2.5",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
     "files": [
@@ -21,9 +21,9 @@
     "devDependencies": {
         "@types/express": "^4.17.21",
         "@types/fs-extra": "^11.0.4",
-        "@vendure/admin-ui": "^2.2.4",
-        "@vendure/common": "^2.2.4",
-        "@vendure/core": "^2.2.4",
+        "@vendure/admin-ui": "^2.2.5",
+        "@vendure/common": "^2.2.5",
+        "@vendure/core": "^2.2.5",
         "express": "^4.18.3",
         "rimraf": "^5.0.5",
         "typescript": "5.4.2"

+ 2 - 2
packages/admin-ui/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/admin-ui",
-    "version": "2.2.4",
+    "version": "2.2.5",
     "license": "MIT",
     "scripts": {
         "ng": "ng",
@@ -49,7 +49,7 @@
         "@ng-select/ng-select": "^12.0.7",
         "@ngx-translate/core": "^15.0.0",
         "@ngx-translate/http-loader": "^8.0.0",
-        "@vendure/common": "^2.2.4",
+        "@vendure/common": "^2.2.5",
         "@webcomponents/custom-elements": "^1.6.0",
         "apollo-angular": "^6.0.0",
         "apollo-upload-client": "^18.0.1",

+ 7 - 6
packages/admin-ui/src/lib/catalog/src/catalog.module.ts

@@ -132,23 +132,24 @@ export class CatalogModule {
         }
         bulkActionRegistryService.registerBulkAction(assignFacetValuesToProductsBulkAction);
         bulkActionRegistryService.registerBulkAction(assignProductsToChannelBulkAction);
-        bulkActionRegistryService.registerBulkAction(assignProductVariantsToChannelBulkAction);
-        bulkActionRegistryService.registerBulkAction(removeProductsFromChannelBulkAction);
-        bulkActionRegistryService.registerBulkAction(removeProductVariantsFromChannelBulkAction);
         bulkActionRegistryService.registerBulkAction(duplicateProductsBulkAction);
+        bulkActionRegistryService.registerBulkAction(removeProductsFromChannelBulkAction);
         bulkActionRegistryService.registerBulkAction(deleteProductsBulkAction);
-        bulkActionRegistryService.registerBulkAction(deleteProductVariantsBulkAction);
+
         bulkActionRegistryService.registerBulkAction(assignFacetValuesToProductVariantsBulkAction);
+        bulkActionRegistryService.registerBulkAction(assignProductVariantsToChannelBulkAction);
+        bulkActionRegistryService.registerBulkAction(removeProductVariantsFromChannelBulkAction);
+        bulkActionRegistryService.registerBulkAction(deleteProductVariantsBulkAction);
 
         bulkActionRegistryService.registerBulkAction(assignFacetsToChannelBulkAction);
-        bulkActionRegistryService.registerBulkAction(removeFacetsFromChannelBulkAction);
         bulkActionRegistryService.registerBulkAction(duplicateFacetsBulkAction);
+        bulkActionRegistryService.registerBulkAction(removeFacetsFromChannelBulkAction);
         bulkActionRegistryService.registerBulkAction(deleteFacetsBulkAction);
 
         bulkActionRegistryService.registerBulkAction(moveCollectionsBulkAction);
         bulkActionRegistryService.registerBulkAction(assignCollectionsToChannelBulkAction);
-        bulkActionRegistryService.registerBulkAction(removeCollectionsFromChannelBulkAction);
         bulkActionRegistryService.registerBulkAction(duplicateCollectionsBulkAction);
+        bulkActionRegistryService.registerBulkAction(removeCollectionsFromChannelBulkAction);
         bulkActionRegistryService.registerBulkAction(deleteCollectionsBulkAction);
 
         pageService.registerPageTab({

+ 1 - 1
packages/admin-ui/src/lib/core/src/common/version.ts

@@ -1,2 +1,2 @@
 // Auto-generated by the set-version.js script.
-export const ADMIN_UI_VERSION = '2.2.4';
+export const ADMIN_UI_VERSION = '2.2.5';

+ 28 - 1
packages/admin-ui/src/lib/core/src/shared/components/dropdown/dropdown-menu.component.ts

@@ -3,6 +3,7 @@ import { TemplatePortal } from '@angular/cdk/portal';
 import {
     AfterViewInit,
     ChangeDetectionStrategy,
+    ChangeDetectorRef,
     Component,
     HostListener,
     Input,
@@ -37,7 +38,7 @@ export type DropdownPosition = 'top-left' | 'top-right' | 'bottom-left' | 'botto
         <ng-template #menu>
             <div [dir]="direction$ | async">
                 <div class="dropdown open">
-                    <div class="dropdown-menu" [ngClass]="customClasses">
+                    <div class="dropdown-menu" [ngClass]="customClasses" [style.maxHeight.px]="maxHeight">
                         <div
                             class="dropdown-content-wrapper"
                             [cdkTrapFocus]="true"
@@ -62,6 +63,27 @@ export class DropdownMenuComponent implements AfterViewInit, OnInit, OnDestroy {
     private menuPortal: TemplatePortal;
     private overlayRef: OverlayRef;
     private backdropClickSub: Subscription;
+    protected maxHeight: number | undefined;
+
+    private resizeObserver = new ResizeObserver(entries => {
+        const margin = 12;
+        for (const entry of entries) {
+            const contentWrapper = entry.target.querySelector('.dropdown-content-wrapper');
+            if (contentWrapper) {
+                const { bottom, top } = contentWrapper?.getBoundingClientRect();
+                if (bottom > window.innerHeight - margin) {
+                    // dropdown is going off the bottom of the screen
+                    this.maxHeight = window.innerHeight - top - margin;
+                    this.changeDetector.markForCheck();
+                }
+                if (top < margin) {
+                    // dropdown is going off the top of the screen
+                    this.maxHeight = bottom - margin;
+                    this.changeDetector.markForCheck();
+                }
+            }
+        }
+    });
 
     @HostListener('window:keydown.escape', ['$event'])
     onEscapeKeydown(event: KeyboardEvent) {
@@ -103,6 +125,7 @@ export class DropdownMenuComponent implements AfterViewInit, OnInit, OnDestroy {
         private viewContainerRef: ViewContainerRef,
         private dropdown: DropdownComponent,
         private localizationService: LocalizationService,
+        private changeDetector: ChangeDetectorRef,
     ) {}
 
     ngOnInit(): void {
@@ -111,8 +134,11 @@ export class DropdownMenuComponent implements AfterViewInit, OnInit, OnDestroy {
         this.dropdown.onOpenChange(isOpen => {
             if (isOpen) {
                 this.overlayRef.attach(this.menuPortal);
+                this.resizeObserver.observe(this.overlayRef.overlayElement);
             } else {
                 this.overlayRef.detach();
+                this.resizeObserver.unobserve(this.overlayRef.overlayElement);
+                this.maxHeight = undefined;
             }
         });
     }
@@ -124,6 +150,7 @@ export class DropdownMenuComponent implements AfterViewInit, OnInit, OnDestroy {
             positionStrategy: this.getPositionStrategy(),
             maxHeight: '70vh',
         });
+
         this.menuPortal = new TemplatePortal(this.menuTemplate, this.viewContainerRef);
         this.backdropClickSub = this.overlayRef.backdropClick().subscribe(() => {
             this.dropdown.toggleOpen();

+ 5 - 2
packages/admin-ui/src/lib/core/src/shared/components/zone-selector/zone-selector.component.ts

@@ -63,10 +63,13 @@ export class ZoneSelectorComponent implements ControlValueAccessor {
     disabled = false;
     value: string | Zone;
     zones$ = this.dataService
-        .query(GetZoneSelectorListDocument, { options: { take: 999 } }, 'cache-first')
+        .query(GetZoneSelectorListDocument, { options: { take: 999 } })
         .mapSingle(result => result.zones.items);
 
-    constructor(private dataService: DataService, private changeDetectorRef: ChangeDetectorRef) {}
+    constructor(
+        private dataService: DataService,
+        private changeDetectorRef: ChangeDetectorRef,
+    ) {}
 
     onChange(selected: Zone) {
         if (this.readonly) {

+ 1 - 1
packages/admin-ui/src/lib/marketing/src/marketing.module.ts

@@ -40,8 +40,8 @@ export class MarketingModule {
             return;
         }
         bulkActionRegistryService.registerBulkAction(assignPromotionsToChannelBulkAction);
-        bulkActionRegistryService.registerBulkAction(removePromotionsFromChannelBulkAction);
         bulkActionRegistryService.registerBulkAction(duplicatePromotionsBulkAction);
+        bulkActionRegistryService.registerBulkAction(removePromotionsFromChannelBulkAction);
         bulkActionRegistryService.registerBulkAction(deletePromotionsBulkAction);
 
         pageService.registerPageTab({

+ 21 - 2
packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.ts

@@ -20,7 +20,7 @@ import {
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { gql } from 'apollo-angular';
 import { Observable } from 'rxjs';
-import { map, mergeMap, take } from 'rxjs/operators';
+import { map, mergeMap, take, takeUntil } from 'rxjs/operators';
 
 export const GET_CHANNEL_DETAIL = gql`
     query GetChannelDetail($id: ID!) {
@@ -74,10 +74,29 @@ export class ChannelDetailComponent
 
     ngOnInit() {
         this.init();
-        // this.zones$ = this.dataService.settings.getZones({ take: 100 }).mapSingle(data => data.zones.items);
         // TODO: make this lazy-loaded autocomplete
         this.sellers$ = this.dataService.settings.getSellerList().mapSingle(data => data.sellers.items);
         this.availableLanguageCodes$ = this.serverConfigService.getAvailableLanguages();
+        this.detailForm.controls.availableCurrencyCodes.valueChanges
+            .pipe(takeUntil(this.destroy$))
+            .subscribe(value => {
+                if (value) {
+                    const defaultCurrencyCode = this.detailForm.controls.defaultCurrencyCode.value;
+                    if (defaultCurrencyCode && !value.includes(defaultCurrencyCode)) {
+                        this.detailForm.controls.defaultCurrencyCode.setValue(value[0] as CurrencyCode);
+                    }
+                }
+            });
+        this.detailForm.controls.availableLanguageCodes.valueChanges
+            .pipe(takeUntil(this.destroy$))
+            .subscribe(value => {
+                if (value) {
+                    const defaultLanguageCode = this.detailForm.controls.defaultLanguageCode.value;
+                    if (defaultLanguageCode && !value.includes(defaultLanguageCode)) {
+                        this.detailForm.controls.defaultLanguageCode.setValue(value[0] as LanguageCode);
+                    }
+                }
+            });
     }
 
     ngOnDestroy() {

+ 3 - 3
packages/asset-server-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/asset-server-plugin",
-    "version": "2.2.4",
+    "version": "2.2.5",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
     "files": [
@@ -26,8 +26,8 @@
         "@types/express": "^4.17.21",
         "@types/fs-extra": "^11.0.4",
         "@types/node-fetch": "^2.6.11",
-        "@vendure/common": "^2.2.4",
-        "@vendure/core": "^2.2.4",
+        "@vendure/common": "^2.2.5",
+        "@vendure/core": "^2.2.5",
         "express": "^4.18.3",
         "node-fetch": "^2.7.0",
         "rimraf": "^5.0.5",

+ 3 - 3
packages/cli/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/cli",
-    "version": "2.2.4",
+    "version": "2.2.5",
     "description": "A modern, headless ecommerce framework",
     "repository": {
         "type": "git",
@@ -35,7 +35,7 @@
     ],
     "dependencies": {
         "@clack/prompts": "^0.7.0",
-        "@vendure/common": "^2.2.4",
+        "@vendure/common": "^2.2.5",
         "change-case": "^4.1.2",
         "commander": "^11.0.0",
         "dotenv": "^16.4.5",
@@ -46,7 +46,7 @@
         "tsconfig-paths": "^4.2.0"
     },
     "devDependencies": {
-        "@vendure/core": "^2.2.4",
+        "@vendure/core": "^2.2.5",
         "typescript": "5.3.3"
     }
 }

+ 4 - 0
packages/cli/src/commands/add/ui-extensions/add-ui-extensions.ts

@@ -55,6 +55,10 @@ async function addUiExtensions(options?: AddUiExtensionsOptions): Promise<CliCom
                 isDevDependency: true,
                 version,
             },
+            {
+                pkg: '@types/react',
+                isDevDependency: true,
+            },
         ]);
     } catch (e: any) {
         log.error(`Failed to install dependencies: ${e.message as string}.`);

+ 1 - 1
packages/common/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/common",
-    "version": "2.2.4",
+    "version": "2.2.5",
     "main": "index.js",
     "license": "MIT",
     "scripts": {

+ 179 - 23
packages/core/e2e/custom-field-relations.e2e-spec.ts

@@ -40,17 +40,7 @@ import { AddItemToOrderMutationVariables } from './graphql/generated-e2e-shop-ty
 import { ADD_ITEM_TO_ORDER } from './graphql/shop-definitions';
 import { sortById } from './utils/test-order-utils';
 
-// From https://github.com/microsoft/TypeScript/issues/13298#issuecomment-654906323
-// to ensure that we _always_ test all entities which support custom fields
-type ValueOf<T> = T[keyof T];
-type NonEmptyArray<T> = [T, ...T[]];
-type MustInclude<T, U extends T[]> = [T] extends [ValueOf<U>] ? U : never;
-const enumerate =
-    <T>() =>
-    <U extends NonEmptyArray<T>>(...elements: MustInclude<T, U>) =>
-        elements;
-
-const entitiesWithCustomFields = enumerate<keyof CustomFields>()(
+const entitiesWithCustomFields: Array<keyof CustomFields> = [
     'Address',
     'Administrator',
     'Asset',
@@ -77,11 +67,12 @@ const entitiesWithCustomFields = enumerate<keyof CustomFields>()(
     'TaxRate',
     'User',
     'Zone',
-);
+];
 
 const customFieldConfig: CustomFields = {};
 for (const entity of entitiesWithCustomFields) {
     customFieldConfig[entity] = [
+        { name: 'primitive', type: 'string', list: false, defaultValue: 'test' },
         { name: 'single', type: 'relation', entity: Asset, graphQLType: 'Asset', list: false },
         { name: 'multi', type: 'relation', entity: Asset, graphQLType: 'Asset', list: true },
     ];
@@ -154,7 +145,7 @@ describe('Custom field relations', () => {
             }
         `);
 
-        const single = globalSettings.serverConfig.customFieldConfig.Customer[0];
+        const single = globalSettings.serverConfig.customFieldConfig.Customer[1];
         expect(single.entity).toBe('Asset');
         expect(single.scalarFields).toEqual([
             'id',
@@ -388,6 +379,7 @@ describe('Custom field relations', () => {
 
         const customFieldsSelection = `
             customFields {
+                primitive
                 single {
                     id
                 }
@@ -508,6 +500,25 @@ describe('Custom field relations', () => {
 
                 assertCustomFieldIds(updateCollection.customFields, 'T_2', ['T_3', 'T_4']);
             });
+
+            // https://github.com/vendure-ecommerce/vendure/issues/2840
+            it('updating custom field relation on Collection does not delete primitive values', async () => {
+                const { updateCollection } = await adminClient.query(gql`
+                    mutation {
+                        updateCollection(
+                            input: {
+                                id: "${collectionId}"
+                                customFields: { singleId: "T_3" }
+                            }
+                        ) {
+                            id
+                            ${customFieldsSelection}
+                        }
+                    }
+                `);
+                expect(updateCollection.customFields.single).toEqual({ id: 'T_3' });
+                expect(updateCollection.customFields.primitive).toBe('test');
+            });
         });
 
         describe('Customer entity', () => {
@@ -606,6 +617,25 @@ describe('Custom field relations', () => {
                 `);
                 assertCustomFieldIds(updateFacet.customFields, 'T_2', ['T_3', 'T_4']);
             });
+
+            // https://github.com/vendure-ecommerce/vendure/issues/2840
+            it('updating custom field relation on Facet does not delete primitive values', async () => {
+                const { updateFacet } = await adminClient.query(gql`
+                    mutation {
+                        updateFacet(
+                            input: {
+                                id: "${facetId}"
+                                customFields: { singleId: "T_3" }
+                            }
+                        ) {
+                            id
+                            ${customFieldsSelection}
+                        }
+                    }
+                `);
+                expect(updateFacet.customFields.single).toEqual({ id: 'T_3' });
+                expect(updateFacet.customFields.primitive).toBe('test');
+            });
         });
 
         describe('FacetValue entity', () => {
@@ -647,11 +677,26 @@ describe('Custom field relations', () => {
                 `);
                 assertCustomFieldIds(updateFacetValues[0].customFields, 'T_2', ['T_3', 'T_4']);
             });
-        });
 
-        // describe('Fulfillment entity', () => {
-        //     // Currently no GraphQL API to set customFields on fulfillments
-        // });
+            // https://github.com/vendure-ecommerce/vendure/issues/2840
+            it('updating custom field relation on FacetValue does not delete primitive values', async () => {
+                const { updateFacetValues } = await adminClient.query(gql`
+                    mutation {
+                        updateFacetValues(
+                            input: {
+                                id: "${facetValueId}"
+                                customFields: { singleId: "T_3" }
+                            }
+                        ) {
+                            id
+                            ${customFieldsSelection}
+                        }
+                    }
+                `);
+                expect(updateFacetValues[0].customFields.single).toEqual({ id: 'T_3' });
+                expect(updateFacetValues[0].customFields.primitive).toBe('test');
+            });
+        });
 
         describe('GlobalSettings entity', () => {
             it('admin updateGlobalSettings', async () => {
@@ -807,6 +852,25 @@ describe('Custom field relations', () => {
                 assertCustomFieldIds(updateProduct.customFields, 'T_2', ['T_3', 'T_4']);
             });
 
+            // https://github.com/vendure-ecommerce/vendure/issues/2840
+            it('updating custom field relation on Product does not delete primitive values', async () => {
+                const { updateProduct } = await adminClient.query(gql`
+                    mutation {
+                        updateProduct(
+                            input: {
+                                id: "${productId}"
+                                customFields: { singleId: "T_3" }
+                            }
+                        ) {
+                            id
+                            ${customFieldsSelection}
+                        }
+                    }
+                `);
+                expect(updateProduct.customFields.single).toEqual({ id: 'T_3' });
+                expect(updateProduct.customFields.primitive).toBe('test');
+            });
+
             let productVariantId: string;
             it('admin createProductVariant', async () => {
                 const { createProductVariants } = await adminClient.query(gql`
@@ -846,6 +910,25 @@ describe('Custom field relations', () => {
                 assertCustomFieldIds(updateProductVariants[0].customFields, 'T_2', ['T_3', 'T_4']);
             });
 
+            // https://github.com/vendure-ecommerce/vendure/issues/2840
+            it('updating custom field relation on ProductVariant does not delete primitive values', async () => {
+                const { updateProductVariants } = await adminClient.query(gql`
+                    mutation {
+                        updateProductVariants(
+                            input: [{
+                                id: "${productVariantId}"
+                                customFields: { singleId: "T_3" }
+                            }]
+                        ) {
+                            id
+                            ${customFieldsSelection}
+                        }
+                    }
+                `);
+                expect(updateProductVariants[0].customFields.single).toEqual({ id: 'T_3' });
+                expect(updateProductVariants[0].customFields.primitive).toBe('test');
+            });
+
             describe('issue 1664', () => {
                 // https://github.com/vendure-ecommerce/vendure/issues/1664
                 it('successfully gets product by id with eager-loading custom field relation', async () => {
@@ -1013,6 +1096,25 @@ describe('Custom field relations', () => {
                 assertCustomFieldIds(updateProductOptionGroup.customFields, 'T_2', ['T_3', 'T_4']);
             });
 
+            // https://github.com/vendure-ecommerce/vendure/issues/2840
+            it('updating custom field relation on ProductOptionGroup does not delete primitive values', async () => {
+                const { updateProductOptionGroup } = await adminClient.query(gql`
+                    mutation {
+                        updateProductOptionGroup(
+                            input: {
+                                id: "${productOptionGroupId}"
+                                customFields: { singleId: "T_3" }
+                            }
+                        ) {
+                            id
+                            ${customFieldsSelection}
+                        }
+                    }
+                `);
+                expect(updateProductOptionGroup.customFields.single).toEqual({ id: 'T_3' });
+                expect(updateProductOptionGroup.customFields.primitive).toBe('test');
+            });
+
             let productOptionId: string;
             it('admin createProductOption', async () => {
                 const { createProductOption } = await adminClient.query(gql`
@@ -1051,11 +1153,26 @@ describe('Custom field relations', () => {
                 `);
                 assertCustomFieldIds(updateProductOption.customFields, 'T_2', ['T_3', 'T_4']);
             });
-        });
 
-        // describe('User entity', () => {
-        //     // Currently no GraphQL API to set User custom fields
-        // });
+            // https://github.com/vendure-ecommerce/vendure/issues/2840
+            it('updating custom field relation on ProductOption does not delete primitive values', async () => {
+                const { updateProductOption } = await adminClient.query(gql`
+                    mutation {
+                        updateProductOption(
+                            input: {
+                                id: "${productOptionId}"
+                                customFields: { singleId: "T_3" }
+                            }
+                        ) {
+                            id
+                            ${customFieldsSelection}
+                        }
+                    }
+                `);
+                expect(updateProductOption.customFields.single).toEqual({ id: 'T_3' });
+                expect(updateProductOption.customFields.primitive).toBe('test');
+            });
+        });
 
         describe('ShippingMethod entity', () => {
             let shippingMethodId: string;
@@ -1112,6 +1229,26 @@ describe('Custom field relations', () => {
                 assertCustomFieldIds(updateShippingMethod.customFields, 'T_2', ['T_3', 'T_4']);
             });
 
+            // https://github.com/vendure-ecommerce/vendure/issues/2840
+            it('updating custom field relation on ShippingMethod does not delete primitive values', async () => {
+                const { updateShippingMethod } = await adminClient.query(gql`
+                    mutation {
+                        updateShippingMethod(
+                            input: {
+                                id: "${shippingMethodId}"
+                                translations: []
+                                customFields: { singleId: "T_3" }
+                            }
+                        ) {
+                            id
+                            ${customFieldsSelection}
+                        }
+                    }
+                `);
+                expect(updateShippingMethod.customFields.single).toEqual({ id: 'T_3' });
+                expect(updateShippingMethod.customFields.primitive).toBe('test');
+            });
+
             it('shop eligibleShippingMethods (ShippingMethodQuote)', async () => {
                 const { eligibleShippingMethods } = await shopClient.query(gql`
                     query {
@@ -1127,7 +1264,7 @@ describe('Custom field relations', () => {
                 const testShippingMethodQuote = eligibleShippingMethods.find(
                     (quote: any) => quote.code === 'test',
                 );
-                assertCustomFieldIds(testShippingMethodQuote.customFields, 'T_2', ['T_3', 'T_4']);
+                assertCustomFieldIds(testShippingMethodQuote.customFields, 'T_3', ['T_3', 'T_4']);
             });
         });
 
@@ -1175,6 +1312,25 @@ describe('Custom field relations', () => {
                 assertCustomFieldIds(updatePaymentMethod.customFields, 'T_2', ['T_3', 'T_4']);
             });
 
+            // https://github.com/vendure-ecommerce/vendure/issues/2840
+            it('updating custom field relation on PaymentMethod does not delete primitive values', async () => {
+                const { updatePaymentMethod } = await adminClient.query(gql`
+                    mutation {
+                        updatePaymentMethod(
+                            input: {
+                                id: "${paymentMethodId}"
+                                customFields: { singleId: "T_3" }
+                            }
+                        ) {
+                            id
+                            ${customFieldsSelection}
+                        }
+                    }
+                `);
+                expect(updatePaymentMethod.customFields.single).toEqual({ id: 'T_3' });
+                expect(updatePaymentMethod.customFields.primitive).toBe('test');
+            });
+
             it('shop eligiblePaymentMethods (PaymentMethodQuote)', async () => {
                 const { eligiblePaymentMethods } = await shopClient.query(gql`
                     query {
@@ -1186,7 +1342,7 @@ describe('Custom field relations', () => {
                         }
                     }
                 `);
-                assertCustomFieldIds(eligiblePaymentMethods[0].customFields, 'T_2', ['T_3', 'T_4']);
+                assertCustomFieldIds(eligiblePaymentMethods[0].customFields, 'T_3', ['T_3', 'T_4']);
             });
         });
 

+ 42 - 0
packages/core/e2e/graphql/admin-definitions.ts

@@ -17,3 +17,45 @@ export const SEARCH_PRODUCTS_ADMIN = gql`
         }
     }
 `;
+
+export const GET_ORDER_WITH_SELLER_ORDERS = gql`
+    query GetOrderWithSellerOrders($id: ID!) {
+        order(id: $id) {
+            id
+            code
+            state
+            sellerOrders {
+                id
+                aggregateOrderId
+                lines {
+                    id
+                    productVariant {
+                        id
+                        name
+                    }
+                }
+                shippingLines {
+                    id
+                    shippingMethod {
+                        id
+                        code
+                    }
+                }
+            }
+            lines {
+                id
+                productVariant {
+                    id
+                    name
+                }
+            }
+            shippingLines {
+                id
+                shippingMethod {
+                    id
+                    code
+                }
+            }
+        }
+    }
+`;

+ 192 - 0
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -7997,6 +7997,26 @@ export type SearchProductsAdminQuery = {
     };
 };
 
+export type GetOrderWithSellerOrdersQueryVariables = Exact<{
+    id: Scalars['ID']['input'];
+}>;
+
+export type GetOrderWithSellerOrdersQuery = {
+    order?: {
+        id: string;
+        code: string;
+        state: string;
+        sellerOrders?: Array<{
+            id: string;
+            aggregateOrderId?: string | null;
+            lines: Array<{ id: string; productVariant: { id: string; name: string } }>;
+            shippingLines: Array<{ id: string; shippingMethod: { id: string; code: string } }>;
+        }> | null;
+        lines: Array<{ id: string; productVariant: { id: string; name: string } }>;
+        shippingLines: Array<{ id: string; shippingMethod: { id: string; code: string } }>;
+    } | null;
+};
+
 export type AdministratorFragment = {
     id: string;
     firstName: string;
@@ -22781,6 +22801,178 @@ export const SearchProductsAdminDocument = {
         },
     ],
 } as unknown as DocumentNode<SearchProductsAdminQuery, SearchProductsAdminQueryVariables>;
+export const GetOrderWithSellerOrdersDocument = {
+    kind: 'Document',
+    definitions: [
+        {
+            kind: 'OperationDefinition',
+            operation: 'query',
+            name: { kind: 'Name', value: 'GetOrderWithSellerOrders' },
+            variableDefinitions: [
+                {
+                    kind: 'VariableDefinition',
+                    variable: { kind: 'Variable', name: { kind: 'Name', value: 'id' } },
+                    type: {
+                        kind: 'NonNullType',
+                        type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
+                    },
+                },
+            ],
+            selectionSet: {
+                kind: 'SelectionSet',
+                selections: [
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'order' },
+                        arguments: [
+                            {
+                                kind: 'Argument',
+                                name: { kind: 'Name', value: 'id' },
+                                value: { kind: 'Variable', name: { kind: 'Name', value: 'id' } },
+                            },
+                        ],
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'code' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'state' } },
+                                {
+                                    kind: 'Field',
+                                    name: { kind: 'Name', value: 'sellerOrders' },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                            {
+                                                kind: 'Field',
+                                                name: { kind: 'Name', value: 'aggregateOrderId' },
+                                            },
+                                            {
+                                                kind: 'Field',
+                                                name: { kind: 'Name', value: 'lines' },
+                                                selectionSet: {
+                                                    kind: 'SelectionSet',
+                                                    selections: [
+                                                        {
+                                                            kind: 'Field',
+                                                            name: { kind: 'Name', value: 'id' },
+                                                        },
+                                                        {
+                                                            kind: 'Field',
+                                                            name: { kind: 'Name', value: 'productVariant' },
+                                                            selectionSet: {
+                                                                kind: 'SelectionSet',
+                                                                selections: [
+                                                                    {
+                                                                        kind: 'Field',
+                                                                        name: { kind: 'Name', value: 'id' },
+                                                                    },
+                                                                    {
+                                                                        kind: 'Field',
+                                                                        name: { kind: 'Name', value: 'name' },
+                                                                    },
+                                                                ],
+                                                            },
+                                                        },
+                                                    ],
+                                                },
+                                            },
+                                            {
+                                                kind: 'Field',
+                                                name: { kind: 'Name', value: 'shippingLines' },
+                                                selectionSet: {
+                                                    kind: 'SelectionSet',
+                                                    selections: [
+                                                        {
+                                                            kind: 'Field',
+                                                            name: { kind: 'Name', value: 'id' },
+                                                        },
+                                                        {
+                                                            kind: 'Field',
+                                                            name: { kind: 'Name', value: 'shippingMethod' },
+                                                            selectionSet: {
+                                                                kind: 'SelectionSet',
+                                                                selections: [
+                                                                    {
+                                                                        kind: 'Field',
+                                                                        name: { kind: 'Name', value: 'id' },
+                                                                    },
+                                                                    {
+                                                                        kind: 'Field',
+                                                                        name: { kind: 'Name', value: 'code' },
+                                                                    },
+                                                                ],
+                                                            },
+                                                        },
+                                                    ],
+                                                },
+                                            },
+                                        ],
+                                    },
+                                },
+                                {
+                                    kind: 'Field',
+                                    name: { kind: 'Name', value: 'lines' },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                            {
+                                                kind: 'Field',
+                                                name: { kind: 'Name', value: 'productVariant' },
+                                                selectionSet: {
+                                                    kind: 'SelectionSet',
+                                                    selections: [
+                                                        {
+                                                            kind: 'Field',
+                                                            name: { kind: 'Name', value: 'id' },
+                                                        },
+                                                        {
+                                                            kind: 'Field',
+                                                            name: { kind: 'Name', value: 'name' },
+                                                        },
+                                                    ],
+                                                },
+                                            },
+                                        ],
+                                    },
+                                },
+                                {
+                                    kind: 'Field',
+                                    name: { kind: 'Name', value: 'shippingLines' },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                            {
+                                                kind: 'Field',
+                                                name: { kind: 'Name', value: 'shippingMethod' },
+                                                selectionSet: {
+                                                    kind: 'SelectionSet',
+                                                    selections: [
+                                                        {
+                                                            kind: 'Field',
+                                                            name: { kind: 'Name', value: 'id' },
+                                                        },
+                                                        {
+                                                            kind: 'Field',
+                                                            name: { kind: 'Name', value: 'code' },
+                                                        },
+                                                    ],
+                                                },
+                                            },
+                                        ],
+                                    },
+                                },
+                            ],
+                        },
+                    },
+                ],
+            },
+        },
+    ],
+} as unknown as DocumentNode<GetOrderWithSellerOrdersQuery, GetOrderWithSellerOrdersQueryVariables>;
 export const CreateAdministratorDocument = {
     kind: 'Document',
     definitions: [

+ 61 - 2
packages/core/e2e/order-modification.e2e-spec.ts

@@ -8,8 +8,13 @@ import {
     freeShipping,
     mergeConfig,
     minimumOrderAmount,
+    Order,
+    OrderItemPriceCalculationStrategy,
     orderPercentageDiscount,
+    PriceCalculationResult,
     productsPercentageDiscount,
+    ProductVariant,
+    RequestContext,
     ShippingCalculator,
 } from '@vendure/core';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
@@ -18,7 +23,7 @@ import path from 'path';
 import { afterAll, beforeAll, describe, expect, it } from 'vitest';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
-import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
+import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
 import { manualFulfillmentHandler } from '../src/config/fulfillment/manual-fulfillment-handler';
 import { orderFixedDiscount } from '../src/config/promotion/actions/order-fixed-discount-action';
 
@@ -64,6 +69,26 @@ import {
 } from './graphql/shop-definitions';
 import { addPaymentToOrder, proceedToArrangingPayment, sortById } from './utils/test-order-utils';
 
+export class TestOrderItemPriceCalculationStrategy implements OrderItemPriceCalculationStrategy {
+    calculateUnitPrice(
+        ctx: RequestContext,
+        productVariant: ProductVariant,
+        orderLineCustomFields: { [key: string]: any },
+        order: Order,
+    ): PriceCalculationResult | Promise<PriceCalculationResult> {
+        if (orderLineCustomFields.color === 'hotpink') {
+            return {
+                price: 1337,
+                priceIncludesTax: true,
+            };
+        }
+        return {
+            price: productVariant.listPrice,
+            priceIncludesTax: productVariant.listPriceIncludesTax,
+        };
+    }
+}
+
 const SHIPPING_GB = 500;
 const SHIPPING_US = 1000;
 const SHIPPING_OTHER = 750;
@@ -107,6 +132,9 @@ describe('Order modification', () => {
                     testFailingPaymentMethod,
                 ],
             },
+            orderOptions: {
+                orderItemPriceCalculationStrategy: new TestOrderItemPriceCalculationStrategy(),
+            },
             shippingOptions: {
                 shippingCalculators: [defaultShippingCalculator, testCalculator],
             },
@@ -702,6 +730,37 @@ describe('Order modification', () => {
             await assertOrderIsUnchanged(order!);
         });
 
+        it('the configured OrderItemPriceCalculationStrategy is applied', async () => {
+            const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
+                GET_ORDER,
+                {
+                    id: orderId,
+                },
+            );
+            const { modifyOrder } = await adminClient.query<
+                Codegen.ModifyOrderMutation,
+                Codegen.ModifyOrderMutationVariables
+            >(MODIFY_ORDER, {
+                input: {
+                    dryRun: true,
+                    orderId,
+                    adjustOrderLines: [
+                        {
+                            orderLineId: order!.lines[1].id,
+                            quantity: 1,
+                            customFields: { color: 'hotpink' },
+                        } as any,
+                    ],
+                },
+            });
+            orderGuard.assertSuccess(modifyOrder);
+
+            const expectedTotal = order!.totalWithTax - order!.lines[1].unitPriceWithTax;
+            expect(modifyOrder.lines[1].quantity).toBe(1);
+            expect(modifyOrder.lines[1].linePriceWithTax).toBe(1337);
+            await assertOrderIsUnchanged(order!);
+        });
+
         it('changing shipping method', async () => {
             const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
                 GET_ORDER,
@@ -2067,7 +2126,7 @@ describe('Order modification', () => {
 
             const result = await adminTransitionOrderToState(orderId5, 'ArrangingAdditionalPayment');
             orderGuard.assertSuccess(result);
-            expect(result!.state).toBe('ArrangingAdditionalPayment');
+            expect(result.state).toBe('ArrangingAdditionalPayment');
             const { addManualPaymentToOrder } = await adminClient.query<
                 Codegen.AddManualPaymentMutation,
                 Codegen.AddManualPaymentMutationVariables

+ 249 - 0
packages/core/e2e/order-multi-vendor.e2e-spec.ts

@@ -0,0 +1,249 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+import { mergeConfig, OrderService } from '@vendure/core';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
+import { multivendorPaymentMethodHandler } from 'dev-server/example-plugins/multivendor-plugin/config/mv-payment-handler';
+import { CONNECTED_PAYMENT_METHOD_CODE } from 'dev-server/example-plugins/multivendor-plugin/constants';
+import { MultivendorPlugin } from 'dev-server/example-plugins/multivendor-plugin/multivendor.plugin';
+import gql from 'graphql-tag';
+import path from 'path';
+import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+
+import { initialData } from '../../../e2e-common/e2e-initial-data';
+import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
+
+import {
+    AssignProductsToChannelDocument,
+    GetOrderWithSellerOrdersDocument,
+} from './graphql/generated-e2e-admin-types';
+import * as CodegenShop from './graphql/generated-e2e-shop-types';
+import { SetShippingMethodDocument } from './graphql/generated-e2e-shop-types';
+import {
+    ADD_ITEM_TO_ORDER,
+    ADD_PAYMENT,
+    GET_ELIGIBLE_SHIPPING_METHODS,
+    SET_SHIPPING_ADDRESS,
+    TRANSITION_TO_STATE,
+} from './graphql/shop-definitions';
+
+declare module '@vendure/core/dist/entity/custom-entity-fields' {
+    interface CustomShippingMethodFields {
+        minPrice: number;
+        maxPrice: number;
+    }
+}
+
+describe('Multi-vendor orders', () => {
+    const { server, adminClient, shopClient } = createTestEnvironment(
+        mergeConfig(testConfig(), {
+            plugins: [
+                MultivendorPlugin.init({
+                    platformFeePercent: 10,
+                    platformFeeSKU: 'FEE',
+                }),
+            ],
+        }),
+    );
+
+    let bobsPartsChannel: { id: string; token: string; variantIds: string[] };
+    let alicesWaresChannel: { id: string; token: string; variantIds: string[] };
+    let orderId: string;
+
+    type OrderSuccessResult =
+        | CodegenShop.UpdatedOrderFragment
+        | CodegenShop.TestOrderFragmentFragment
+        | CodegenShop.TestOrderWithPaymentsFragment
+        | CodegenShop.ActiveOrderCustomerFragment;
+    const orderResultGuard: ErrorResultGuard<OrderSuccessResult> = createErrorResultGuard(
+        input => !!input.lines,
+    );
+
+    beforeAll(async () => {
+        await server.init({
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 3,
+        });
+        await adminClient.asSuperAdmin();
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    it('setup sellers', async () => {
+        const result1 = await shopClient.query(REGISTER_SELLER, {
+            input: {
+                shopName: "Bob's Parts",
+                seller: {
+                    firstName: 'Bob',
+                    lastName: 'Dobalina',
+                    emailAddress: 'bob@bobs-parts.com',
+                    password: 'test',
+                },
+            },
+        });
+        bobsPartsChannel = result1.registerNewSeller;
+        expect(bobsPartsChannel.token).toBe('bobs-parts-token');
+
+        const result2 = await shopClient.query(REGISTER_SELLER, {
+            input: {
+                shopName: "Alice's Wares",
+                seller: {
+                    firstName: 'Alice',
+                    lastName: 'Smith',
+                    emailAddress: 'alice@alices-wares.com',
+                    password: 'test',
+                },
+            },
+        });
+        alicesWaresChannel = result2.registerNewSeller;
+        expect(alicesWaresChannel.token).toBe('alices-wares-token');
+    });
+
+    it('assign products to sellers', async () => {
+        const { assignProductsToChannel } = await adminClient.query(AssignProductsToChannelDocument, {
+            input: {
+                channelId: bobsPartsChannel.id,
+                productIds: ['T_1'],
+                priceFactor: 1,
+            },
+        });
+
+        expect(assignProductsToChannel[0].channels.map(c => c.code)).toEqual([
+            '__default_channel__',
+            'bobs-parts',
+        ]);
+        bobsPartsChannel.variantIds = assignProductsToChannel[0].variants.map(v => v.id);
+
+        expect(bobsPartsChannel.variantIds).toEqual(['T_1', 'T_2', 'T_3', 'T_4']);
+
+        const { assignProductsToChannel: result2 } = await adminClient.query(
+            AssignProductsToChannelDocument,
+            {
+                input: {
+                    channelId: alicesWaresChannel.id,
+                    productIds: ['T_11'],
+                    priceFactor: 1,
+                },
+            },
+        );
+        expect(result2[0].channels.map(c => c.code)).toEqual(['__default_channel__', 'alices-wares']);
+        alicesWaresChannel.variantIds = result2[0].variants.map(v => v.id);
+
+        expect(alicesWaresChannel.variantIds).toEqual(['T_22']);
+    });
+
+    it('adds items and sets shipping methods', async () => {
+        await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
+        await shopClient.query<
+            CodegenShop.AddItemToOrderMutation,
+            CodegenShop.AddItemToOrderMutationVariables
+        >(ADD_ITEM_TO_ORDER, {
+            productVariantId: bobsPartsChannel.variantIds[0],
+            quantity: 1,
+        });
+        await shopClient.query<
+            CodegenShop.AddItemToOrderMutation,
+            CodegenShop.AddItemToOrderMutationVariables
+        >(ADD_ITEM_TO_ORDER, {
+            productVariantId: alicesWaresChannel.variantIds[0],
+            quantity: 1,
+        });
+
+        await shopClient.query<
+            CodegenShop.SetShippingAddressMutation,
+            CodegenShop.SetShippingAddressMutationVariables
+        >(SET_SHIPPING_ADDRESS, {
+            input: {
+                streetLine1: '12 the street',
+                postalCode: '123456',
+                countryCode: 'US',
+            },
+        });
+
+        const { eligibleShippingMethods } = await shopClient.query<CodegenShop.GetShippingMethodsQuery>(
+            GET_ELIGIBLE_SHIPPING_METHODS,
+        );
+
+        expect(eligibleShippingMethods.map(m => m.code).sort()).toEqual([
+            'alices-wares-shipping',
+            'bobs-parts-shipping',
+            'express-shipping',
+            'standard-shipping',
+        ]);
+
+        const { setOrderShippingMethod } = await shopClient.query(SetShippingMethodDocument, {
+            id: [
+                eligibleShippingMethods.find(m => m.code === 'bobs-parts-shipping')!.id,
+                eligibleShippingMethods.find(m => m.code === 'alices-wares-shipping')!.id,
+            ],
+        });
+
+        orderResultGuard.assertSuccess(setOrderShippingMethod);
+        expect(setOrderShippingMethod.shippingLines.map(l => l.shippingMethod.code).sort()).toEqual([
+            'alices-wares-shipping',
+            'bobs-parts-shipping',
+        ]);
+    });
+
+    it('completing checkout splits order', async () => {
+        const { transitionOrderToState } = await shopClient.query<
+            CodegenShop.TransitionToStateMutation,
+            CodegenShop.TransitionToStateMutationVariables
+        >(TRANSITION_TO_STATE, { state: 'ArrangingPayment' });
+
+        orderResultGuard.assertSuccess(transitionOrderToState);
+
+        const { addPaymentToOrder } = await shopClient.query<
+            CodegenShop.AddPaymentToOrderMutation,
+            CodegenShop.AddPaymentToOrderMutationVariables
+        >(ADD_PAYMENT, {
+            input: {
+                method: CONNECTED_PAYMENT_METHOD_CODE,
+                metadata: {},
+            },
+        });
+        orderResultGuard.assertSuccess(addPaymentToOrder);
+
+        expect(addPaymentToOrder.state).toBe('PaymentSettled');
+
+        const { order } = await adminClient.query(GetOrderWithSellerOrdersDocument, {
+            id: addPaymentToOrder.id,
+        });
+        orderId = order!.id;
+
+        expect(order?.sellerOrders?.length).toBe(2);
+    });
+
+    it('order lines get split', async () => {
+        const { order } = await adminClient.query(GetOrderWithSellerOrdersDocument, {
+            id: orderId,
+        });
+
+        expect(order?.sellerOrders?.[0].lines.map(l => l.productVariant.name)).toEqual([
+            'Laptop 13 inch 8GB',
+        ]);
+        expect(order?.sellerOrders?.[1].lines.map(l => l.productVariant.name)).toEqual(['Road Bike']);
+    });
+
+    it('shippingLines get split', async () => {
+        const { order } = await adminClient.query(GetOrderWithSellerOrdersDocument, {
+            id: orderId,
+        });
+
+        expect(order?.sellerOrders?.[0]?.shippingLines.length).toBe(1);
+        expect(order?.sellerOrders?.[1]?.shippingLines.length).toBe(1);
+        expect(order?.sellerOrders?.[0]?.shippingLines[0].shippingMethod.code).toBe('bobs-parts-shipping');
+        expect(order?.sellerOrders?.[1]?.shippingLines[0].shippingMethod.code).toBe('alices-wares-shipping');
+    });
+});
+
+export const REGISTER_SELLER = gql`
+    mutation RegisterSeller($input: RegisterSellerInput!) {
+        registerNewSeller(input: $input) {
+            id
+            code
+            token
+        }
+    }
+`;

+ 12 - 22
packages/core/e2e/order-multiple-shipping.e2e-spec.ts

@@ -1,17 +1,17 @@
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
 import { summate } from '@vendure/common/lib/shared-utils';
 import {
+    defaultShippingCalculator,
+    defaultShippingEligibilityChecker,
+    manualFulfillmentHandler,
     mergeConfig,
-    RequestContext,
-    ShippingLineAssignmentStrategy,
-    ShippingLine,
     Order,
     OrderLine,
-    manualFulfillmentHandler,
-    defaultShippingCalculator,
-    defaultShippingEligibilityChecker,
     OrderService,
+    RequestContext,
     RequestContextService,
+    ShippingLine,
+    ShippingLineAssignmentStrategy,
 } from '@vendure/core';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import path from 'path';
@@ -19,20 +19,12 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
-import { hydratingShippingEligibilityChecker } from './fixtures/test-shipping-eligibility-checkers';
-import {
-    CreateAddressInput,
-    CreateShippingMethodDocument,
-    LanguageCode,
-} from './graphql/generated-e2e-admin-types';
 
-import * as Codegen from './graphql/generated-e2e-admin-types';
+import { CreateShippingMethodDocument, LanguageCode } from './graphql/generated-e2e-admin-types';
 import * as CodegenShop from './graphql/generated-e2e-shop-types';
-import { GET_COUNTRY_LIST, UPDATE_COUNTRY } from './graphql/shared-definitions';
 import {
     ADD_ITEM_TO_ORDER,
     GET_ACTIVE_ORDER,
-    GET_AVAILABLE_COUNTRIES,
     GET_ELIGIBLE_SHIPPING_METHODS,
     REMOVE_ITEM_FROM_ORDER,
     SET_SHIPPING_ADDRESS,
@@ -57,7 +49,7 @@ class CustomShippingLineAssignmentStrategy implements ShippingLineAssignmentStra
     }
 }
 
-describe('Shop orders', () => {
+describe('Multiple shipping orders', () => {
     const { server, adminClient, shopClient } = createTestEnvironment(
         mergeConfig(testConfig(), {
             customFields: {
@@ -220,9 +212,8 @@ describe('Shop orders', () => {
         expect(order?.shippingLines.length).toBe(1);
         expect(order?.shippingLines[0].shippingMethod.code).toBe('less-than-100');
 
-        const { activeOrder: activeOrder2 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
-            GET_ACTIVE_ORDER,
-        );
+        const { activeOrder: activeOrder2 } =
+            await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
 
         expect(activeOrder2?.shippingWithTax).toBe(summate(activeOrder2!.shippingLines, 'priceWithTax'));
     });
@@ -241,9 +232,8 @@ describe('Shop orders', () => {
         const order = await getInternalOrder(activeOrder!.id);
         expect(order?.lines.length).toBe(0);
 
-        const { activeOrder: activeOrder2 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
-            GET_ACTIVE_ORDER,
-        );
+        const { activeOrder: activeOrder2 } =
+            await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
 
         expect(activeOrder2?.shippingWithTax).toBe(0);
     });

+ 2 - 2
packages/core/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/core",
-    "version": "2.2.4",
+    "version": "2.2.5",
     "description": "A modern, headless ecommerce framework",
     "repository": {
         "type": "git",
@@ -51,7 +51,7 @@
         "@nestjs/testing": "10.3.3",
         "@nestjs/typeorm": "10.0.2",
         "@types/fs-extra": "^9.0.1",
-        "@vendure/common": "^2.2.4",
+        "@vendure/common": "^2.2.5",
         "bcrypt": "^5.1.1",
         "body-parser": "^1.20.2",
         "cookie-session": "^2.1.0",

+ 11 - 0
packages/core/src/api/resolvers/entity/order-entity.resolver.ts

@@ -63,6 +63,17 @@ export class OrderEntityResolver {
         return lines;
     }
 
+    @ResolveField()
+    async shippingLines(@Ctx() ctx: RequestContext, @Parent() order: Order) {
+        if (order.shippingLines) {
+            return order.shippingLines;
+        }
+        const { shippingLines } = await assertFound(
+            this.orderService.findOne(ctx, order.id, ['shippingLines.shippingMethod']),
+        );
+        return shippingLines;
+    }
+
     @ResolveField()
     async history(
         @Ctx() ctx: RequestContext,

+ 0 - 1
packages/core/src/config/fulfillment/default-fulfillment-process.ts

@@ -97,7 +97,6 @@ export const defaultFulfillmentProcess: FulfillmentProcess<FulfillmentState> = {
                 quantity: l.quantity,
             }));
             await stockMovementService.createCancellationsForOrderLines(ctx, orderLineInput);
-            // const lines = await groupOrderItemsIntoLines(ctx, orderLineInput);
             await stockMovementService.createAllocationsForOrderLines(ctx, orderLineInput);
         }
         if (fromState === 'Created' && toState === 'Pending') {

+ 12 - 2
packages/core/src/service/helpers/custom-field-relation/custom-field-relation.service.ts

@@ -17,7 +17,10 @@ import { VendureEntity } from '../../../entity/base/base.entity';
 
 @Injectable()
 export class CustomFieldRelationService {
-    constructor(private connection: TransactionalConnection, private configService: ConfigService) {}
+    constructor(
+        private connection: TransactionalConnection,
+        private configService: ConfigService,
+    ) {}
 
     /**
      * @description
@@ -54,7 +57,14 @@ export class CustomFieldRelationService {
                             .findOne({ where: { id: idOrIds } });
                     }
                     if (relations !== undefined) {
-                        entity.customFields = { ...entity.customFields, [field.name]: relations };
+                        const entityWithCustomFields = await this.connection
+                            .getRepository(ctx, entityType)
+                            .findOne({ where: { id: entity.id } as any, loadEagerRelations: false });
+                        entity.customFields = {
+                            ...entity.customFields,
+                            ...entityWithCustomFields?.customFields,
+                            [field.name]: relations,
+                        };
                         await this.connection
                             .getRepository(ctx, entityType)
                             .save(pick(entity, ['id', 'customFields']) as any, { reload: false });

+ 2 - 49
packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts

@@ -1,6 +1,5 @@
 import { Injectable } from '@nestjs/common';
 import { Type } from '@vendure/common/lib/shared-types';
-import { isObject } from '@vendure/common/lib/shared-utils';
 import { unique } from '@vendure/common/lib/unique';
 import { SelectQueryBuilder } from 'typeorm';
 
@@ -14,6 +13,7 @@ import { TranslatorService } from '../translator/translator.service';
 import { joinTreeRelationsDynamically } from '../utils/tree-relations-qb-joiner';
 
 import { HydrateOptions } from './entity-hydrator-types';
+import { mergeDeep } from './merge-deep';
 
 /**
  * @description
@@ -134,7 +134,7 @@ export class EntityHydrator {
                 const hydrated = await hydratedQb.getOne();
                 const propertiesToAdd = unique(missingRelations.map(relation => relation.split('.')[0]));
                 for (const prop of propertiesToAdd) {
-                    (target as any)[prop] = this.mergeDeep((target as any)[prop], hydrated[prop]);
+                    (target as any)[prop] = mergeDeep((target as any)[prop], hydrated[prop]);
                 }
 
                 const relationsWithEntities = missingRelations.map(relation => ({
@@ -306,51 +306,4 @@ export class EntityHydrator {
             ? input[0]?.hasOwnProperty('translations') ?? false
             : input?.hasOwnProperty('translations') ?? false;
     }
-
-    /**
-     * Merges properties into a target entity. This is needed for the cases in which a
-     * property already exists on the target, but the hydrated version also contains that
-     * property with a different set of properties. This prevents the original target
-     * entity from having data overwritten.
-     */
-    private mergeDeep<T extends { [key: string]: any }>(a: T | undefined, b: T): T {
-        if (!a) {
-            return b;
-        }
-        if (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.length > 1) {
-            if (a[0].hasOwnProperty('id')) {
-                // If the array contains entities, we can use the id to match them up
-                // so that we ensure that we don't merge properties from different entities
-                // with the same index.
-                const aIds = a.map(e => e.id);
-                const bIds = b.map(e => e.id);
-                if (JSON.stringify(aIds) !== JSON.stringify(bIds)) {
-                    // The entities in the arrays are not in the same order, so we can't
-                    // safely merge them. We need to sort the `b` array so that the entities
-                    // are in the same order as the `a` array.
-                    const idToIndexMap = new Map();
-                    a.forEach((item, index) => {
-                        idToIndexMap.set(item.id, index);
-                    });
-                    b.sort((_a, _b) => {
-                        return idToIndexMap.get(_a.id) - idToIndexMap.get(_b.id);
-                    });
-                }
-            }
-        }
-        for (const [key, value] of Object.entries(b)) {
-            if (Object.getOwnPropertyDescriptor(b, key)?.writable) {
-                if (Array.isArray(value)) {
-                    (a as any)[key] = value.map((v, index) =>
-                        this.mergeDeep(a?.[key]?.[index], b[key][index]),
-                    );
-                } else if (isObject(value)) {
-                    (a as any)[key] = this.mergeDeep(a?.[key], b[key]);
-                } else {
-                    (a as any)[key] = b[key];
-                }
-            }
-        }
-        return a ?? b;
-    }
 }

+ 45 - 0
packages/core/src/service/helpers/entity-hydrator/merge-deep.spec.ts

@@ -0,0 +1,45 @@
+import { describe, expect, it } from 'vitest';
+
+import { Order, Sale } from '../../../entity';
+
+import { mergeDeep } from './merge-deep';
+
+describe('mergeDeep()', () => {
+    // https://github.com/vendure-ecommerce/vendure/issues/2864
+    it('should sync the order of sub relations', () => {
+        const prefetched = new Order({
+            lines: [
+                {
+                    id: 'line1',
+                    sales: [new Sale({ id: 'sale-of-line-1' })],
+                },
+                {
+                    id: 'line2',
+                    sales: [new Sale({ id: 'sale-of-line-2' })],
+                },
+            ],
+        });
+
+        const hydrationFetched = new Order({
+            lines: [
+                {
+                    id: 'line2',
+                    productVariant: { id: 'variant-of-line-2' },
+                },
+                {
+                    id: 'line1',
+                    productVariant: { id: 'variant-of-line-1' },
+                },
+            ],
+        });
+
+        const merged = mergeDeep(prefetched, hydrationFetched);
+        const line1 = merged.lines.find(l => l.id === 'line1');
+        const line2 = merged.lines.find(l => l.id === 'line2');
+
+        expect(line1?.sales[0].id).toBe('sale-of-line-1');
+        expect(line1?.productVariant?.id).toBe('variant-of-line-1');
+        expect(line2?.sales[0].id).toBe('sale-of-line-2');
+        expect(line2?.productVariant?.id).toBe('variant-of-line-2');
+    });
+});

+ 44 - 0
packages/core/src/service/helpers/entity-hydrator/merge-deep.ts

@@ -0,0 +1,44 @@
+import { isObject } from '@vendure/common/lib/shared-utils';
+
+/**
+ * Merges properties into a target entity. This is needed for the cases in which a
+ * property already exists on the target, but the hydrated version also contains that
+ * property with a different set of properties. This prevents the original target
+ * entity from having data overwritten.
+ */
+export function mergeDeep<T extends { [key: string]: any }>(a: T | undefined, b: T): T {
+    if (!a) {
+        return b;
+    }
+    if (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.length > 1) {
+        if (a[0].hasOwnProperty('id')) {
+            // If the array contains entities, we can use the id to match them up
+            // so that we ensure that we don't merge properties from different entities
+            // with the same index.
+            const aIds = a.map(e => e.id);
+            const bIds = b.map(e => e.id);
+            if (JSON.stringify(aIds) !== JSON.stringify(bIds)) {
+                // The entities in the arrays are not in the same order, so we can't
+                // safely merge them. We need to sort the `b` array so that the entities
+                // are in the same order as the `a` array.
+                const idToIndexMap = new Map();
+                a.forEach((item, index) => {
+                    idToIndexMap.set(item.id, index);
+                });
+                b.sort((_a, _b) => {
+                    return idToIndexMap.get(_a.id) - idToIndexMap.get(_b.id);
+                });
+            }
+        }
+    }
+    for (const [key, value] of Object.entries(b)) {
+        if (Object.getOwnPropertyDescriptor(b, key)?.writable) {
+            if (Array.isArray(value) || isObject(value)) {
+                (a as any)[key] = mergeDeep(a?.[key], b[key]);
+            } else {
+                (a as any)[key] = b[key];
+            }
+        }
+    }
+    return a ?? b;
+}

+ 18 - 0
packages/core/src/service/helpers/order-modifier/order-modifier.ts

@@ -618,6 +618,24 @@ export class OrderModifier {
                 return result;
             }
         }
+        const { orderItemPriceCalculationStrategy } = this.configService.orderOptions;
+        for (const orderLine of updatedOrderLines) {
+            const variant = await this.productVariantService.applyChannelPriceAndTax(
+                orderLine.productVariant,
+                ctx,
+                order,
+            );
+            const priceResult = await orderItemPriceCalculationStrategy.calculateUnitPrice(
+                ctx,
+                variant,
+                orderLine.customFields || {},
+                order,
+                orderLine.quantity,
+            );
+            orderLine.listPrice = priceResult.price;
+            orderLine.listPriceIncludesTax = priceResult.priceIncludesTax;
+        }
+
         await this.orderCalculator.applyPriceAdjustments(ctx, order, promotions, updatedOrderLines, {
             recalculateShipping: input.options?.recalculateShipping,
         });

+ 4 - 3
packages/core/src/service/helpers/order-splitter/order-splitter.ts

@@ -40,11 +40,12 @@ export class OrderSplitter {
             const shippingLines: ShippingLine[] = [];
             for (const shippingLine of partialOrder.shippingLines) {
                 const newShippingLine = await this.duplicateShippingLine(ctx, shippingLine);
-                lines.map((line) => {
-                    if(shippingLine.id === line.shippingLineId) {
+                for (const line of lines) {
+                    if (shippingLine.id === line.shippingLineId) {
                         line.shippingLineId = newShippingLine.id;
+                        await this.connection.getRepository(ctx, OrderLine).save(line);
                     }
-                })
+                }
                 shippingLines.push(newShippingLine);
             }
             const sellerOrder = await this.connection.getRepository(ctx, Order).save(

+ 1 - 1
packages/core/src/service/services/order.service.ts

@@ -473,8 +473,8 @@ export class OrderService {
     async updateCustomFields(ctx: RequestContext, orderId: ID, customFields: any) {
         let order = await this.getOrderOrThrow(ctx, orderId);
         order = patchEntity(order, { customFields });
-        await this.customFieldRelationService.updateRelations(ctx, Order, { customFields }, order);
         const updatedOrder = await this.connection.getRepository(ctx, Order).save(order);
+        await this.customFieldRelationService.updateRelations(ctx, Order, { customFields }, updatedOrder);
         await this.eventBus.publish(new OrderEvent(ctx, updatedOrder, 'updated'));
         return updatedOrder;
     }

+ 3 - 3
packages/core/src/service/services/shipping-method.service.ts

@@ -168,15 +168,15 @@ export class ShippingMethodService {
                 input.fulfillmentHandler,
             );
         }
-        await this.connection
-            .getRepository(ctx, ShippingMethod)
-            .save(updatedShippingMethod, { reload: false });
         await this.customFieldRelationService.updateRelations(
             ctx,
             ShippingMethod,
             input,
             updatedShippingMethod,
         );
+        await this.connection
+            .getRepository(ctx, ShippingMethod)
+            .save(updatedShippingMethod, { reload: false });
         await this.eventBus.publish(new ShippingMethodEvent(ctx, shippingMethod, 'updated', input));
         return assertFound(this.findOne(ctx, shippingMethod.id));
     }

+ 3 - 3
packages/create/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/create",
-    "version": "2.2.4",
+    "version": "2.2.5",
     "license": "MIT",
     "bin": {
         "create": "./index.js"
@@ -28,14 +28,14 @@
         "@types/fs-extra": "^11.0.4",
         "@types/handlebars": "^4.1.0",
         "@types/semver": "^7.5.8",
-        "@vendure/core": "^2.2.4",
+        "@vendure/core": "^2.2.5",
         "rimraf": "^5.0.5",
         "ts-node": "^10.9.2",
         "typescript": "5.3.3"
     },
     "dependencies": {
         "@clack/prompts": "^0.7.0",
-        "@vendure/common": "^2.2.4",
+        "@vendure/common": "^2.2.5",
         "commander": "^11.0.0",
         "cross-spawn": "^7.0.3",
         "detect-port": "^1.5.1",

+ 4 - 1
packages/create/src/helpers.ts

@@ -263,9 +263,12 @@ export function getDependencies(
         `@vendure/admin-ui-plugin${vendurePkgVersion}`,
         'dotenv',
         dbDriverPackage(dbType),
+    ];
+    const devDependencies = [
+        `@vendure/cli${vendurePkgVersion}`,
+        'concurrently',
         `typescript@${TYPESCRIPT_VERSION}`,
     ];
-    const devDependencies = ['concurrently', `@vendure/cli${vendurePkgVersion}`];
     return { dependencies, devDependencies };
 }
 

+ 11 - 0
packages/create/templates/readme.hbs

@@ -120,3 +120,14 @@ data that you cannot lose.
 ---
 
 You can also run any pending migrations manually, without starting the server via the "vendure migrate" command.
+
+---
+
+## Troubleshooting
+
+### Error: Could not load the "sharp" module using the \[OS\]-x\[Architecture\] runtime when running Vendure server.
+
+- Make sure your Node version is ^18.17.0 || ^20.3.0 || >=21.0.0 to support the Sharp library.
+- Make sure your package manager is up to date.
+- **Not recommended**: if none of the above helps to resolve the issue, install sharp specifying your machines OS and Architecture. For example: `pnpm install sharp --config.platform=linux --config.architecture=x64` or `npm install sharp --os linux --cpu x64`
+

+ 9 - 9
packages/dev-server/package.json

@@ -1,6 +1,6 @@
 {
     "name": "dev-server",
-    "version": "2.2.4",
+    "version": "2.2.5",
     "main": "index.js",
     "license": "MIT",
     "private": true,
@@ -15,17 +15,17 @@
     },
     "dependencies": {
         "@nestjs/axios": "^3.0.2",
-        "@vendure/admin-ui-plugin": "^2.2.4",
-        "@vendure/asset-server-plugin": "^2.2.4",
-        "@vendure/common": "^2.2.4",
-        "@vendure/core": "^2.2.4",
-        "@vendure/elasticsearch-plugin": "^2.2.4",
-        "@vendure/email-plugin": "^2.2.4",
+        "@vendure/admin-ui-plugin": "^2.2.5",
+        "@vendure/asset-server-plugin": "^2.2.5",
+        "@vendure/common": "^2.2.5",
+        "@vendure/core": "^2.2.5",
+        "@vendure/elasticsearch-plugin": "^2.2.5",
+        "@vendure/email-plugin": "^2.2.5",
         "typescript": "5.3.3"
     },
     "devDependencies": {
-        "@vendure/testing": "^2.2.4",
-        "@vendure/ui-devkit": "^2.2.4",
+        "@vendure/testing": "^2.2.5",
+        "@vendure/ui-devkit": "^2.2.5",
         "commander": "^12.0.0",
         "concurrently": "^8.2.2",
         "csv-stringify": "^6.4.6",

+ 3 - 3
packages/elasticsearch-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/elasticsearch-plugin",
-    "version": "2.2.4",
+    "version": "2.2.5",
     "license": "MIT",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -26,8 +26,8 @@
         "fast-deep-equal": "^3.1.3"
     },
     "devDependencies": {
-        "@vendure/common": "^2.2.4",
-        "@vendure/core": "^2.2.4",
+        "@vendure/common": "^2.2.5",
+        "@vendure/core": "^2.2.5",
         "rimraf": "^5.0.5",
         "typescript": "5.3.3"
     }

+ 3 - 3
packages/email-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/email-plugin",
-    "version": "2.2.4",
+    "version": "2.2.5",
     "license": "MIT",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -34,8 +34,8 @@
         "@types/express": "^4.17.21",
         "@types/fs-extra": "^11.0.4",
         "@types/mjml": "^4.7.4",
-        "@vendure/common": "^2.2.4",
-        "@vendure/core": "^2.2.4",
+        "@vendure/common": "^2.2.5",
+        "@vendure/core": "^2.2.5",
         "rimraf": "^5.0.5",
         "typescript": "5.3.3"
     }

+ 3 - 3
packages/harden-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/harden-plugin",
-    "version": "2.2.4",
+    "version": "2.2.5",
     "license": "MIT",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -21,7 +21,7 @@
         "graphql-query-complexity": "^0.12.0"
     },
     "devDependencies": {
-        "@vendure/common": "^2.2.4",
-        "@vendure/core": "^2.2.4"
+        "@vendure/common": "^2.2.5",
+        "@vendure/core": "^2.2.5"
     }
 }

+ 3 - 3
packages/job-queue-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/job-queue-plugin",
-    "version": "2.2.4",
+    "version": "2.2.5",
     "license": "MIT",
     "main": "package/index.js",
     "types": "package/index.d.ts",
@@ -23,8 +23,8 @@
     },
     "devDependencies": {
         "@google-cloud/pubsub": "^2.8.0",
-        "@vendure/common": "^2.2.4",
-        "@vendure/core": "^2.2.4",
+        "@vendure/common": "^2.2.5",
+        "@vendure/core": "^2.2.5",
         "bullmq": "^5.4.2",
         "ioredis": "^5.3.2",
         "rimraf": "^5.0.5",

+ 11 - 0
packages/payments-plugin/e2e/graphql/admin-queries.ts

@@ -114,3 +114,14 @@ export const CREATE_CHANNEL = gql`
         }
     }
 `;
+
+export const CREATE_COUPON = gql`
+    mutation CreatePromotion($input: CreatePromotionInput!) {
+        createPromotion(input: $input) {
+            ... on ErrorResult {
+                errorCode
+            }
+            __typename
+        }
+    }
+`;

+ 65 - 0
packages/payments-plugin/e2e/graphql/generated-admin-types.ts

@@ -6386,6 +6386,16 @@ export type CreateChannelMutation = {
         | { errorCode: ErrorCode; message: string };
 };
 
+export type CreatePromotionMutationVariables = Exact<{
+    input: CreatePromotionInput;
+}>;
+
+export type CreatePromotionMutation = {
+    createPromotion:
+        | { __typename: 'MissingConditionsError'; errorCode: ErrorCode }
+        | { __typename: 'Promotion' };
+};
+
 export const PaymentMethodFragmentDoc = {
     kind: 'Document',
     definitions: [
@@ -6847,3 +6857,58 @@ export const CreateChannelDocument = {
         },
     ],
 } as unknown as DocumentNode<CreateChannelMutation, CreateChannelMutationVariables>;
+export const CreatePromotionDocument = {
+    kind: 'Document',
+    definitions: [
+        {
+            kind: 'OperationDefinition',
+            operation: 'mutation',
+            name: { kind: 'Name', value: 'CreatePromotion' },
+            variableDefinitions: [
+                {
+                    kind: 'VariableDefinition',
+                    variable: { kind: 'Variable', name: { kind: 'Name', value: 'input' } },
+                    type: {
+                        kind: 'NonNullType',
+                        type: { kind: 'NamedType', name: { kind: 'Name', value: 'CreatePromotionInput' } },
+                    },
+                },
+            ],
+            selectionSet: {
+                kind: 'SelectionSet',
+                selections: [
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'createPromotion' },
+                        arguments: [
+                            {
+                                kind: 'Argument',
+                                name: { kind: 'Name', value: 'input' },
+                                value: { kind: 'Variable', name: { kind: 'Name', value: 'input' } },
+                            },
+                        ],
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                {
+                                    kind: 'InlineFragment',
+                                    typeCondition: {
+                                        kind: 'NamedType',
+                                        name: { kind: 'Name', value: 'ErrorResult' },
+                                    },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            { kind: 'Field', name: { kind: 'Name', value: 'errorCode' } },
+                                        ],
+                                    },
+                                },
+                                { kind: 'Field', name: { kind: 'Name', value: '__typename' } },
+                            ],
+                        },
+                    },
+                ],
+            },
+        },
+    ],
+} as unknown as DocumentNode<CreatePromotionMutation, CreatePromotionMutationVariables>;

+ 794 - 63
packages/payments-plugin/e2e/graphql/generated-shop-types.ts

@@ -3329,6 +3329,17 @@ export type TestOrderFragmentFragment = {
             type: AdjustmentType;
         }>;
     }>;
+    shippingAddress?: {
+        fullName?: string | null;
+        company?: string | null;
+        streetLine1?: string | null;
+        streetLine2?: string | null;
+        city?: string | null;
+        province?: string | null;
+        postalCode?: string | null;
+        country?: string | null;
+        phoneNumber?: string | null;
+    } | null;
     shippingLines: Array<{ shippingMethod: { id: string; code: string; description: string } }>;
     customer?: { id: string; emailAddress: string; user?: { id: string; identifier: string } | null } | null;
     history: { items: Array<{ id: string; type: HistoryEntryType; data: any }> };
@@ -3389,6 +3400,17 @@ export type AddPaymentToOrderMutation = {
                       type: AdjustmentType;
                   }>;
               }>;
+              shippingAddress?: {
+                  fullName?: string | null;
+                  company?: string | null;
+                  streetLine1?: string | null;
+                  streetLine2?: string | null;
+                  city?: string | null;
+                  province?: string | null;
+                  postalCode?: string | null;
+                  country?: string | null;
+                  phoneNumber?: string | null;
+              } | null;
               shippingLines: Array<{ shippingMethod: { id: string; code: string; description: string } }>;
               customer?: {
                   id: string;
@@ -3411,6 +3433,52 @@ export type SetShippingAddressMutation = {
     setOrderShippingAddress:
         | { errorCode: ErrorCode; message: string }
         | {
+              id: string;
+              code: string;
+              state: string;
+              active: boolean;
+              subTotal: number;
+              subTotalWithTax: number;
+              shipping: number;
+              shippingWithTax: number;
+              total: number;
+              totalWithTax: number;
+              currencyCode: CurrencyCode;
+              couponCodes: Array<string>;
+              discounts: Array<{
+                  adjustmentSource: string;
+                  amount: number;
+                  amountWithTax: number;
+                  description: string;
+                  type: AdjustmentType;
+              }>;
+              payments?: Array<{
+                  id: string;
+                  transactionId?: string | null;
+                  method: string;
+                  amount: number;
+                  state: string;
+                  metadata?: any | null;
+              }> | null;
+              lines: Array<{
+                  id: string;
+                  quantity: number;
+                  linePrice: number;
+                  linePriceWithTax: number;
+                  unitPrice: number;
+                  unitPriceWithTax: number;
+                  unitPriceChangeSinceAdded: number;
+                  unitPriceWithTaxChangeSinceAdded: number;
+                  proratedUnitPriceWithTax: number;
+                  productVariant: { id: string };
+                  discounts: Array<{
+                      adjustmentSource: string;
+                      amount: number;
+                      amountWithTax: number;
+                      description: string;
+                      type: AdjustmentType;
+                  }>;
+              }>;
               shippingAddress?: {
                   fullName?: string | null;
                   company?: string | null;
@@ -3422,6 +3490,13 @@ export type SetShippingAddressMutation = {
                   country?: string | null;
                   phoneNumber?: string | null;
               } | null;
+              shippingLines: Array<{ shippingMethod: { id: string; code: string; description: string } }>;
+              customer?: {
+                  id: string;
+                  emailAddress: string;
+                  user?: { id: string; identifier: string } | null;
+              } | null;
+              history: { items: Array<{ id: string; type: HistoryEntryType; data: any }> };
           };
 };
 
@@ -3509,6 +3584,17 @@ export type SetShippingMethodMutation = {
                       type: AdjustmentType;
                   }>;
               }>;
+              shippingAddress?: {
+                  fullName?: string | null;
+                  company?: string | null;
+                  streetLine1?: string | null;
+                  streetLine2?: string | null;
+                  city?: string | null;
+                  province?: string | null;
+                  postalCode?: string | null;
+                  country?: string | null;
+                  phoneNumber?: string | null;
+              } | null;
               shippingLines: Array<{ shippingMethod: { id: string; code: string; description: string } }>;
               customer?: {
                   id: string;
@@ -3578,6 +3664,17 @@ export type AddItemToOrderMutation = {
                           type: AdjustmentType;
                       }>;
                   }>;
+                  shippingAddress?: {
+                      fullName?: string | null;
+                      company?: string | null;
+                      streetLine1?: string | null;
+                      streetLine2?: string | null;
+                      city?: string | null;
+                      province?: string | null;
+                      postalCode?: string | null;
+                      country?: string | null;
+                      phoneNumber?: string | null;
+                  } | null;
                   shippingLines: Array<{ shippingMethod: { id: string; code: string; description: string } }>;
                   customer?: {
                       id: string;
@@ -3635,6 +3732,17 @@ export type AddItemToOrderMutation = {
                       type: AdjustmentType;
                   }>;
               }>;
+              shippingAddress?: {
+                  fullName?: string | null;
+                  company?: string | null;
+                  streetLine1?: string | null;
+                  streetLine2?: string | null;
+                  city?: string | null;
+                  province?: string | null;
+                  postalCode?: string | null;
+                  country?: string | null;
+                  phoneNumber?: string | null;
+              } | null;
               shippingLines: Array<{ shippingMethod: { id: string; code: string; description: string } }>;
               customer?: {
                   id: string;
@@ -3705,6 +3813,17 @@ export type AdjustOrderLineMutation = {
                           type: AdjustmentType;
                       }>;
                   }>;
+                  shippingAddress?: {
+                      fullName?: string | null;
+                      company?: string | null;
+                      streetLine1?: string | null;
+                      streetLine2?: string | null;
+                      city?: string | null;
+                      province?: string | null;
+                      postalCode?: string | null;
+                      country?: string | null;
+                      phoneNumber?: string | null;
+                  } | null;
                   shippingLines: Array<{ shippingMethod: { id: string; code: string; description: string } }>;
                   customer?: {
                       id: string;
@@ -3762,6 +3881,17 @@ export type AdjustOrderLineMutation = {
                       type: AdjustmentType;
                   }>;
               }>;
+              shippingAddress?: {
+                  fullName?: string | null;
+                  company?: string | null;
+                  streetLine1?: string | null;
+                  streetLine2?: string | null;
+                  city?: string | null;
+                  province?: string | null;
+                  postalCode?: string | null;
+                  country?: string | null;
+                  phoneNumber?: string | null;
+              } | null;
               shippingLines: Array<{ shippingMethod: { id: string; code: string; description: string } }>;
               customer?: {
                   id: string;
@@ -3826,6 +3956,17 @@ export type GetOrderByCodeQuery = {
                 type: AdjustmentType;
             }>;
         }>;
+        shippingAddress?: {
+            fullName?: string | null;
+            company?: string | null;
+            streetLine1?: string | null;
+            streetLine2?: string | null;
+            city?: string | null;
+            province?: string | null;
+            postalCode?: string | null;
+            country?: string | null;
+            phoneNumber?: string | null;
+        } | null;
         shippingLines: Array<{ shippingMethod: { id: string; code: string; description: string } }>;
         customer?: {
             id: string;
@@ -3886,6 +4027,17 @@ export type GetActiveOrderQuery = {
                 type: AdjustmentType;
             }>;
         }>;
+        shippingAddress?: {
+            fullName?: string | null;
+            company?: string | null;
+            streetLine1?: string | null;
+            streetLine2?: string | null;
+            city?: string | null;
+            province?: string | null;
+            postalCode?: string | null;
+            country?: string | null;
+            phoneNumber?: string | null;
+        } | null;
         shippingLines: Array<{ shippingMethod: { id: string; code: string; description: string } }>;
         customer?: {
             id: string;
@@ -3896,6 +4048,83 @@ export type GetActiveOrderQuery = {
     } | null;
 };
 
+export type ApplyCouponCodeMutationVariables = Exact<{
+    couponCode: Scalars['String']['input'];
+}>;
+
+export type ApplyCouponCodeMutation = {
+    applyCouponCode:
+        | { errorCode: ErrorCode; message: string }
+        | { errorCode: ErrorCode; message: string }
+        | { errorCode: ErrorCode; message: string }
+        | {
+              id: string;
+              code: string;
+              state: string;
+              active: boolean;
+              subTotal: number;
+              subTotalWithTax: number;
+              shipping: number;
+              shippingWithTax: number;
+              total: number;
+              totalWithTax: number;
+              currencyCode: CurrencyCode;
+              couponCodes: Array<string>;
+              discounts: Array<{
+                  adjustmentSource: string;
+                  amount: number;
+                  amountWithTax: number;
+                  description: string;
+                  type: AdjustmentType;
+              }>;
+              payments?: Array<{
+                  id: string;
+                  transactionId?: string | null;
+                  method: string;
+                  amount: number;
+                  state: string;
+                  metadata?: any | null;
+              }> | null;
+              lines: Array<{
+                  id: string;
+                  quantity: number;
+                  linePrice: number;
+                  linePriceWithTax: number;
+                  unitPrice: number;
+                  unitPriceWithTax: number;
+                  unitPriceChangeSinceAdded: number;
+                  unitPriceWithTaxChangeSinceAdded: number;
+                  proratedUnitPriceWithTax: number;
+                  productVariant: { id: string };
+                  discounts: Array<{
+                      adjustmentSource: string;
+                      amount: number;
+                      amountWithTax: number;
+                      description: string;
+                      type: AdjustmentType;
+                  }>;
+              }>;
+              shippingAddress?: {
+                  fullName?: string | null;
+                  company?: string | null;
+                  streetLine1?: string | null;
+                  streetLine2?: string | null;
+                  city?: string | null;
+                  province?: string | null;
+                  postalCode?: string | null;
+                  country?: string | null;
+                  phoneNumber?: string | null;
+              } | null;
+              shippingLines: Array<{ shippingMethod: { id: string; code: string; description: string } }>;
+              customer?: {
+                  id: string;
+                  emailAddress: string;
+                  user?: { id: string; identifier: string } | null;
+              } | null;
+              history: { items: Array<{ id: string; type: HistoryEntryType; data: any }> };
+          };
+};
+
 export const TestOrderFragmentFragmentDoc = {
     kind: 'Document',
     definitions: [
@@ -3993,6 +4222,24 @@ export const TestOrderFragmentFragmentDoc = {
                             ],
                         },
                     },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'shippingAddress' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'fullName' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'company' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'streetLine1' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'streetLine2' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'city' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'province' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'postalCode' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'country' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'phoneNumber' } },
+                            ],
+                        },
+                    },
                     {
                         kind: 'Field',
                         name: { kind: 'Name', value: 'shippingLines' },
@@ -4277,6 +4524,24 @@ export const AddPaymentToOrderDocument = {
                             ],
                         },
                     },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'shippingAddress' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'fullName' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'company' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'streetLine1' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'streetLine2' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'city' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'province' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'postalCode' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'country' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'phoneNumber' } },
+                            ],
+                        },
+                    },
                     {
                         kind: 'Field',
                         name: { kind: 'Name', value: 'shippingLines' },
@@ -4389,49 +4654,8 @@ export const SetShippingAddressDocument = {
                                         kind: 'SelectionSet',
                                         selections: [
                                             {
-                                                kind: 'Field',
-                                                name: { kind: 'Name', value: 'shippingAddress' },
-                                                selectionSet: {
-                                                    kind: 'SelectionSet',
-                                                    selections: [
-                                                        {
-                                                            kind: 'Field',
-                                                            name: { kind: 'Name', value: 'fullName' },
-                                                        },
-                                                        {
-                                                            kind: 'Field',
-                                                            name: { kind: 'Name', value: 'company' },
-                                                        },
-                                                        {
-                                                            kind: 'Field',
-                                                            name: { kind: 'Name', value: 'streetLine1' },
-                                                        },
-                                                        {
-                                                            kind: 'Field',
-                                                            name: { kind: 'Name', value: 'streetLine2' },
-                                                        },
-                                                        {
-                                                            kind: 'Field',
-                                                            name: { kind: 'Name', value: 'city' },
-                                                        },
-                                                        {
-                                                            kind: 'Field',
-                                                            name: { kind: 'Name', value: 'province' },
-                                                        },
-                                                        {
-                                                            kind: 'Field',
-                                                            name: { kind: 'Name', value: 'postalCode' },
-                                                        },
-                                                        {
-                                                            kind: 'Field',
-                                                            name: { kind: 'Name', value: 'country' },
-                                                        },
-                                                        {
-                                                            kind: 'Field',
-                                                            name: { kind: 'Name', value: 'phoneNumber' },
-                                                        },
-                                                    ],
-                                                },
+                                                kind: 'FragmentSpread',
+                                                name: { kind: 'Name', value: 'TestOrderFragment' },
                                             },
                                         ],
                                     },
@@ -4456,39 +4680,218 @@ export const SetShippingAddressDocument = {
                 ],
             },
         },
-    ],
-} as unknown as DocumentNode<SetShippingAddressMutation, SetShippingAddressMutationVariables>;
-export const GetShippingMethodsDocument = {
-    kind: 'Document',
-    definitions: [
         {
-            kind: 'OperationDefinition',
-            operation: 'query',
-            name: { kind: 'Name', value: 'GetShippingMethods' },
+            kind: 'FragmentDefinition',
+            name: { kind: 'Name', value: 'TestOrderFragment' },
+            typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Order' } },
             selectionSet: {
                 kind: 'SelectionSet',
                 selections: [
+                    { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'code' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'state' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'active' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'subTotal' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'subTotalWithTax' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'shipping' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'shippingWithTax' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'total' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'totalWithTax' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'currencyCode' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'couponCodes' } },
                     {
                         kind: 'Field',
-                        name: { kind: 'Name', value: 'eligibleShippingMethods' },
+                        name: { kind: 'Name', value: 'discounts' },
                         selectionSet: {
                             kind: 'SelectionSet',
                             selections: [
-                                { kind: 'Field', name: { kind: 'Name', value: 'id' } },
-                                { kind: 'Field', name: { kind: 'Name', value: 'code' } },
-                                { kind: 'Field', name: { kind: 'Name', value: 'price' } },
-                                { kind: 'Field', name: { kind: 'Name', value: 'name' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'adjustmentSource' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'amount' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'amountWithTax' } },
                                 { kind: 'Field', name: { kind: 'Name', value: 'description' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'type' } },
                             ],
                         },
                     },
-                ],
-            },
-        },
-    ],
-} as unknown as DocumentNode<GetShippingMethodsQuery, GetShippingMethodsQueryVariables>;
-export const TransitionToStateDocument = {
-    kind: 'Document',
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'payments' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'transactionId' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'method' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'amount' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'state' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'metadata' } },
+                            ],
+                        },
+                    },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'lines' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'quantity' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'linePrice' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'linePriceWithTax' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'unitPrice' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'unitPriceWithTax' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'unitPriceChangeSinceAdded' } },
+                                {
+                                    kind: 'Field',
+                                    name: { kind: 'Name', value: 'unitPriceWithTaxChangeSinceAdded' },
+                                },
+                                { kind: 'Field', name: { kind: 'Name', value: 'proratedUnitPriceWithTax' } },
+                                {
+                                    kind: 'Field',
+                                    name: { kind: 'Name', value: 'productVariant' },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [{ kind: 'Field', name: { kind: 'Name', value: 'id' } }],
+                                    },
+                                },
+                                {
+                                    kind: 'Field',
+                                    name: { kind: 'Name', value: 'discounts' },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            {
+                                                kind: 'Field',
+                                                name: { kind: 'Name', value: 'adjustmentSource' },
+                                            },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'amount' } },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'amountWithTax' } },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'description' } },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'type' } },
+                                        ],
+                                    },
+                                },
+                            ],
+                        },
+                    },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'shippingAddress' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'fullName' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'company' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'streetLine1' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'streetLine2' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'city' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'province' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'postalCode' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'country' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'phoneNumber' } },
+                            ],
+                        },
+                    },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'shippingLines' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                {
+                                    kind: 'Field',
+                                    name: { kind: 'Name', value: 'shippingMethod' },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'code' } },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'description' } },
+                                        ],
+                                    },
+                                },
+                            ],
+                        },
+                    },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'customer' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'emailAddress' } },
+                                {
+                                    kind: 'Field',
+                                    name: { kind: 'Name', value: 'user' },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'identifier' } },
+                                        ],
+                                    },
+                                },
+                            ],
+                        },
+                    },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'history' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                {
+                                    kind: 'Field',
+                                    name: { kind: 'Name', value: 'items' },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'type' } },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'data' } },
+                                        ],
+                                    },
+                                },
+                            ],
+                        },
+                    },
+                ],
+            },
+        },
+    ],
+} as unknown as DocumentNode<SetShippingAddressMutation, SetShippingAddressMutationVariables>;
+export const GetShippingMethodsDocument = {
+    kind: 'Document',
+    definitions: [
+        {
+            kind: 'OperationDefinition',
+            operation: 'query',
+            name: { kind: 'Name', value: 'GetShippingMethods' },
+            selectionSet: {
+                kind: 'SelectionSet',
+                selections: [
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'eligibleShippingMethods' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'code' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'price' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'name' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'description' } },
+                            ],
+                        },
+                    },
+                ],
+            },
+        },
+    ],
+} as unknown as DocumentNode<GetShippingMethodsQuery, GetShippingMethodsQueryVariables>;
+export const TransitionToStateDocument = {
+    kind: 'Document',
     definitions: [
         {
             kind: 'OperationDefinition',
@@ -4716,6 +5119,24 @@ export const SetShippingMethodDocument = {
                             ],
                         },
                     },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'shippingAddress' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'fullName' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'company' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'streetLine1' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'streetLine2' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'city' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'province' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'postalCode' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'country' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'phoneNumber' } },
+                            ],
+                        },
+                    },
                     {
                         kind: 'Field',
                         name: { kind: 'Name', value: 'shippingLines' },
@@ -4984,6 +5405,24 @@ export const AddItemToOrderDocument = {
                             ],
                         },
                     },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'shippingAddress' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'fullName' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'company' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'streetLine1' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'streetLine2' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'city' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'province' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'postalCode' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'country' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'phoneNumber' } },
+                            ],
+                        },
+                    },
                     {
                         kind: 'Field',
                         name: { kind: 'Name', value: 'shippingLines' },
@@ -5249,6 +5688,24 @@ export const AdjustOrderLineDocument = {
                             ],
                         },
                     },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'shippingAddress' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'fullName' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'company' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'streetLine1' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'streetLine2' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'city' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'province' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'postalCode' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'country' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'phoneNumber' } },
+                            ],
+                        },
+                    },
                     {
                         kind: 'Field',
                         name: { kind: 'Name', value: 'shippingLines' },
@@ -5455,6 +5912,24 @@ export const GetOrderByCodeDocument = {
                             ],
                         },
                     },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'shippingAddress' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'fullName' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'company' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'streetLine1' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'streetLine2' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'city' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'province' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'postalCode' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'country' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'phoneNumber' } },
+                            ],
+                        },
+                    },
                     {
                         kind: 'Field',
                         name: { kind: 'Name', value: 'shippingLines' },
@@ -5644,6 +6119,24 @@ export const GetActiveOrderDocument = {
                             ],
                         },
                     },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'shippingAddress' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'fullName' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'company' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'streetLine1' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'streetLine2' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'city' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'province' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'postalCode' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'country' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'phoneNumber' } },
+                            ],
+                        },
+                    },
                     {
                         kind: 'Field',
                         name: { kind: 'Name', value: 'shippingLines' },
@@ -5713,3 +6206,241 @@ export const GetActiveOrderDocument = {
         },
     ],
 } as unknown as DocumentNode<GetActiveOrderQuery, GetActiveOrderQueryVariables>;
+export const ApplyCouponCodeDocument = {
+    kind: 'Document',
+    definitions: [
+        {
+            kind: 'OperationDefinition',
+            operation: 'mutation',
+            name: { kind: 'Name', value: 'ApplyCouponCode' },
+            variableDefinitions: [
+                {
+                    kind: 'VariableDefinition',
+                    variable: { kind: 'Variable', name: { kind: 'Name', value: 'couponCode' } },
+                    type: {
+                        kind: 'NonNullType',
+                        type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } },
+                    },
+                },
+            ],
+            selectionSet: {
+                kind: 'SelectionSet',
+                selections: [
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'applyCouponCode' },
+                        arguments: [
+                            {
+                                kind: 'Argument',
+                                name: { kind: 'Name', value: 'couponCode' },
+                                value: { kind: 'Variable', name: { kind: 'Name', value: 'couponCode' } },
+                            },
+                        ],
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                {
+                                    kind: 'FragmentSpread',
+                                    name: { kind: 'Name', value: 'TestOrderFragment' },
+                                },
+                                {
+                                    kind: 'InlineFragment',
+                                    typeCondition: {
+                                        kind: 'NamedType',
+                                        name: { kind: 'Name', value: 'ErrorResult' },
+                                    },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            { kind: 'Field', name: { kind: 'Name', value: 'errorCode' } },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'message' } },
+                                        ],
+                                    },
+                                },
+                            ],
+                        },
+                    },
+                ],
+            },
+        },
+        {
+            kind: 'FragmentDefinition',
+            name: { kind: 'Name', value: 'TestOrderFragment' },
+            typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Order' } },
+            selectionSet: {
+                kind: 'SelectionSet',
+                selections: [
+                    { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'code' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'state' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'active' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'subTotal' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'subTotalWithTax' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'shipping' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'shippingWithTax' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'total' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'totalWithTax' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'currencyCode' } },
+                    { kind: 'Field', name: { kind: 'Name', value: 'couponCodes' } },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'discounts' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'adjustmentSource' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'amount' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'amountWithTax' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'description' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'type' } },
+                            ],
+                        },
+                    },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'payments' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'transactionId' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'method' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'amount' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'state' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'metadata' } },
+                            ],
+                        },
+                    },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'lines' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'quantity' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'linePrice' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'linePriceWithTax' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'unitPrice' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'unitPriceWithTax' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'unitPriceChangeSinceAdded' } },
+                                {
+                                    kind: 'Field',
+                                    name: { kind: 'Name', value: 'unitPriceWithTaxChangeSinceAdded' },
+                                },
+                                { kind: 'Field', name: { kind: 'Name', value: 'proratedUnitPriceWithTax' } },
+                                {
+                                    kind: 'Field',
+                                    name: { kind: 'Name', value: 'productVariant' },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [{ kind: 'Field', name: { kind: 'Name', value: 'id' } }],
+                                    },
+                                },
+                                {
+                                    kind: 'Field',
+                                    name: { kind: 'Name', value: 'discounts' },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            {
+                                                kind: 'Field',
+                                                name: { kind: 'Name', value: 'adjustmentSource' },
+                                            },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'amount' } },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'amountWithTax' } },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'description' } },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'type' } },
+                                        ],
+                                    },
+                                },
+                            ],
+                        },
+                    },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'shippingAddress' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'fullName' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'company' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'streetLine1' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'streetLine2' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'city' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'province' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'postalCode' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'country' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'phoneNumber' } },
+                            ],
+                        },
+                    },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'shippingLines' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                {
+                                    kind: 'Field',
+                                    name: { kind: 'Name', value: 'shippingMethod' },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'code' } },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'description' } },
+                                        ],
+                                    },
+                                },
+                            ],
+                        },
+                    },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'customer' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'emailAddress' } },
+                                {
+                                    kind: 'Field',
+                                    name: { kind: 'Name', value: 'user' },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'identifier' } },
+                                        ],
+                                    },
+                                },
+                            ],
+                        },
+                    },
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'history' },
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                {
+                                    kind: 'Field',
+                                    name: { kind: 'Name', value: 'items' },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'type' } },
+                                            { kind: 'Field', name: { kind: 'Name', value: 'data' } },
+                                        ],
+                                    },
+                                },
+                            ],
+                        },
+                    },
+                ],
+            },
+        },
+    ],
+} as unknown as DocumentNode<ApplyCouponCodeMutation, ApplyCouponCodeMutationVariables>;

+ 26 - 11
packages/payments-plugin/e2e/graphql/shop-queries.ts

@@ -50,6 +50,17 @@ export const TEST_ORDER_FRAGMENT = gql`
                 type
             }
         }
+        shippingAddress {
+            fullName
+            company
+            streetLine1
+            streetLine2
+            city
+            province
+            postalCode
+            country
+            phoneNumber
+        }
         shippingLines {
             shippingMethod {
                 id
@@ -104,17 +115,7 @@ export const SET_SHIPPING_ADDRESS = gql`
     mutation SetShippingAddress($input: CreateAddressInput!) {
         setOrderShippingAddress(input: $input) {
             ... on Order {
-                shippingAddress {
-                    fullName
-                    company
-                    streetLine1
-                    streetLine2
-                    city
-                    province
-                    postalCode
-                    country
-                    phoneNumber
-                }
+                ...TestOrderFragment
             }
             ... on ErrorResult {
                 errorCode
@@ -122,6 +123,7 @@ export const SET_SHIPPING_ADDRESS = gql`
             }
         }
     }
+    ${TEST_ORDER_FRAGMENT}
 `;
 
 export const GET_ELIGIBLE_SHIPPING_METHODS = gql`
@@ -221,3 +223,16 @@ export const GET_ACTIVE_ORDER = gql`
     }
     ${TEST_ORDER_FRAGMENT}
 `;
+
+export const APPLY_COUPON_CODE = gql`
+    mutation ApplyCouponCode($couponCode: String!) {
+        applyCouponCode(couponCode: $couponCode) {
+            ...TestOrderFragment
+            ... on ErrorResult {
+                errorCode
+                message
+            }
+        }
+    }
+    ${TEST_ORDER_FRAGMENT}
+`;

+ 19 - 50
packages/payments-plugin/e2e/mollie-dev-server.ts

@@ -1,14 +1,6 @@
 import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
-import {
-    ChannelService,
-    DefaultLogger,
-    DefaultSearchPlugin,
-    LogLevel,
-    mergeConfig,
-    RequestContext,
-} from '@vendure/core';
+import { DefaultLogger, DefaultSearchPlugin, LogLevel, mergeConfig } from '@vendure/core';
 import { createTestEnvironment, registerInitializer, SqljsInitializer, testConfig } from '@vendure/testing';
-import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
 import gql from 'graphql-tag';
 import localtunnel from 'localtunnel';
 import path from 'path';
@@ -23,9 +15,13 @@ import {
     CreatePaymentMethodMutationVariables,
     LanguageCode,
 } from './graphql/generated-admin-types';
-import { AddItemToOrderMutation, AddItemToOrderMutationVariables } from './graphql/generated-shop-types';
-import { ADD_ITEM_TO_ORDER, ADJUST_ORDER_LINE } from './graphql/shop-queries';
-import { CREATE_MOLLIE_PAYMENT_INTENT, setShipping } from './payment-helpers';
+import { ADD_ITEM_TO_ORDER, APPLY_COUPON_CODE } from './graphql/shop-queries';
+import {
+    CREATE_MOLLIE_PAYMENT_INTENT,
+    createFixedDiscountCoupon,
+    createFreeShippingCoupon,
+    setShipping,
+} from './payment-helpers';
 
 /**
  * This should only be used to locally test the Mollie payment plugin
@@ -99,50 +95,23 @@ async function runMollieDevServer() {
             },
         },
     );
-    // Prepare order with 2 items
+    // Prepare a test order where the total is 0
     await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
-    // Add another item to the order
-    await shopClient.query<AddItemToOrderMutation, AddItemToOrderMutationVariables>(ADD_ITEM_TO_ORDER, {
-        productVariantId: 'T_4',
-        quantity: 1,
-    });
-    await shopClient.query<AddItemToOrderMutation, AddItemToOrderMutationVariables>(ADD_ITEM_TO_ORDER, {
-        productVariantId: 'T_5',
+    await shopClient.query(ADD_ITEM_TO_ORDER, {
+        productVariantId: 'T_1',
         quantity: 1,
     });
     await setShipping(shopClient);
-    // Create payment intent
-    // Create payment intent
-    const { createMolliePaymentIntent } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
-        input: {
-            redirectUrl: `${tunnel.url}/admin/orders?filter=open&page=1`,
-            paymentMethodCode: 'mollie',
-            //            molliePaymentMethodCode: 'klarnapaylater'
-        },
-    });
-    if (createMolliePaymentIntent.errorCode) {
-        throw createMolliePaymentIntent;
-    }
-    // eslint-disable-next-line no-console
-    console.log('\x1b[41m', `Mollie payment link: ${createMolliePaymentIntent.url as string}`, '\x1b[0m');
-
-    // Remove first orderLine
-    await shopClient.query(ADJUST_ORDER_LINE, {
-        orderLineId: 'T_1',
-        quantity: 0,
-    });
-    await setShipping(shopClient);
+    // Comment out these lines if you want to test the payment flow via Mollie
+    await createFixedDiscountCoupon(adminClient, 156880, 'DISCOUNT_ORDER');
+    await createFreeShippingCoupon(adminClient, 'FREE_SHIPPING');
+    await shopClient.query(APPLY_COUPON_CODE, { couponCode: 'DISCOUNT_ORDER' });
+    await shopClient.query(APPLY_COUPON_CODE, { couponCode: 'FREE_SHIPPING' });
 
-    // Create another intent after Xs, should update the mollie order
-    await new Promise(resolve => setTimeout(resolve, 5000));
-    const { createMolliePaymentIntent: secondIntent } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
-        input: {
-            redirectUrl: `${tunnel.url}/admin/orders?filter=open&page=1&dynamicRedirectUrl=true`,
-            paymentMethodCode: 'mollie',
-        },
-    });
+    // Create Payment Intent
+    const result = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, { input: {} });
     // eslint-disable-next-line no-console
-    console.log('\x1b[41m', `Second payment link: ${secondIntent.url as string}`, '\x1b[0m');
+    console.log('Payment intent result', result);
 }
 
 (async () => {

+ 44 - 2
packages/payments-plugin/e2e/mollie-payment.e2e-spec.ts

@@ -4,6 +4,7 @@ import {
     EventBus,
     LanguageCode,
     mergeConfig,
+    Order,
     OrderPlacedEvent,
     OrderService,
     RequestContext,
@@ -44,10 +45,17 @@ import {
     GetOrderByCodeQueryVariables,
     TestOrderFragmentFragment,
 } from './graphql/generated-shop-types';
-import { ADD_ITEM_TO_ORDER, GET_ORDER_BY_CODE } from './graphql/shop-queries';
+import {
+    ADD_ITEM_TO_ORDER,
+    APPLY_COUPON_CODE,
+    GET_ACTIVE_ORDER,
+    GET_ORDER_BY_CODE,
+} from './graphql/shop-queries';
 import {
     addManualPayment,
     CREATE_MOLLIE_PAYMENT_INTENT,
+    createFixedDiscountCoupon,
+    createFreeShippingCoupon,
     GET_MOLLIE_PAYMENT_METHODS,
     refundOrderLine,
     setShipping,
@@ -196,7 +204,7 @@ describe('Mollie payments', () => {
                 authorizedAsOwnerOnly: false,
                 channel: await server.app.get(ChannelService).getDefaultChannel(),
             });
-            await server.app.get(OrderService).addSurchargeToOrder(ctx, 1, {
+            await server.app.get(OrderService).addSurchargeToOrder(ctx, order.id.replace('T_', ''), {
                 description: 'Negative test surcharge',
                 listPrice: SURCHARGE_AMOUNT,
             });
@@ -441,6 +449,34 @@ describe('Mollie payments', () => {
             expect(method.maximumAmount).toBeDefined();
             expect(method.image).toBeDefined();
         });
+
+        it('Transitions to PaymentSettled for orders with a total of $0', async () => {
+            await shopClient.asUserWithCredentials(customers[1].emailAddress, 'test');
+            const { addItemToOrder } = await shopClient.query(ADD_ITEM_TO_ORDER, {
+                productVariantId: 'T_1',
+                quantity: 1,
+            });
+            await setShipping(shopClient);
+            // Discount the order so it has a total of $0
+            await createFixedDiscountCoupon(adminClient, 156880, 'DISCOUNT_ORDER');
+            await createFreeShippingCoupon(adminClient, 'FREE_SHIPPING');
+            await shopClient.query(APPLY_COUPON_CODE, { couponCode: 'DISCOUNT_ORDER' });
+            await shopClient.query(APPLY_COUPON_CODE, { couponCode: 'FREE_SHIPPING' });
+            // Create payment intent
+            const { createMolliePaymentIntent: intent } = await shopClient.query(
+                CREATE_MOLLIE_PAYMENT_INTENT,
+                {
+                    input: {
+                        paymentMethodCode: mockData.methodCode,
+                        redirectUrl: 'https://my-storefront.io/order-confirmation',
+                    },
+                },
+            );
+            const { orderByCode } = await shopClient.query(GET_ORDER_BY_CODE, { code: addItemToOrder.code });
+            expect(intent.url).toBe('https://my-storefront.io/order-confirmation');
+            expect(orderByCode.totalWithTax).toBe(0);
+            expect(orderByCode.state).toBe('PaymentSettled');
+        });
     });
 
     describe('Handle standard payment methods', () => {
@@ -486,6 +522,7 @@ describe('Mollie payments', () => {
                 body: JSON.stringify({ id: mockData.mollieOrderResponse.id }),
                 headers: { 'Content-Type': 'application/json' },
             });
+            await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
             const { orderByCode } = await shopClient.query<GetOrderByCodeQuery, GetOrderByCodeQueryVariables>(
                 GET_ORDER_BY_CODE,
                 {
@@ -502,6 +539,11 @@ describe('Mollie payments', () => {
             expect(orderPlacedEvent?.ctx.languageCode).toBe('nl');
         });
 
+        it('Resulting events should have a ctx.req ', () => {
+            // We've set the languageCode to 'nl' in the mock response's metadata
+            expect(orderPlacedEvent?.ctx?.req).toBeDefined();
+        });
+
         it('Should have Mollie metadata on payment', async () => {
             const {
                 order: { payments },

+ 113 - 13
packages/payments-plugin/e2e/payment-helpers.ts

@@ -1,10 +1,21 @@
 import { ID } from '@vendure/common/lib/shared-types';
-import { ChannelService, OrderService, PaymentService, RequestContext } from '@vendure/core';
+import {
+    ChannelService,
+    ErrorResult,
+    OrderService,
+    PaymentService,
+    RequestContext,
+    assertFound,
+} from '@vendure/core';
 import { SimpleGraphQLClient, TestServer } from '@vendure/testing';
 import gql from 'graphql-tag';
 
-import { REFUND_ORDER } from './graphql/admin-queries';
-import { RefundFragment, RefundOrderMutation, RefundOrderMutationVariables } from './graphql/generated-admin-types';
+import { CREATE_COUPON, REFUND_ORDER } from './graphql/admin-queries';
+import {
+    RefundFragment,
+    RefundOrderMutation,
+    RefundOrderMutationVariables,
+} from './graphql/generated-admin-types';
 import {
     GetShippingMethodsQuery,
     SetShippingMethodMutation,
@@ -21,7 +32,7 @@ import {
 } from './graphql/shop-queries';
 
 export async function setShipping(shopClient: SimpleGraphQLClient): Promise<void> {
-    await shopClient.query(SET_SHIPPING_ADDRESS, {
+    const { setOrderShippingAddress: order } = await shopClient.query(SET_SHIPPING_ADDRESS, {
         input: {
             fullName: 'name',
             streetLine1: '12 the street',
@@ -33,9 +44,17 @@ export async function setShipping(shopClient: SimpleGraphQLClient): Promise<void
     const { eligibleShippingMethods } = await shopClient.query<GetShippingMethodsQuery>(
         GET_ELIGIBLE_SHIPPING_METHODS,
     );
-    await shopClient.query<SetShippingMethodMutation, SetShippingMethodMutationVariables>(SET_SHIPPING_METHOD, {
-        id: eligibleShippingMethods[1].id,
-    });
+    if (!eligibleShippingMethods?.length) {
+        throw Error(
+            `No eligible shipping methods found for order '${String(order.code)}' with a total of '${String(order.totalWithTax)}'`,
+        );
+    }
+    await shopClient.query<SetShippingMethodMutation, SetShippingMethodMutationVariables>(
+        SET_SHIPPING_METHOD,
+        {
+            id: eligibleShippingMethods[1].id,
+        },
+    );
 }
 
 export async function proceedToArrangingPayment(shopClient: SimpleGraphQLClient): Promise<ID> {
@@ -78,18 +97,98 @@ export async function addManualPayment(server: TestServer, orderId: ID, amount:
         authorizedAsOwnerOnly: false,
         channel: await server.app.get(ChannelService).getDefaultChannel(),
     });
-    const order = await server.app.get(OrderService).findOne(ctx, orderId);
+    const order = await assertFound(server.app.get(OrderService).findOne(ctx, orderId));
     // tslint:disable-next-line:no-non-null-assertion
-    await server.app.get(PaymentService).createManualPayment(ctx, order!, amount, {
+    await server.app.get(PaymentService).createManualPayment(ctx, order, amount, {
         method: 'Gift card',
-        // tslint:disable-next-line:no-non-null-assertion
-        orderId: order!.id,
+        orderId: order.id,
         metadata: {
             bogus: 'test',
         },
     });
 }
 
+/**
+ * Create a coupon with the given code and discount amount.
+ */
+export async function createFixedDiscountCoupon(
+    adminClient: SimpleGraphQLClient,
+    amount: number,
+    couponCode: string,
+): Promise<void> {
+    const { createPromotion } = await adminClient.query(CREATE_COUPON, {
+        input: {
+            conditions: [],
+            actions: [
+                {
+                    code: 'order_fixed_discount',
+                    arguments: [
+                        {
+                            name: 'discount',
+                            value: String(amount),
+                        },
+                    ],
+                },
+            ],
+            couponCode,
+            startsAt: null,
+            endsAt: null,
+            perCustomerUsageLimit: null,
+            usageLimit: null,
+            enabled: true,
+            translations: [
+                {
+                    languageCode: 'en',
+                    name: `Coupon ${couponCode}`,
+                    description: '',
+                    customFields: {},
+                },
+            ],
+            customFields: {},
+        },
+    });
+    if (createPromotion.__typename === 'ErrorResult') {
+        throw new Error(`Error creating coupon: ${(createPromotion as ErrorResult).errorCode}`);
+    }
+}
+/**
+ * Create a coupon that discounts the shipping costs
+ */
+export async function createFreeShippingCoupon(
+    adminClient: SimpleGraphQLClient,
+    couponCode: string,
+): Promise<void> {
+    const { createPromotion } = await adminClient.query(CREATE_COUPON, {
+        input: {
+            conditions: [],
+            actions: [
+                {
+                    code: 'free_shipping',
+                    arguments: [],
+                },
+            ],
+            couponCode,
+            startsAt: null,
+            endsAt: null,
+            perCustomerUsageLimit: null,
+            usageLimit: null,
+            enabled: true,
+            translations: [
+                {
+                    languageCode: 'en',
+                    name: `Coupon ${couponCode}`,
+                    description: '',
+                    customFields: {},
+                },
+            ],
+            customFields: {},
+        },
+    });
+    if (createPromotion.__typename === 'ErrorResult') {
+        throw new Error(`Error creating coupon: ${(createPromotion as ErrorResult).errorCode}`);
+    }
+}
+
 export const CREATE_MOLLIE_PAYMENT_INTENT = gql`
     mutation createMolliePaymentIntent($input: MolliePaymentIntentInput!) {
         createMolliePaymentIntent(input: $input) {
@@ -105,9 +204,10 @@ export const CREATE_MOLLIE_PAYMENT_INTENT = gql`
 `;
 
 export const CREATE_STRIPE_PAYMENT_INTENT = gql`
-    mutation createStripePaymentIntent{
+    mutation createStripePaymentIntent {
         createStripePaymentIntent
-    }`;
+    }
+`;
 
 export const GET_MOLLIE_PAYMENT_METHODS = gql`
     query molliePaymentMethods($input: MolliePaymentMethodsInput!) {

+ 4 - 4
packages/payments-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/payments-plugin",
-    "version": "2.2.4",
+    "version": "2.2.5",
     "license": "MIT",
     "main": "package/index.js",
     "types": "package/index.d.ts",
@@ -46,9 +46,9 @@
         "@mollie/api-client": "^3.7.0",
         "@types/braintree": "^3.3.11",
         "@types/localtunnel": "2.0.4",
-        "@vendure/common": "^2.2.4",
-        "@vendure/core": "^2.2.4",
-        "@vendure/testing": "^2.2.4",
+        "@vendure/common": "^2.2.5",
+        "@vendure/core": "^2.2.5",
+        "@vendure/testing": "^2.2.5",
         "braintree": "^3.22.0",
         "localtunnel": "2.0.2",
         "nock": "^13.1.4",

+ 10 - 4
packages/payments-plugin/src/mollie/mollie.controller.ts

@@ -1,12 +1,16 @@
-import { Body, Controller, Param, Post } from '@nestjs/common';
+import { Body, Controller, Param, Post, Req } from '@nestjs/common';
 import { Ctx, Logger, RequestContext, Transaction, ChannelService, LanguageCode } from '@vendure/core';
+import { Request } from 'express';
 
 import { loggerCtx } from './constants';
 import { MollieService } from './mollie.service';
 
 @Controller('payments')
 export class MollieController {
-    constructor(private mollieService: MollieService, private channelService: ChannelService) {}
+    constructor(
+        private mollieService: MollieService,
+        private channelService: ChannelService,
+    ) {}
 
     @Post('mollie/:channelToken/:paymentMethodId')
     @Transaction()
@@ -14,6 +18,7 @@ export class MollieController {
         @Param('channelToken') channelToken: string,
         @Param('paymentMethodId') paymentMethodId: string,
         @Body() body: any,
+        @Req() req: Request,
     ): Promise<void> {
         if (!body.id) {
             return Logger.warn(' Ignoring incoming webhook, because it has no body.id.', loggerCtx);
@@ -21,7 +26,7 @@ export class MollieController {
         try {
             // We need to construct a RequestContext based on the channelToken,
             // because this is an incoming webhook, not a graphql request with a valid Ctx
-            const ctx = await this.createContext(channelToken);
+            const ctx = await this.createContext(channelToken, req);
             await this.mollieService.handleMollieStatusUpdate(ctx, {
                 paymentMethodId,
                 orderId: body.id,
@@ -36,13 +41,14 @@ export class MollieController {
         }
     }
 
-    private async createContext(channelToken: string): Promise<RequestContext> {
+    private async createContext(channelToken: string, req: Request): Promise<RequestContext> {
         const channel = await this.channelService.getChannelFromToken(channelToken);
         return new RequestContext({
             apiType: 'admin',
             isAuthorized: true,
             authorizedAsOwnerOnly: false,
             channel,
+            req,
             languageCode: LanguageCode.en,
         });
     }

+ 9 - 4
packages/payments-plugin/src/mollie/mollie.handler.ts

@@ -1,4 +1,9 @@
-import createMollieClient, { OrderEmbed, PaymentStatus, RefundStatus } from '@mollie/api-client';
+import createMollieClient, {
+    OrderEmbed,
+    PaymentStatus,
+    RefundStatus,
+    Order as MollieOrder,
+} from '@mollie/api-client';
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 import {
     CreatePaymentErrorResult,
@@ -57,13 +62,13 @@ export const molliePaymentHandler = new PaymentMethodHandler({
     init(injector) {
         mollieService = injector.get(MollieService);
     },
-    createPayment: async (
+    createPayment: (
         ctx,
         order,
-        _amount, // Don't use this amount, but the amount from the metadata
+        _amount, // Don't use this amount, but the amount from the metadata, because that has the actual paid amount from Mollie
         args,
         metadata,
-    ): Promise<CreatePaymentResult | CreatePaymentErrorResult> => {
+    ): CreatePaymentResult | CreatePaymentErrorResult => {
         // Only Admins and internal calls should be allowed to settle and authorize payments
         if (ctx.apiType !== 'admin' && ctx.apiType !== 'custom') {
             throw Error(`CreatePayment is not allowed for apiType '${ctx.apiType}'`);

+ 36 - 11
packages/payments-plugin/src/mollie/mollie.service.ts

@@ -176,6 +176,28 @@ export class MollieService {
         }
         const alreadyPaid = totalCoveredByPayments(order);
         const amountToPay = order.totalWithTax - alreadyPaid;
+        if (amountToPay === 0) {
+            // The order can be transitioned to PaymentSettled, because the order has 0 left to pay
+            // Only admins can add payments, so we need an admin ctx
+            const adminCtx = new RequestContext({
+                apiType: 'admin',
+                isAuthorized: true,
+                authorizedAsOwnerOnly: false,
+                channel: ctx.channel,
+                languageCode: ctx.languageCode,
+            });
+            await this.addPayment(
+                adminCtx,
+                order,
+                amountToPay,
+                { method: 'Settled without Mollie' },
+                paymentMethod.code,
+                'Settled',
+            );
+            return {
+                url: redirectUrl,
+            };
+        }
         const orderInput: CreateParameters = {
             orderNumber: order.code,
             amount: toAmount(amountToPay, order.currencyCode),
@@ -260,6 +282,7 @@ export class MollieService {
                 apiType: 'admin',
                 isAuthorized: true,
                 authorizedAsOwnerOnly: false,
+                req: ctx.req,
                 channel: ctx.channel,
                 languageCode: mollieOrder.metadata.languageCode as LanguageCode,
             });
@@ -289,17 +312,18 @@ export class MollieService {
             );
             return;
         }
+        const amount = amountToCents(mollieOrder.amount);
         if (mollieOrder.status === OrderStatus.expired) {
             // Expired is fine, a customer can retry the payment later
             return;
         }
         if (mollieOrder.status === OrderStatus.paid) {
             // Paid is only used by 1-step payments without Authorized state. This will settle immediately
-            await this.addPayment(ctx, order, mollieOrder, paymentMethod.code, 'Settled');
+            await this.addPayment(ctx, order, amount, mollieOrder, paymentMethod.code, 'Settled');
             return;
         }
         if (order.state === 'AddingItems' && mollieOrder.status === OrderStatus.authorized) {
-            order = await this.addPayment(ctx, order, mollieOrder, paymentMethod.code, 'Authorized');
+            order = await this.addPayment(ctx, order, amount, mollieOrder, paymentMethod.code, 'Authorized');
             if (autoCapture && mollieOrder.status === OrderStatus.authorized) {
                 // Immediately capture payment if autoCapture is set
                 Logger.info(`Auto capturing payment for order ${order.code}`, loggerCtx);
@@ -327,7 +351,8 @@ export class MollieService {
     async addPayment(
         ctx: RequestContext,
         order: Order,
-        mollieOrder: MollieOrder,
+        amount: number,
+        mollieMetadata: Partial<MollieOrder>,
         paymentMethodCode: string,
         status: 'Authorized' | 'Settled',
     ): Promise<Order> {
@@ -347,15 +372,15 @@ export class MollieService {
         const addPaymentToOrderResult = await this.orderService.addPaymentToOrder(ctx, order.id, {
             method: paymentMethodCode,
             metadata: {
-                amount: amountToCents(mollieOrder.amount),
+                amount,
                 status,
-                orderId: mollieOrder.id,
-                mode: mollieOrder.mode,
-                method: mollieOrder.method,
-                profileId: mollieOrder.profileId,
-                settlementAmount: mollieOrder.amount,
-                authorizedAt: mollieOrder.authorizedAt,
-                paidAt: mollieOrder.paidAt,
+                orderId: mollieMetadata.id,
+                mode: mollieMetadata.mode,
+                method: mollieMetadata.method,
+                profileId: mollieMetadata.profileId,
+                settlementAmount: mollieMetadata.amount,
+                authorizedAt: mollieMetadata.authorizedAt,
+                paidAt: mollieMetadata.paidAt,
             },
         });
         if (!(addPaymentToOrderResult instanceof Order)) {

+ 3 - 3
packages/sentry-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/sentry-plugin",
-    "version": "2.2.4",
+    "version": "2.2.5",
     "license": "MIT",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -22,7 +22,7 @@
     },
     "devDependencies": {
         "@sentry/node": "^7.106.1",
-        "@vendure/common": "^2.2.4",
-        "@vendure/core": "^2.2.4"
+        "@vendure/common": "^2.2.5",
+        "@vendure/core": "^2.2.5"
     }
 }

+ 3 - 3
packages/stellate-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/stellate-plugin",
-    "version": "2.2.4",
+    "version": "2.2.5",
     "license": "MIT",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -21,7 +21,7 @@
         "node-fetch": "^2.7.0"
     },
     "devDependencies": {
-        "@vendure/common": "^2.2.4",
-        "@vendure/core": "^2.2.4"
+        "@vendure/common": "^2.2.5",
+        "@vendure/core": "^2.2.5"
     }
 }

+ 3 - 3
packages/testing/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/testing",
-    "version": "2.2.4",
+    "version": "2.2.5",
     "description": "End-to-end testing tools for Vendure projects",
     "keywords": [
         "vendure",
@@ -37,7 +37,7 @@
     },
     "dependencies": {
         "@graphql-typed-document-node/core": "^3.2.0",
-        "@vendure/common": "^2.2.4",
+        "@vendure/common": "^2.2.5",
         "faker": "^4.1.0",
         "form-data": "^4.0.0",
         "graphql": "16.8.1",
@@ -50,7 +50,7 @@
         "@types/mysql": "^2.15.26",
         "@types/node-fetch": "^2.6.4",
         "@types/pg": "^8.11.2",
-        "@vendure/core": "^2.2.4",
+        "@vendure/core": "^2.2.5",
         "mysql": "^2.18.1",
         "pg": "^8.11.3",
         "rimraf": "^5.0.5",

+ 4 - 4
packages/ui-devkit/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/ui-devkit",
-    "version": "2.2.4",
+    "version": "2.2.5",
     "description": "A library for authoring Vendure Admin UI extensions",
     "keywords": [
         "vendure",
@@ -40,8 +40,8 @@
         "@angular/cli": "^17.2.3",
         "@angular/compiler": "^17.2.4",
         "@angular/compiler-cli": "^17.2.4",
-        "@vendure/admin-ui": "^2.2.4",
-        "@vendure/common": "^2.2.4",
+        "@vendure/admin-ui": "^2.2.5",
+        "@vendure/common": "^2.2.5",
         "chalk": "^4.1.0",
         "chokidar": "^3.6.0",
         "fs-extra": "^11.2.0",
@@ -52,7 +52,7 @@
         "@rollup/plugin-node-resolve": "^15.2.3",
         "@rollup/plugin-terser": "^0.4.4",
         "@types/fs-extra": "^11.0.4",
-        "@vendure/core": "^2.2.4",
+        "@vendure/core": "^2.2.5",
         "react": "^18.2.0",
         "react-dom": "^18.2.0",
         "rimraf": "^5.0.5",

+ 1 - 0
scripts/codegen/generate-graphql-types.ts

@@ -31,6 +31,7 @@ const specFileToIgnore = [
     'relations-decorator.e2e-spec',
     'active-order-strategy.e2e-spec',
     'error-handler-strategy.e2e-spec',
+    'order-multi-vendor.e2e-spec',
 ];
 const E2E_ADMIN_QUERY_FILES = path.join(
     __dirname,