Просмотр исходного кода

Merge branch 'master' into minor

Michael Bromley 1 год назад
Родитель
Сommit
107e1c7763
37 измененных файлов с 833 добавлено и 403 удалено
  1. 26 0
      CHANGELOG.md
  2. 6 2
      docs/docs/guides/developer-guide/plugins/index.mdx
  3. 405 0
      docs/docs/guides/how-to/publish-plugin/index.mdx
  4. 1 1
      lerna.json
  5. 8 0
      license/signatures/version1/cla.json
  6. 56 56
      package-lock.json
  7. 4 4
      packages/admin-ui-plugin/package.json
  8. 2 2
      packages/admin-ui/package.json
  9. 1 1
      packages/admin-ui/src/lib/core/src/common/version.ts
  10. 3 3
      packages/asset-server-plugin/package.json
  11. 3 3
      packages/cli/package.json
  12. 1 1
      packages/common/package.json
  13. 47 2
      packages/core/e2e/entity-hydrator.e2e-spec.ts
  14. 58 2
      packages/core/e2e/fixtures/test-plugins/hydration-test-plugin.ts
  15. 72 0
      packages/core/e2e/order-modification.e2e-spec.ts
  16. 2 2
      packages/core/package.json
  17. 11 0
      packages/core/src/api/resolvers/entity/user-entity.resolver.ts
  18. 3 3
      packages/core/src/service/helpers/order-calculator/order-calculator.ts
  19. 7 2
      packages/core/src/service/helpers/utils/tree-relations-qb-joiner.ts
  20. 3 3
      packages/create/package.json
  21. 9 9
      packages/dev-server/package.json
  22. 3 3
      packages/elasticsearch-plugin/package.json
  23. 3 3
      packages/email-plugin/package.json
  24. 3 3
      packages/harden-plugin/package.json
  25. 3 3
      packages/job-queue-plugin/package.json
  26. 0 0
      packages/payments-plugin/e2e/fixtures/stripe-checkout-test.plugin.ts
  27. 2 6
      packages/payments-plugin/e2e/mollie-dev-server.ts
  28. 52 68
      packages/payments-plugin/e2e/mollie-payment.e2e-spec.ts
  29. 4 4
      packages/payments-plugin/package.json
  30. 0 16
      packages/payments-plugin/src/mollie/custom-fields.ts
  31. 0 76
      packages/payments-plugin/src/mollie/extended-mollie-client.ts
  32. 0 2
      packages/payments-plugin/src/mollie/mollie.plugin.ts
  33. 22 110
      packages/payments-plugin/src/mollie/mollie.service.ts
  34. 3 3
      packages/sentry-plugin/package.json
  35. 3 3
      packages/stellate-plugin/package.json
  36. 3 3
      packages/testing/package.json
  37. 4 4
      packages/ui-devkit/package.json

+ 26 - 0
CHANGELOG.md

@@ -1,3 +1,29 @@
+## <small>3.0.1 (2024-08-21)</small>
+
+
+#### Fixes
+
+* **admin-ui** Add missing and revise portuguese and brazilian portuguese translations (#3002) ([9b5911f](https://github.com/vendure-ecommerce/vendure/commit/9b5911f)), closes [#3002](https://github.com/vendure-ecommerce/vendure/issues/3002)
+* **admin-ui** Fix overflow in channel assignment block (#2984) ([0f8bdb5](https://github.com/vendure-ecommerce/vendure/commit/0f8bdb5)), closes [#2984](https://github.com/vendure-ecommerce/vendure/issues/2984)
+* **admin-ui** Make sku optional in create-product-variant-dialog (#3007) ([13fe069](https://github.com/vendure-ecommerce/vendure/commit/13fe069)), closes [#3007](https://github.com/vendure-ecommerce/vendure/issues/3007) [#2999](https://github.com/vendure-ecommerce/vendure/issues/2999)
+* **admin-ui** Use correct 24hr format for locale in dates (#2972) ([f078b41](https://github.com/vendure-ecommerce/vendure/commit/f078b41)), closes [#2972](https://github.com/vendure-ecommerce/vendure/issues/2972) [#2970](https://github.com/vendure-ecommerce/vendure/issues/2970)
+* **core** Allow fulfillment creation with deleted product variants (#2982) ([752c2b6](https://github.com/vendure-ecommerce/vendure/commit/752c2b6)), closes [#2982](https://github.com/vendure-ecommerce/vendure/issues/2982) [#2434](https://github.com/vendure-ecommerce/vendure/issues/2434)
+* **core** Fix EntityHydrator error on long table names (#2959) ([bcfcf7d](https://github.com/vendure-ecommerce/vendure/commit/bcfcf7d)), closes [#2959](https://github.com/vendure-ecommerce/vendure/issues/2959) [#2899](https://github.com/vendure-ecommerce/vendure/issues/2899)
+* **core** Fix NaN error thrown when modifying pro-rated discounted OrderLine to 0 (#3009) ([fa50770](https://github.com/vendure-ecommerce/vendure/commit/fa50770)), closes [#3009](https://github.com/vendure-ecommerce/vendure/issues/3009)
+* **core** Make firstName and lastName required in CreateCustomerAndUser method (#2996) ([0d8054d](https://github.com/vendure-ecommerce/vendure/commit/0d8054d)), closes [#2996](https://github.com/vendure-ecommerce/vendure/issues/2996)
+* **core** Resolve User.roles field in GraphQL APIs (#3011) ([8f99b2d](https://github.com/vendure-ecommerce/vendure/commit/8f99b2d)), closes [#3011](https://github.com/vendure-ecommerce/vendure/issues/3011)
+* **core** Return type of collection breadcrumb was missing slug (#2960) ([620eeb1](https://github.com/vendure-ecommerce/vendure/commit/620eeb1)), closes [#2960](https://github.com/vendure-ecommerce/vendure/issues/2960)
+* **create** Dynamically find open port if 3000 in use ([a40fbb1](https://github.com/vendure-ecommerce/vendure/commit/a40fbb1))
+* **create** Fix typo (#2994) ([999e89e](https://github.com/vendure-ecommerce/vendure/commit/999e89e)), closes [#2994](https://github.com/vendure-ecommerce/vendure/issues/2994)
+* **create** Update EmailPlugin config to use templateLoader API ([6708440](https://github.com/vendure-ecommerce/vendure/commit/6708440)), closes [#2981](https://github.com/vendure-ecommerce/vendure/issues/2981)
+* **payments-plugin** Fix Mollie not calling webhook on updated orders (#3014) ([694845f](https://github.com/vendure-ecommerce/vendure/commit/694845f)), closes [#3014](https://github.com/vendure-ecommerce/vendure/issues/3014) [#2941](https://github.com/vendure-ecommerce/vendure/issues/2941)
+* **payments-plugin** Mollie - add missing request when settled amount is 0 (#2993) ([afd6435](https://github.com/vendure-ecommerce/vendure/commit/afd6435)), closes [#2993](https://github.com/vendure-ecommerce/vendure/issues/2993)
+
+#### Perf
+
+* **core** Improve hydrator performance for customFields (#2961) ([f40761d](https://github.com/vendure-ecommerce/vendure/commit/f40761d)), closes [#2961](https://github.com/vendure-ecommerce/vendure/issues/2961)
+* **core** Refactor applyCollectionFiltersInternal method to improve performance (#2978) ([6eeae1c](https://github.com/vendure-ecommerce/vendure/commit/6eeae1c)), closes [#2978](https://github.com/vendure-ecommerce/vendure/issues/2978)
+
 ## 3.0.0 (2024-07-17)
 
 Note: the changes in this release are identical to v2.3.0.

+ 6 - 2
docs/docs/guides/developer-guide/plugins/index.mdx

@@ -168,8 +168,6 @@ For any unit of functionality that you need to add to your project, you'll be cr
 For a complete working example of a Vendure plugin, see the [real-world-vendure Reviews plugin](https://github.com/vendure-ecommerce/real-world-vendure/tree/master/src/plugins/reviews)
 
 You can also use the [Vendure CLI](/guides/developer-guide/cli) to quickly scaffold a new plugin.
-
-If you intend to write a shared plugin to be distributed as an npm package, see the [vendure plugin-template repo](https://github.com/vendure-ecommerce/plugin-template)
 :::
 
 In this guide, we will implement a simple but fully-functional **wishlist plugin** step-by-step. The goal of this plugin is to allow signed-in customers to add products to a wishlist, and to view and manage their wishlist.
@@ -782,3 +780,9 @@ mutation RemoveFromWishlist {
   </TabItem>
 </Tabs>
 
+## Publishing plugins
+
+If you have created a plugin that you would like to share with the community, you can publish it to npm, and even
+have it listed on the [Vendure Hub](https://vendure.io/hub).
+
+For a full guide to publishing plugins, see the [Publishing a Plugin how-to guide](/guides/how-to/publish-plugin/).

+ 405 - 0
docs/docs/guides/how-to/publish-plugin/index.mdx

@@ -0,0 +1,405 @@
+---
+title: "Publishing a Plugin"
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+Vendure's [plugin-based architecture](/guides/developer-guide/plugins/) means you'll be writing a lot of plugins.
+Some of those plugins may be useful to others, and you may want to share them with the community.
+
+We have created [Vendure Hub](https://vendure.io/hub) as a central listing for high-quality Vendure plugins.
+
+This guide will walk you through the process of publishing a plugin to npm and submitting it to Vendure Hub.
+
+## Project setup
+
+There are a couple of ways you can structure your plugin project:
+
+### Repo structure
+
+#### Stand-alone repo
+
+You can have a single repository for your plugin. For this scenario you can use
+the [Vendure Plugin Template](https://github.com/vendure-ecommerce/plugin-template) as a starting point.
+
+**Pros**: simple to set up.
+
+**Cons**: if you have multiple plugins, you'll have multiple repositories to manage with duplicated setup and configuration.
+
+#### Monorepo
+
+If you have multiple plugins, you can use a monorepo setup. Tools such as [Lerna](https://lerna.js.org/) or
+[Nx](https://nx.dev/) can help you manage multiple packages in a single repository. A good example of this approach
+can be found in the [Pinelab plugins repo](https://github.com/Pinelab-studio/pinelab-vendure-plugins).
+
+**Pros**: single repository to manage; can scale to any number of plugins; can share configuration and tooling.
+
+**Cons**: Initial setup is more complex.
+
+### Plugin naming
+
+We recommend that you use [scoped packages](https://docs.npmjs.com/cli/v10/using-npm/scope#publishing-scoped-packages) for your plugins, which means
+they will be named like `@<scope>/<plugin-name>`. For example, if your company is called `acme`, and you are publishing a plugin that
+implements a loyalty points system, you could name it `@acme/vendure-plugin-loyalty-points`.
+
+### Dependencies
+
+Your plugin should **not** include Vendure packages as dependencies in the `package.json` file. You _may_ declare them as a peer dependencies, but this is not
+a must. The same goes for any of the transitive dependencies of Vendure core such as `@nestjs/graphql`, `@nestjs/common`, `typeorm` etc. You can assume
+that these dependencies will be available in the Vendure project that uses your plugin.
+
+As for version compatibility, you should use the
+[compatibility property](/guides/developer-guide/plugins/#step-7-specify-compatibility) in your plugin definition to ensure that the Vendure project
+is using a compatible version of Vendure.
+
+### License
+
+Your plugin **must** use a license that is compatible with the GPL v3 license that Vendure uses. This means your package.json
+file should include `"license": "GPL-3.0-or-later"`, and your repository should include a license file (usually named `LICENSE.txt`) containing the
+[full text of the GPL v3 license](https://www.gnu.org/licenses/gpl-3.0.txt).
+
+## Publishing to npm
+
+Once your plugin is ready, you can publish it to npm. This is covered in the [npm documentation on publishing packages](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages).
+
+## Requirements for Vendure Hub
+
+[Vendure Hub](https://vendure.io/hub) is a curated list of high-quality plugins. To be accepted into Vendure Hub, we require some additional
+requirements be satisfied.
+
+### Changelog
+
+Your plugin package **must** include a `CHANGELOG.md` file which looks like this:
+
+```md
+# Changelog
+
+## 1.6.1 (2024-06-07)
+
+- Fix a bug where the `foo` was not correctly bar (Fixes [#123](https://github.com/myorg/my-repo/issues/31))
+
+## 1.6.0 (2024-03-11)
+
+- Add a new feature to the `bar` service
+- Update the `baz` service to use the new `qux` method
+
+... etc
+```
+
+The exact format of the entries is up to you - you can e.g. use [Keep a Changelog](https://keepachangelog.com/) format, grouping by type of change, using tooling
+to help generate the entries, etc. The important thing is that the `CHANGELOG.md` file is present and up-to-date, and published as part of your
+package by specifying it in the `files` field of your `package.json` file.
+
+```json
+{
+ "files": [
+    "dist",
+    "README.md",
+    // highlight-next-line
+    "CHANGELOG.md"
+  ]
+}
+```
+
+Vendure Hub will read the contents of your changelog to display the latest changes in your plugin listing.
+
+### Documentation
+
+Good documentation is a key criteria for acceptance into Vendure Hub.
+
+#### README.md
+
+Your plugin package **must** include a `README.md` file which contains full instructions on how to install and use your plugin. Here's a template you can use:
+
+<div class="limited-height-code-block">
+
+````md
+# Acme Loyalty Points Plugin
+
+This plugin adds a loyalty points system to your Vendure store.
+
+## Installation
+
+```bash
+npm install @acme/vendure-plugin-loyalty-points
+```
+
+Add the plugin to your Vendure config:
+
+```ts
+// vendure-config.ts
+import { LoyaltyPointsPlugin } from '@acme/vendure-plugin-loyalty-points';
+
+export const config = {
+    //...
+    plugins: [
+        LoyaltyPointsPlugin.init({
+            enablePartialRedemption: true,
+        }),
+    ],
+};
+```
+
+[If your plugin includes UI extensions]
+If not already installed, install the `@vendure/ui-devkit` package:
+
+```bash
+npm install @vendure/ui-devkit
+```
+
+Then set up the compilation of the UI extensions for the Admin UI:
+
+```ts
+// vendure-config.ts
+import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
+import { LoyaltyPointsPlugin } from '@acme/vendure-plugin-loyalty-points';
+
+// ...
+plugins: [
+  AdminUiPlugin.init({
+    route: 'admin',
+    port: 3002,
+    app: compileUiExtensions({
+      outputPath: path.join(__dirname, '../admin-ui'),
+      extensions: [LoyaltyPointsPlugin.uiExtensions],
+      devMode: false,
+    })
+  }),
+],
+```
+[/If your plugin includes UI extensions]
+
+## Usage
+
+Describe how to use your plugin here. Make sure to cover the key
+functionality and any configuration options. Include examples
+where possible.
+
+Make sure to document any extensions made to the GraphQL APIs,
+as well as how to integrate the plugin with a storefront app.
+````
+
+</div>
+
+#### JS Docs
+
+All publicly-exposed services, entities, strategies, interfaces etc should be documented using JSDoc comments.
+Not only does this improve the developer experience for your users, but it also allows Vendure Hub to auto-generate
+documentation pages for your plugin.
+
+Here are some examples of well-documented plugin code (implementation details omitted for brevity):
+
+<Tabs>
+<TabItem value="plugin" label="Plugin">
+
+- Tag the plugin with `@category Plugin`. This will be used when generating the docs pages to group plugins together.
+- Usually the `.init()` method is the thing that users will call to configure the plugin. Document this method with an example of how to use it.
+- The constructor and any lifecycle methods should be tagged with `@internal`.
+
+<div class="limited-height-code-block">
+
+```ts
+/**
+ * Advanced search and search analytics for Vendure.
+ *
+ * @category Plugin
+ */
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    // ...
+})
+export class LoyaltyPointsPlugin implements OnApplicationBootstrap {
+    /** @internal */
+    static options: LoyaltyPointsPluginInitOptions;
+
+    /**
+     * The static `init()` method is called with the options to
+     * configure the plugin.
+     *
+     * @example
+     * ```ts
+     * LoyaltyPointsPlugin.init({
+     *     enablePartialRedemption: true
+     * }),
+     * ```
+     */
+    static init(options: LoyaltyPointsPluginInitOptions) {
+        this.options = options;
+        return AdvancedSearchPlugin;
+    }
+
+    /**
+     * The static `uiExtensions` property is used to provide the
+     * necessary UI extensions to the Admin UI
+     * in order to display the loyalty points admin features.
+     * This property is used in the `AdminUiPlugin` initialization.
+     *
+     * @example
+     * ```ts
+     * import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
+     * import { AdvancedSearchPlugin } from '@acme/vendure-plugin-loyalty-points';
+     *
+     * // ...
+     * plugins: [
+     *   AdminUiPlugin.init({
+     *     route: 'admin',
+     *     port: 3002,
+     *     app: compileUiExtensions({
+     *       outputPath: path.join(__dirname, '../admin-ui'),
+     *       extensions: [LoyaltyPointsPlugin.uiExtensions],
+     *       devMode: false,
+     *     })
+     *   }),
+     * ],
+     * ```
+     */
+    static uiExtensions = advancedSearchPluginUi;
+
+    /** @internal */
+    constructor(/* ... */) {}
+
+    /** @internal */
+    async onApplicationBootstrap() {
+        // Logic to set up event subscribers etc.
+    }
+}
+```
+
+</div>
+</TabItem>
+<TabItem value="plugin-options" label="Plugin options">
+- Tag the options interface with `@category Plugin`.
+- Document any default values for optional properties.
+
+```ts
+/**
+ * Configuration options for the LoyaltyPointsPlugin.
+ *
+ * @category Plugin
+ */
+export interface LoyaltyPointsPluginInitOptions {
+    /**
+     * Whether to allow partial redemption of points.
+     *
+     * @default true
+     */
+    enablePartialRedemption?: boolean;
+}
+```
+
+</TabItem>
+<TabItem value="services" label="Services">
+
+- Only services that are exported in the plugin's `exports` array need to be documented. Internal services can be left undocumented.
+- Tag services with `@category Services`. This will be used when generating the docs pages to group services together.
+- By default all non-private methods are included in the docs. If you want to exclude a method, tag it with `@internal`.
+
+```ts
+/**
+ * The LoyaltyPointsService provides methods for managing a
+ * customer's loyalty points balance.
+ *
+ * @category Services
+ */
+@Injectable()
+export class LoyaltyPointsService {
+
+    /** @internal */
+    constructor(private connection: TransactionalConnection) {}
+
+    /**
+     * Adds the given number of points to the customer's balance.
+     */
+    addPoints(ctx: RequestContext, customerId: ID, points: number): Promise<LoyaltyPointsTransaction> {
+        // implementation...
+    }
+
+    /**
+     * Deducts the given number of points from the customer's balance.
+     */
+    deductPoints(customerId: ID, points: number): Promise<LoyaltyPointsTransaction> {
+        // implementation...
+    }
+}
+```
+
+</TabItem>
+<TabItem value="entities" label="Entities">
+
+- Tag entities with `@category Entities`. This will be used when generating the docs pages to group entities together.
+
+```ts
+/**
+ * Represents a transaction of loyalty points,
+ * when points are added or deducted.
+ *
+ * @category Entities
+ */
+@Entity()
+export class LoyaltyPointsTransaction extends VendureEntity {
+
+    /**
+     * The number of points added or deducted.
+     * A negative value indicates points deducted.
+     */
+    @Column()
+    points: number;
+
+    /**
+     * The Customer to whom the points were added or deducted.
+     */
+    @ManyToOne(type => Customer)
+    customer: Customer;
+
+    /**
+     * The reason for the points transaction.
+     */
+    @Column()
+    reason: string;
+}
+```
+
+</TabItem>
+<TabItem value="events" label="Events">
+
+- Tag events with `@category Events`. This will be used when generating the docs pages to group events together.
+
+```ts
+/**
+ * This event is fired whenever a LoyaltyPointsTransaction is created.
+ *
+ * @category Events
+ */
+export class LoyaltyPointsTransactionEvent extends VendureEvent {
+    constructor(public ctx: RequestContext, public transaction: LoyaltyPointsTransaction) {
+        super();
+    }
+}
+```
+
+</TabItem>
+</Tabs>
+
+### Tests
+
+Testing is an important part of ensuring the quality of your plugin, as well as preventing regressions when you make
+changes.
+
+For plugins of any complexity, you should aim to have a suite of end-to-end tests as covered in the [testing docs](/guides/developer-guide/testing/).
+
+In future we may use the test results to help determine the quality of a plugin.
+
+## Submitting to Vendure Hub
+
+Once your plugin is published to npm and satisfies the requirements above, you can submit it to Vendure Hub
+via our [contact form](https://vendure.io/contact?interested_in=publish_paid_plugins), making sure to include
+a link to the npm package and the GitHub repository.
+
+## Publishing a paid plugin
+
+Vendure Hub supports the listing of paid plugins. If you would like to sell plugins through Vendure Hub,
+    please [contact us](https://vendure.io/contact?interested_in=publish_paid_plugins) and provide
+details about the plugins you would like to sell.
+
+

+ 1 - 1
lerna.json

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

+ 8 - 0
license/signatures/version1/cla.json

@@ -103,6 +103,14 @@
       "created_at": "2024-08-10T13:11:42Z",
       "repoId": 136938012,
       "pullRequestNo": 3007
+    },
+    {
+      "name": "jyling",
+      "id": 11886327,
+      "comment_id": 2286576339,
+      "created_at": "2024-08-13T15:47:42Z",
+      "repoId": 136938012,
+      "pullRequestNo": 3009
     }
   ]
 }

+ 56 - 56
package-lock.json

@@ -31919,7 +31919,7 @@
         },
         "packages/admin-ui": {
             "name": "@vendure/admin-ui",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "@angular/animations": "^17.2.4",
@@ -31942,7 +31942,7 @@
                 "@ng-select/ng-select": "^12.0.7",
                 "@ngx-translate/core": "^15.0.0",
                 "@ngx-translate/http-loader": "^8.0.0",
-                "@vendure/common": "^3.0.0",
+                "@vendure/common": "^3.0.1",
                 "@webcomponents/custom-elements": "^1.6.0",
                 "apollo-angular": "^6.0.0",
                 "apollo-upload-client": "^18.0.1",
@@ -32013,7 +32013,7 @@
         },
         "packages/admin-ui-plugin": {
             "name": "@vendure/admin-ui-plugin",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "date-fns": "^2.30.0",
@@ -32022,9 +32022,9 @@
             "devDependencies": {
                 "@types/express": "^4.17.21",
                 "@types/fs-extra": "^11.0.4",
-                "@vendure/admin-ui": "^3.0.0",
-                "@vendure/common": "^3.0.0",
-                "@vendure/core": "^3.0.0",
+                "@vendure/admin-ui": "^3.0.1",
+                "@vendure/common": "^3.0.1",
+                "@vendure/core": "^3.0.1",
                 "express": "^4.18.3",
                 "rimraf": "^5.0.5",
                 "typescript": "5.4.2"
@@ -32055,7 +32055,7 @@
         },
         "packages/asset-server-plugin": {
             "name": "@vendure/asset-server-plugin",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "file-type": "^19.0.0",
@@ -32068,8 +32068,8 @@
                 "@types/express": "^4.17.21",
                 "@types/fs-extra": "^11.0.4",
                 "@types/node-fetch": "^2.6.11",
-                "@vendure/common": "^3.0.0",
-                "@vendure/core": "^3.0.0",
+                "@vendure/common": "^3.0.1",
+                "@vendure/core": "^3.0.1",
                 "express": "^4.18.3",
                 "node-fetch": "^2.7.0",
                 "rimraf": "^5.0.5",
@@ -32081,11 +32081,11 @@
         },
         "packages/cli": {
             "name": "@vendure/cli",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "@clack/prompts": "^0.7.0",
-                "@vendure/common": "^3.0.0",
+                "@vendure/common": "^3.0.1",
                 "change-case": "^4.1.2",
                 "commander": "^11.0.0",
                 "dotenv": "^16.4.5",
@@ -32099,7 +32099,7 @@
                 "vendure": "dist/cli.js"
             },
             "devDependencies": {
-                "@vendure/core": "^3.0.0",
+                "@vendure/core": "^3.0.1",
                 "typescript": "5.3.3"
             },
             "funding": {
@@ -32136,7 +32136,7 @@
         },
         "packages/common": {
             "name": "@vendure/common",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "license": "GPL-3.0-or-later",
             "devDependencies": {
                 "rimraf": "^5.0.5",
@@ -32148,7 +32148,7 @@
         },
         "packages/core": {
             "name": "@vendure/core",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "@apollo/server": "^4.10.4",
@@ -32162,7 +32162,7 @@
                 "@nestjs/testing": "~10.3.10",
                 "@nestjs/typeorm": "~10.0.2",
                 "@types/fs-extra": "^9.0.1",
-                "@vendure/common": "^3.0.0",
+                "@vendure/common": "^3.0.1",
                 "bcrypt": "^5.1.1",
                 "body-parser": "^1.20.2",
                 "cookie-session": "^2.1.0",
@@ -32298,11 +32298,11 @@
         },
         "packages/create": {
             "name": "@vendure/create",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "@clack/prompts": "^0.7.0",
-                "@vendure/common": "^3.0.0",
+                "@vendure/common": "^3.0.1",
                 "commander": "^11.0.0",
                 "cross-spawn": "^7.0.3",
                 "fs-extra": "^11.2.0",
@@ -32319,7 +32319,7 @@
                 "@types/fs-extra": "^11.0.4",
                 "@types/handlebars": "^4.1.0",
                 "@types/semver": "^7.5.8",
-                "@vendure/core": "^3.0.0",
+                "@vendure/core": "^3.0.1",
                 "rimraf": "^5.0.5",
                 "ts-node": "^10.9.2",
                 "typescript": "5.3.3"
@@ -32336,21 +32336,21 @@
             }
         },
         "packages/dev-server": {
-            "version": "3.0.0",
+            "version": "3.0.1",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "@nestjs/axios": "^3.0.2",
-                "@vendure/admin-ui-plugin": "^3.0.0",
-                "@vendure/asset-server-plugin": "^3.0.0",
-                "@vendure/common": "^3.0.0",
-                "@vendure/core": "^3.0.0",
-                "@vendure/elasticsearch-plugin": "^3.0.0",
-                "@vendure/email-plugin": "^3.0.0",
+                "@vendure/admin-ui-plugin": "^3.0.1",
+                "@vendure/asset-server-plugin": "^3.0.1",
+                "@vendure/common": "^3.0.1",
+                "@vendure/core": "^3.0.1",
+                "@vendure/elasticsearch-plugin": "^3.0.1",
+                "@vendure/email-plugin": "^3.0.1",
                 "typescript": "5.3.3"
             },
             "devDependencies": {
-                "@vendure/testing": "^3.0.0",
-                "@vendure/ui-devkit": "^3.0.0",
+                "@vendure/testing": "^3.0.1",
+                "@vendure/ui-devkit": "^3.0.1",
                 "commander": "^12.0.0",
                 "concurrently": "^8.2.2",
                 "csv-stringify": "^6.4.6",
@@ -32368,7 +32368,7 @@
         },
         "packages/elasticsearch-plugin": {
             "name": "@vendure/elasticsearch-plugin",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "@elastic/elasticsearch": "~7.9.1",
@@ -32376,8 +32376,8 @@
                 "fast-deep-equal": "^3.1.3"
             },
             "devDependencies": {
-                "@vendure/common": "^3.0.0",
-                "@vendure/core": "^3.0.0",
+                "@vendure/common": "^3.0.1",
+                "@vendure/core": "^3.0.1",
                 "rimraf": "^5.0.5",
                 "typescript": "5.3.3"
             },
@@ -32387,7 +32387,7 @@
         },
         "packages/email-plugin": {
             "name": "@vendure/email-plugin",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "@types/nodemailer": "^6.4.9",
@@ -32403,8 +32403,8 @@
                 "@types/express": "^4.17.21",
                 "@types/fs-extra": "^11.0.4",
                 "@types/mjml": "^4.7.4",
-                "@vendure/common": "^3.0.0",
-                "@vendure/core": "^3.0.0",
+                "@vendure/common": "^3.0.1",
+                "@vendure/core": "^3.0.1",
                 "rimraf": "^5.0.5",
                 "typescript": "5.3.3"
             },
@@ -32414,14 +32414,14 @@
         },
         "packages/harden-plugin": {
             "name": "@vendure/harden-plugin",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "graphql-query-complexity": "^0.12.0"
             },
             "devDependencies": {
-                "@vendure/common": "^3.0.0",
-                "@vendure/core": "^3.0.0"
+                "@vendure/common": "^3.0.1",
+                "@vendure/core": "^3.0.1"
             },
             "funding": {
                 "url": "https://github.com/sponsors/michaelbromley"
@@ -32429,12 +32429,12 @@
         },
         "packages/job-queue-plugin": {
             "name": "@vendure/job-queue-plugin",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "license": "GPL-3.0-or-later",
             "devDependencies": {
                 "@google-cloud/pubsub": "^2.8.0",
-                "@vendure/common": "^3.0.0",
-                "@vendure/core": "^3.0.0",
+                "@vendure/common": "^3.0.1",
+                "@vendure/core": "^3.0.1",
                 "bullmq": "^5.4.2",
                 "ioredis": "^5.3.2",
                 "rimraf": "^5.0.5",
@@ -32446,7 +32446,7 @@
         },
         "packages/payments-plugin": {
             "name": "@vendure/payments-plugin",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "currency.js": "2.0.4"
@@ -32455,9 +32455,9 @@
                 "@mollie/api-client": "^3.7.0",
                 "@types/braintree": "^3.3.11",
                 "@types/localtunnel": "2.0.4",
-                "@vendure/common": "^3.0.0",
-                "@vendure/core": "^3.0.0",
-                "@vendure/testing": "^3.0.0",
+                "@vendure/common": "^3.0.1",
+                "@vendure/core": "^3.0.1",
+                "@vendure/testing": "^3.0.1",
                 "braintree": "^3.22.0",
                 "localtunnel": "2.0.2",
                 "nock": "^13.1.4",
@@ -32513,12 +32513,12 @@
         },
         "packages/sentry-plugin": {
             "name": "@vendure/sentry-plugin",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "license": "GPL-3.0-or-later",
             "devDependencies": {
                 "@sentry/node": "^7.106.1",
-                "@vendure/common": "^3.0.0",
-                "@vendure/core": "^3.0.0"
+                "@vendure/common": "^3.0.1",
+                "@vendure/core": "^3.0.1"
             },
             "funding": {
                 "url": "https://github.com/sponsors/michaelbromley"
@@ -32529,14 +32529,14 @@
         },
         "packages/stellate-plugin": {
             "name": "@vendure/stellate-plugin",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "node-fetch": "^2.7.0"
             },
             "devDependencies": {
-                "@vendure/common": "^3.0.0",
-                "@vendure/core": "^3.0.0"
+                "@vendure/common": "^3.0.1",
+                "@vendure/core": "^3.0.1"
             },
             "funding": {
                 "url": "https://github.com/sponsors/michaelbromley"
@@ -32544,11 +32544,11 @@
         },
         "packages/testing": {
             "name": "@vendure/testing",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "@graphql-typed-document-node/core": "^3.2.0",
-                "@vendure/common": "^3.0.0",
+                "@vendure/common": "^3.0.1",
                 "faker": "^4.1.0",
                 "form-data": "^4.0.0",
                 "graphql": "~16.9.0",
@@ -32561,7 +32561,7 @@
                 "@types/mysql": "^2.15.26",
                 "@types/node-fetch": "^2.6.4",
                 "@types/pg": "^8.11.2",
-                "@vendure/core": "^3.0.0",
+                "@vendure/core": "^3.0.1",
                 "mysql": "^2.18.1",
                 "pg": "^8.11.3",
                 "rimraf": "^5.0.5",
@@ -32577,15 +32577,15 @@
         },
         "packages/ui-devkit": {
             "name": "@vendure/ui-devkit",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "license": "GPL-3.0-or-later",
             "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": "^3.0.0",
-                "@vendure/common": "^3.0.0",
+                "@vendure/admin-ui": "^3.0.1",
+                "@vendure/common": "^3.0.1",
                 "chalk": "^4.1.0",
                 "chokidar": "^3.6.0",
                 "fs-extra": "^11.2.0",
@@ -32596,7 +32596,7 @@
                 "@rollup/plugin-node-resolve": "^15.2.3",
                 "@rollup/plugin-terser": "^0.4.4",
                 "@types/fs-extra": "^11.0.4",
-                "@vendure/core": "^3.0.0",
+                "@vendure/core": "^3.0.1",
                 "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": "3.0.0",
+    "version": "3.0.1",
     "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": "^3.0.0",
-        "@vendure/common": "^3.0.0",
-        "@vendure/core": "^3.0.0",
+        "@vendure/admin-ui": "^3.0.1",
+        "@vendure/common": "^3.0.1",
+        "@vendure/core": "^3.0.1",
         "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": "3.0.0",
+    "version": "3.0.1",
     "license": "GPL-3.0-or-later",
     "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": "^3.0.0",
+        "@vendure/common": "^3.0.1",
         "@webcomponents/custom-elements": "^1.6.0",
         "apollo-angular": "^6.0.0",
         "apollo-upload-client": "^18.0.1",

+ 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 = '3.0.0';
+export const ADMIN_UI_VERSION = '3.0.1';

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/asset-server-plugin",
-    "version": "3.0.0",
+    "version": "3.0.1",
     "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": "^3.0.0",
-        "@vendure/core": "^3.0.0",
+        "@vendure/common": "^3.0.1",
+        "@vendure/core": "^3.0.1",
         "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": "3.0.0",
+    "version": "3.0.1",
     "description": "A modern, headless ecommerce framework",
     "repository": {
         "type": "git",
@@ -35,7 +35,7 @@
     ],
     "dependencies": {
         "@clack/prompts": "^0.7.0",
-        "@vendure/common": "^3.0.0",
+        "@vendure/common": "^3.0.1",
         "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": "^3.0.0",
+        "@vendure/core": "^3.0.1",
         "typescript": "5.3.3"
     }
 }

+ 1 - 1
packages/common/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/common",
-    "version": "3.0.0",
+    "version": "3.0.1",
     "main": "index.js",
     "license": "GPL-3.0-or-later",
     "scripts": {

+ 47 - 2
packages/core/e2e/entity-hydrator.e2e-spec.ts

@@ -22,7 +22,11 @@ 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 { AdditionalConfig, HydrationTestPlugin } from './fixtures/test-plugins/hydration-test-plugin';
+import {
+    AdditionalConfig,
+    HydrationTestPlugin,
+    TreeEntity,
+} from './fixtures/test-plugins/hydration-test-plugin';
 import { UpdateChannelMutation, UpdateChannelMutationVariables } from './graphql/generated-e2e-admin-types';
 import {
     AddItemToOrderDocument,
@@ -54,11 +58,17 @@ describe('Entity hydration', () => {
 
         const connection = server.app.get(TransactionalConnection).rawConnection;
         const asset = await connection.getRepository(Asset).findOne({ where: {} });
-        await connection.getRepository(AdditionalConfig).save(
+        const additionalConfig = await connection.getRepository(AdditionalConfig).save(
             new AdditionalConfig({
                 backgroundImage: asset,
             }),
         );
+        const parent = await connection
+            .getRepository(TreeEntity)
+            .save(new TreeEntity({ additionalConfig, image1: asset, image2: asset }));
+        await connection
+            .getRepository(TreeEntity)
+            .save(new TreeEntity({ parent, image1: asset, image2: asset }));
     }, TEST_SETUP_TIMEOUT_MS);
 
     afterAll(async () => {
@@ -382,6 +392,35 @@ describe('Entity hydration', () => {
             expect(line.productVariantId).toBe(line.productVariant.id);
         }
     });
+
+    /*
+     * Postgres has a character limit for alias names which can cause issues when joining
+     * multiple aliases with the same prefix
+     * https://github.com/vendure-ecommerce/vendure/issues/2899
+     */
+    it('Hydrates properties with very long names', async () => {
+        await adminClient.query<UpdateChannelMutation, UpdateChannelMutationVariables>(UPDATE_CHANNEL, {
+            input: {
+                id: 'T_1',
+                customFields: {
+                    additionalConfigId: 'T_1',
+                },
+            },
+        });
+
+        const { hydrateChannelWithVeryLongPropertyName } = await adminClient.query<{
+            hydrateChannelWithVeryLongPropertyName: any;
+        }>(GET_HYDRATED_CHANNEL_LONG_ALIAS, {
+            id: 'T_1',
+        });
+
+        const entity = (
+            hydrateChannelWithVeryLongPropertyName.customFields.additionalConfig as AdditionalConfig
+        ).treeEntity[0];
+        const child = entity.childrenPropertyWithAVeryLongNameThatExceedsPostgresLimitsEasilyByItself[0];
+        expect(child.image1).toBeDefined();
+        expect(child.image2).toBeDefined();
+    });
 });
 
 function getVariantWithName(product: Product, name: string) {
@@ -432,3 +471,9 @@ const GET_HYDRATED_CHANNEL_NESTED = gql`
         hydrateChannelWithNestedRelation(id: $id)
     }
 `;
+
+const GET_HYDRATED_CHANNEL_LONG_ALIAS = gql`
+    query GetHydratedChannelNested($id: ID!) {
+        hydrateChannelWithVeryLongPropertyName(id: $id)
+    }
+`;

+ 58 - 2
packages/core/e2e/fixtures/test-plugins/hydration-test-plugin.ts

@@ -19,7 +19,7 @@ import {
     VendurePlugin,
 } from '@vendure/core';
 import gql from 'graphql-tag';
-import { Entity, ManyToOne } from 'typeorm';
+import { Entity, ManyToOne, OneToMany } from 'typeorm';
 
 @Resolver()
 export class TestAdminPluginResolver {
@@ -141,6 +141,30 @@ export class TestAdminPluginResolver {
         });
         return channel;
     }
+
+    @Query()
+    async hydrateChannelWithVeryLongPropertyName(@Ctx() ctx: RequestContext, @Args() args: { id: ID }) {
+        const channel = await this.channelService.findOne(ctx, args.id);
+        await this.entityHydrator.hydrate(ctx, channel!, {
+            relations: ['customFields.additionalConfig.treeEntity'],
+        });
+
+        // Make sure we start on a tree entity to make use of tree-relations-qb-joiner.ts
+        await Promise.all(
+            ((channel!.customFields as any).additionalConfig.treeEntity as TreeEntity[]).map(treeEntity =>
+                this.entityHydrator.hydrate(ctx, treeEntity, {
+                    relations: [
+                        'childrenPropertyWithAVeryLongNameThatExceedsPostgresLimitsEasilyByItself',
+                        'childrenPropertyWithAVeryLongNameThatExceedsPostgresLimitsEasilyByItself',
+                        'childrenPropertyWithAVeryLongNameThatExceedsPostgresLimitsEasilyByItself.image1',
+                        'childrenPropertyWithAVeryLongNameThatExceedsPostgresLimitsEasilyByItself.image2',
+                    ],
+                }),
+            ),
+        );
+
+        return channel;
+    }
 }
 
 @Entity()
@@ -151,11 +175,42 @@ export class AdditionalConfig extends VendureEntity {
 
     @ManyToOne(() => Asset, { onDelete: 'SET NULL', nullable: true })
     backgroundImage: Asset;
+
+    @OneToMany(() => TreeEntity, entity => entity.additionalConfig)
+    treeEntity: TreeEntity[];
+}
+
+@Entity()
+export class TreeEntity extends VendureEntity {
+    constructor(input?: DeepPartial<TreeEntity>) {
+        super(input);
+    }
+
+    @ManyToOne(() => AdditionalConfig, e => e.treeEntity, { nullable: true })
+    additionalConfig: AdditionalConfig;
+
+    @OneToMany(() => TreeEntity, entity => entity.parent)
+    childrenPropertyWithAVeryLongNameThatExceedsPostgresLimitsEasilyByItself: TreeEntity[];
+
+    @ManyToOne(
+        () => TreeEntity,
+        entity => entity.childrenPropertyWithAVeryLongNameThatExceedsPostgresLimitsEasilyByItself,
+        {
+            nullable: true,
+        },
+    )
+    parent: TreeEntity;
+
+    @ManyToOne(() => Asset)
+    image1: Asset;
+
+    @ManyToOne(() => Asset)
+    image2: Asset;
 }
 
 @VendurePlugin({
     imports: [PluginCommonModule],
-    entities: [AdditionalConfig],
+    entities: [AdditionalConfig, TreeEntity],
     adminApiExtensions: {
         resolvers: [TestAdminPluginResolver],
         schema: gql`
@@ -168,6 +223,7 @@ export class AdditionalConfig extends VendureEntity {
                 hydrateOrderReturnQuantities(id: ID!): JSON
                 hydrateChannel(id: ID!): JSON
                 hydrateChannelWithNestedRelation(id: ID!): JSON
+                hydrateChannelWithVeryLongPropertyName(id: ID!): JSON
             }
         `,
     },

+ 72 - 0
packages/core/e2e/order-modification.e2e-spec.ts

@@ -2461,6 +2461,78 @@ describe('Order modification', () => {
             expect(modifyOrder.totalWithTax).toBe(getOrderPaymentsTotalWithRefunds(modifyOrder));
             expect(modifyOrder.payments![0].refunds[0].total).toBe(shippingWithTax);
         });
+
+        it('adjustOrderLines empty quantity with discounts', async () => {
+            const PercentDiscount50Percent = '50PERCENT';
+            await adminClient.query<
+                Codegen.CreatePromotionMutation,
+                Codegen.CreatePromotionMutationVariables
+            >(CREATE_PROMOTION, {
+                input: {
+                    enabled: true,
+                    couponCode: PercentDiscount50Percent,
+                    conditions: [
+                        {
+                            code: 'minimum_order_amount',
+                            arguments: [
+                                { name: 'amount', value: '0' },
+                                { name: 'taxInclusive', value: 'false' },
+                            ],
+                        },
+                    ],
+                    actions: [
+                        {
+                            code: orderPercentageDiscount.code,
+                            arguments: [{ name: 'discount', value: '50' }],
+                        },
+                    ],
+                    translations: [{ languageCode: LanguageCode.en, name: 'half price' }],
+                },
+            });
+            await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
+            await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
+                productVariantId: 'T_1',
+                quantity: 1,
+            } as any);
+            await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
+                productVariantId: 'T_2',
+                quantity: 1,
+            } as any);
+
+            await proceedToArrangingPayment(shopClient);
+            const paidOrder = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
+            orderGuard.assertSuccess(paidOrder);
+
+            const transitionOrderToState = await adminTransitionOrderToState(paidOrder.id, 'Modifying');
+            orderGuard.assertSuccess(transitionOrderToState);
+
+            expect(transitionOrderToState.state).toBe('Modifying');
+
+            // modify order should not throw an error when setting quantity to 0 for a order line
+            const { modifyOrder } = await adminClient.query<
+                Codegen.ModifyOrderMutation,
+                Codegen.ModifyOrderMutationVariables
+            >(MODIFY_ORDER, {
+                input: {
+                    dryRun: true,
+                    orderId: order.id,
+                    couponCodes: [PercentDiscount50Percent],
+                    adjustOrderLines: [{ orderLineId: order.lines[0].id, quantity: 0 }],
+                },
+            });
+
+            orderGuard.assertSuccess(modifyOrder);
+            expect(modifyOrder.id).toBeDefined();
+
+            // ensure correct adjustments applied
+            // The first line should have a linePrice of 0 because it has zero quantity
+            expect(modifyOrder.lines[0].linePriceWithTax).toBe(0);
+            expect(modifyOrder.lines[0].proratedLinePriceWithTax).toBe(0);
+            // The second line should have the proratedLinePriceWithTax discounted per the promotion
+            expect(modifyOrder.lines[1].proratedLinePriceWithTax).toBe(
+                modifyOrder.lines[1].discountedLinePriceWithTax / 2,
+            );
+        });
     });
 
     async function adminTransitionOrderToState(id: string, state: string) {

+ 2 - 2
packages/core/package.json

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

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

@@ -4,6 +4,7 @@ import { AuthenticationMethod as AuthenticationMethodType } from '@vendure/commo
 import { NATIVE_AUTH_STRATEGY_NAME } from '../../../config/auth/native-authentication-strategy';
 import { AuthenticationMethod } from '../../../entity/authentication-method/authentication-method.entity';
 import { ExternalAuthenticationMethod } from '../../../entity/authentication-method/external-authentication-method.entity';
+import { Role } from '../../../entity/role/role.entity';
 import { User } from '../../../entity/user/user.entity';
 import { UserService } from '../../../service/services/user.service';
 import { RequestContext } from '../../common/request-context';
@@ -32,4 +33,14 @@ export class UserEntityResolver {
             strategy: m instanceof ExternalAuthenticationMethod ? m.strategy : NATIVE_AUTH_STRATEGY_NAME,
         }));
     }
+
+    @ResolveField()
+    async roles(@Ctx() ctx: RequestContext, @Parent() user: User): Promise<Role[]> {
+        if (user.roles) {
+            return user.roles;
+        }
+        let roles: Role[] = [];
+        roles = await this.userService.getUserById(ctx, user.id).then(u => u?.roles ?? []);
+        return roles;
+    }
 }

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

@@ -249,9 +249,9 @@ export class OrderCalculator {
                     const adjustment = await promotion.apply(ctx, { order }, state);
                     if (adjustment && adjustment.amount !== 0) {
                         const amount = adjustment.amount;
-                        const weights = order.lines
-                            .filter(l => l.quantity !== 0)
-                            .map(l => l.proratedLinePriceWithTax);
+                        const weights = order.lines.map(l =>
+                            l.quantity !== 0 ? l.proratedLinePriceWithTax : 0,
+                        );
                         const distribution = prorate(weights, amount);
                         order.lines.forEach((line, i) => {
                             const shareOfAmount = distribution[i];

+ 7 - 2
packages/core/src/service/helpers/utils/tree-relations-qb-joiner.ts

@@ -1,6 +1,6 @@
 import { EntityMetadata, FindOneOptions, SelectQueryBuilder } from 'typeorm';
 import { EntityTarget } from 'typeorm/common/EntityTarget';
-import { FindOptionsRelationByString, FindOptionsRelations } from 'typeorm/find-options/FindOptionsRelations';
+import { DriverUtils } from 'typeorm/driver/DriverUtils';
 
 import { findOptionsObjectToArray } from '../../../connection/find-options-object-to-array';
 import { VendureEntity } from '../../../entity';
@@ -108,7 +108,12 @@ export function joinTreeRelationsDynamically<T extends VendureEntity>(
         if (relationMetadata.isEager) {
             joinConnector = '__';
         }
-        const nextAlias = `${currentAlias}${joinConnector}${part.replace(/\./g, '_')}`;
+        const nextAlias = DriverUtils.buildAlias(
+            qb.connection.driver,
+            { shorten: false },
+            currentAlias,
+            part.replace(/\./g, '_'),
+        );
         const nextPath = parts.join('.');
         const fullPath = [...(parentPath || []), part].join('.');
         if (!qb.expressionMap.joinAttributes.some(ja => ja.alias.name === nextAlias)) {

+ 3 - 3
packages/create/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/create",
-    "version": "3.0.0",
+    "version": "3.0.1",
     "license": "GPL-3.0-or-later",
     "bin": {
         "create": "./index.js"
@@ -27,14 +27,14 @@
         "@types/fs-extra": "^11.0.4",
         "@types/handlebars": "^4.1.0",
         "@types/semver": "^7.5.8",
-        "@vendure/core": "^3.0.0",
+        "@vendure/core": "^3.0.1",
         "rimraf": "^5.0.5",
         "ts-node": "^10.9.2",
         "typescript": "5.3.3"
     },
     "dependencies": {
         "@clack/prompts": "^0.7.0",
-        "@vendure/common": "^3.0.0",
+        "@vendure/common": "^3.0.1",
         "commander": "^11.0.0",
         "cross-spawn": "^7.0.3",
         "fs-extra": "^11.2.0",

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

@@ -1,6 +1,6 @@
 {
     "name": "dev-server",
-    "version": "3.0.0",
+    "version": "3.0.1",
     "main": "index.js",
     "license": "GPL-3.0-or-later",
     "private": true,
@@ -15,17 +15,17 @@
     },
     "dependencies": {
         "@nestjs/axios": "^3.0.2",
-        "@vendure/admin-ui-plugin": "^3.0.0",
-        "@vendure/asset-server-plugin": "^3.0.0",
-        "@vendure/common": "^3.0.0",
-        "@vendure/core": "^3.0.0",
-        "@vendure/elasticsearch-plugin": "^3.0.0",
-        "@vendure/email-plugin": "^3.0.0",
+        "@vendure/admin-ui-plugin": "^3.0.1",
+        "@vendure/asset-server-plugin": "^3.0.1",
+        "@vendure/common": "^3.0.1",
+        "@vendure/core": "^3.0.1",
+        "@vendure/elasticsearch-plugin": "^3.0.1",
+        "@vendure/email-plugin": "^3.0.1",
         "typescript": "5.3.3"
     },
     "devDependencies": {
-        "@vendure/testing": "^3.0.0",
-        "@vendure/ui-devkit": "^3.0.0",
+        "@vendure/testing": "^3.0.1",
+        "@vendure/ui-devkit": "^3.0.1",
         "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": "3.0.0",
+    "version": "3.0.1",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -26,8 +26,8 @@
         "fast-deep-equal": "^3.1.3"
     },
     "devDependencies": {
-        "@vendure/common": "^3.0.0",
-        "@vendure/core": "^3.0.0",
+        "@vendure/common": "^3.0.1",
+        "@vendure/core": "^3.0.1",
         "rimraf": "^5.0.5",
         "typescript": "5.3.3"
     }

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/email-plugin",
-    "version": "3.0.0",
+    "version": "3.0.1",
     "license": "GPL-3.0-or-later",
     "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": "^3.0.0",
-        "@vendure/core": "^3.0.0",
+        "@vendure/common": "^3.0.1",
+        "@vendure/core": "^3.0.1",
         "rimraf": "^5.0.5",
         "typescript": "5.3.3"
     }

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/harden-plugin",
-    "version": "3.0.0",
+    "version": "3.0.1",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -21,7 +21,7 @@
         "graphql-query-complexity": "^0.12.0"
     },
     "devDependencies": {
-        "@vendure/common": "^3.0.0",
-        "@vendure/core": "^3.0.0"
+        "@vendure/common": "^3.0.1",
+        "@vendure/core": "^3.0.1"
     }
 }

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

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

+ 0 - 0
packages/payments-plugin/e2e/stripe-checkout-test.plugin.ts → packages/payments-plugin/e2e/fixtures/stripe-checkout-test.plugin.ts


+ 2 - 6
packages/payments-plugin/e2e/mollie-dev-server.ts

@@ -26,7 +26,6 @@ import {
 /**
  * This should only be used to locally test the Mollie payment plugin
  * Make sure you have `MOLLIE_APIKEY=test_xxxx` in your .env file
- * Make sure you have `MOLLIE_APIKEY=test_xxxx` in your .env file
  */
 /* eslint-disable @typescript-eslint/no-floating-promises */
 async function runMollieDevServer() {
@@ -109,13 +108,10 @@ async function runMollieDevServer() {
     // eslint-disable-next-line no-console
     console.log('Payment intent result', result);
 
-    // Change order amount and create new intent
-    await createFixedDiscountCoupon(adminClient, 20000, 'DISCOUNT_ORDER');
-    await shopClient.query(APPLY_COUPON_CODE, { couponCode: 'DISCOUNT_ORDER' });
-    await new Promise(resolve => setTimeout(resolve, 3000));
+    // Create another Payment Intent to test duplicate paymnets
     const result2 = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, { input: {} });
     // eslint-disable-next-line no-console
-    console.log('Payment intent result', result2);
+    console.log('Second payment intent result', result2);
 }
 
 (async () => {

+ 52 - 68
packages/payments-plugin/e2e/mollie-payment.e2e-spec.ts

@@ -3,6 +3,7 @@ import {
     ChannelService,
     EventBus,
     LanguageCode,
+    Logger,
     mergeConfig,
     OrderPlacedEvent,
     OrderService,
@@ -22,7 +23,7 @@ import {
 import nock from 'nock';
 import fetch from 'node-fetch';
 import path from 'path';
-import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
+import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
@@ -213,6 +214,34 @@ describe('Mollie payments', () => {
         expect(customers).toHaveLength(2);
     });
 
+    it('Should create a Mollie paymentMethod', async () => {
+        const { createPaymentMethod } = await adminClient.query<
+            CreatePaymentMethodMutation,
+            CreatePaymentMethodMutationVariables
+        >(CREATE_PAYMENT_METHOD, {
+            input: {
+                code: mockData.methodCode,
+                enabled: true,
+                handler: {
+                    code: molliePaymentHandler.code,
+                    arguments: [
+                        { name: 'redirectUrl', value: mockData.redirectUrl },
+                        { name: 'apiKey', value: mockData.apiKey },
+                        { name: 'autoCapture', value: 'false' },
+                    ],
+                },
+                translations: [
+                    {
+                        languageCode: LanguageCode.en,
+                        name: 'Mollie payment test',
+                        description: 'This is a Mollie test payment method',
+                    },
+                ],
+            },
+        });
+        expect(createPaymentMethod.code).toBe(mockData.methodCode);
+    });
+
     describe('Payment intent creation', () => {
         it('Should prepare an order', async () => {
             await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
@@ -238,34 +267,6 @@ describe('Mollie payments', () => {
             expect(order.code).toBeDefined();
         });
 
-        it('Should add a Mollie paymentMethod', async () => {
-            const { createPaymentMethod } = await adminClient.query<
-                CreatePaymentMethodMutation,
-                CreatePaymentMethodMutationVariables
-            >(CREATE_PAYMENT_METHOD, {
-                input: {
-                    code: mockData.methodCode,
-                    enabled: true,
-                    handler: {
-                        code: molliePaymentHandler.code,
-                        arguments: [
-                            { name: 'redirectUrl', value: mockData.redirectUrl },
-                            { name: 'apiKey', value: mockData.apiKey },
-                            { name: 'autoCapture', value: 'false' },
-                        ],
-                    },
-                    translations: [
-                        {
-                            languageCode: LanguageCode.en,
-                            name: 'Mollie payment test',
-                            description: 'This is a Mollie test payment method',
-                        },
-                    ],
-                },
-            });
-            expect(createPaymentMethod.code).toBe(mockData.methodCode);
-        });
-
         it('Should fail to create payment intent without shippingmethod', async () => {
             await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
             const { createMolliePaymentIntent: result } = await shopClient.query(
@@ -387,45 +388,6 @@ describe('Mollie payments', () => {
             });
         });
 
-        it('Should recreate all order lines in Mollie', async () => {
-            // Should fetch the existing order from Mollie
-            nock('https://api.mollie.com/')
-                .get('/v2/orders/ord_mockId')
-                .reply(200, mockData.mollieOrderResponse);
-            // Should patch existing order
-            nock('https://api.mollie.com/')
-                .patch(`/v2/orders/${mockData.mollieOrderResponse.id}`)
-                .reply(200, mockData.mollieOrderResponse);
-            // Should patch existing order lines
-            let molliePatchRequest: any | undefined;
-            nock('https://api.mollie.com/')
-                .patch(`/v2/orders/${mockData.mollieOrderResponse.id}/lines`, body => {
-                    molliePatchRequest = body;
-                    return true;
-                })
-                .reply(200, mockData.mollieOrderResponse);
-            const { createMolliePaymentIntent } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
-                input: {
-                    paymentMethodCode: mockData.methodCode,
-                },
-            });
-            expect(createMolliePaymentIntent.url).toBeDefined();
-            // Should have removed all 3 previous order lines
-            const cancelledLines = molliePatchRequest.operations.filter((o: any) => o.operation === 'cancel');
-            expect(cancelledLines.length).toBe(3);
-            // Should have added all 3 new order lines
-            const addedLines = molliePatchRequest.operations.filter((o: any) => o.operation === 'add');
-            expect(addedLines.length).toBe(3);
-            addedLines.forEach((line: any) => {
-                expect(line.data).toHaveProperty('name');
-                expect(line.data).toHaveProperty('quantity');
-                expect(line.data).toHaveProperty('unitPrice');
-                expect(line.data).toHaveProperty('totalAmount');
-                expect(line.data).toHaveProperty('vatRate');
-                expect(line.data).toHaveProperty('vatAmount');
-            });
-        });
-
         it('Should get payment url with deducted amount if a payment is already made', async () => {
             let mollieRequest: any | undefined;
             nock('https://api.mollie.com/')
@@ -564,6 +526,28 @@ describe('Mollie payments', () => {
             expect(order.state).toBe('PaymentSettled');
         });
 
+        it('Should log error when order is paid again with a different mollie order', async () => {
+            const errorLogSpy = vi.spyOn(Logger, 'error');
+            nock('https://api.mollie.com/')
+                .get('/v2/orders/ord_newMockId')
+                .reply(200, {
+                    ...mockData.mollieOrderResponse,
+                    id: 'ord_newMockId',
+                    amount: { value: '100', currency: 'EUR' }, // Try to pay another 100
+                    orderNumber: order.code,
+                    status: OrderStatus.paid,
+                });
+            await fetch(`http://localhost:${serverPort}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`, {
+                method: 'post',
+                body: JSON.stringify({ id: 'ord_newMockId' }),
+                headers: { 'Content-Type': 'application/json' },
+            });
+            const logMessage = errorLogSpy.mock.calls?.[0]?.[0];
+            expect(logMessage).toBe(
+                `Order '${order.code}' is already paid. Mollie order 'ord_newMockId' should be refunded.`,
+            );
+        });
+
         it('Should have preserved original languageCode ', () => {
             // We've set the languageCode to 'nl' in the mock response's metadata
             expect(orderPlacedEvent?.ctx.languageCode).toBe('nl');

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/payments-plugin",
-    "version": "3.0.0",
+    "version": "3.0.1",
     "license": "GPL-3.0-or-later",
     "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": "^3.0.0",
-        "@vendure/core": "^3.0.0",
-        "@vendure/testing": "^3.0.0",
+        "@vendure/common": "^3.0.1",
+        "@vendure/core": "^3.0.1",
+        "@vendure/testing": "^3.0.1",
         "braintree": "^3.22.0",
         "localtunnel": "2.0.2",
         "nock": "^13.1.4",

+ 0 - 16
packages/payments-plugin/src/mollie/custom-fields.ts

@@ -1,16 +0,0 @@
-import { CustomFieldConfig, Order, CustomOrderFields } from '@vendure/core';
-
-export interface OrderWithMollieReference extends Order {
-    customFields: CustomOrderFields & {
-        mollieOrderId?: string;
-    };
-}
-
-export const orderCustomFields: CustomFieldConfig[] = [
-    {
-        name: 'mollieOrderId',
-        type: 'string',
-        internal: true,
-        nullable: true,
-    },
-];

+ 0 - 76
packages/payments-plugin/src/mollie/extended-mollie-client.ts

@@ -1,76 +0,0 @@
-import createMollieClient, { MollieClient, Order as MollieOrder } from '@mollie/api-client';
-import { Amount } from '@mollie/api-client/dist/types/src/data/global';
-// We depend on the axios dependency from '@mollie/api-client'
-import axios, { AxiosInstance } from 'axios';
-import { create } from 'domain';
-
-/**
- * Create an extended Mollie client that also supports the manage order lines endpoint, because
- * the NodeJS client doesn't support it yet.
- *
- * See https://docs.mollie.com/reference/v2/orders-api/manage-order-lines
- * FIXME: Remove this when the NodeJS client supports it.
- */
-export function createExtendedMollieClient(options: {apiKey: string}): ExtendedMollieClient {
-    const client = createMollieClient(options) as ExtendedMollieClient;
-    // Add our custom method
-    client.manageOrderLines = async (orderId: string, input: ManageOrderLineInput): Promise<MollieOrder> => {
-        const instance = axios.create({
-            baseURL: `https://api.mollie.com`,
-            timeout: 5000,
-            headers: {
-                'Content-Type': 'application/json',
-                Authorization: `Bearer ${options.apiKey}`,
-            },
-            validateStatus: () => true, // We handle errors ourselves, for better error messages
-        });
-        const {status, data} = await instance.patch(`/v2/orders/${orderId}/lines`, input);
-        if (status < 200 || status > 300) {
-            throw Error(JSON.stringify(data, null, 2))
-        }
-        return data;
-    }
-    return client;
-}
-
-
-export interface ExtendedMollieClient extends MollieClient {
-    /**
-    * Update all order lines in 1 request.
-    */
-    manageOrderLines(orderId: string, input: ManageOrderLineInput): Promise<MollieOrder>;
-}
-
-interface CancelOperation {
-    operation: 'cancel';
-    data: { id: string }
-}
-
-interface UpdateOperation {
-    operation: 'update';
-    data: {
-        id: string
-        name?: string
-        quantity?: number,
-        unitPrice?: Amount
-        totalAmount?: Amount
-        vatRate?: string
-        vatAmount?: Amount
-    }
-}
-
-interface AddOperation {
-    operation: 'add';
-    data: {
-        name: string
-        quantity: number,
-        unitPrice: Amount
-        totalAmount: Amount
-        vatRate: string
-        vatAmount: Amount
-    }
-}
-
-export interface ManageOrderLineInput {
-    operations: Array<CancelOperation | AddOperation | UpdateOperation>
-}

+ 0 - 2
packages/payments-plugin/src/mollie/mollie.plugin.ts

@@ -10,7 +10,6 @@ import {
 
 import { shopApiExtensions, adminApiExtensions } from './api-extensions';
 import { PLUGIN_INIT_OPTIONS } from './constants';
-import { orderCustomFields } from './custom-fields';
 import { MollieCommonResolver } from './mollie.common-resolver';
 import { MollieController } from './mollie.controller';
 import { molliePaymentHandler } from './mollie.handler';
@@ -195,7 +194,6 @@ export interface MolliePluginOptions {
     providers: [MollieService, { provide: PLUGIN_INIT_OPTIONS, useFactory: () => MolliePlugin.options }],
     configuration: (config: RuntimeVendureConfig) => {
         config.paymentOptions.paymentMethodHandlers.push(molliePaymentHandler);
-        config.customFields.Order.push(...orderCustomFields);
         return config;
     },
     shopApiExtensions: {

+ 22 - 110
packages/payments-plugin/src/mollie/mollie.service.ts

@@ -1,4 +1,9 @@
-import { Order as MollieOrder, OrderStatus, PaymentMethod as MollieClientMethod } from '@mollie/api-client';
+import createMollieClient, {
+    Order as MollieOrder,
+    OrderStatus,
+    PaymentMethod as MollieClientMethod,
+    MollieClient,
+} from '@mollie/api-client';
 import { CreateParameters } from '@mollie/api-client/dist/types/src/binders/orders/parameters';
 import { Inject, Injectable } from '@nestjs/common';
 import { ModuleRef } from '@nestjs/core';
@@ -25,12 +30,6 @@ import { OrderStateMachine } from '@vendure/core/';
 import { totalCoveredByPayments } from '@vendure/core/dist/service/helpers/utils/order-utils';
 
 import { loggerCtx, PLUGIN_INIT_OPTIONS } from './constants';
-import { OrderWithMollieReference } from './custom-fields';
-import {
-    createExtendedMollieClient,
-    ExtendedMollieClient,
-    ManageOrderLineInput,
-} from './extended-mollie-client';
 import {
     ErrorCode,
     MolliePaymentIntentError,
@@ -69,7 +68,6 @@ export class MollieService {
         private activeOrderService: ActiveOrderService,
         private orderService: OrderService,
         private entityHydrator: EntityHydrator,
-        private variantService: ProductVariantService,
         private moduleRef: ModuleRef,
     ) {
         this.injector = new Injector(this.moduleRef);
@@ -154,7 +152,7 @@ export class MollieService {
             );
             return new PaymentIntentError(`Paymentmethod ${paymentMethod.code} has no apiKey configured`);
         }
-        const mollieClient = createExtendedMollieClient({ apiKey });
+        const mollieClient = createMollieClient({ apiKey });
         const vendureHost = this.options.vendureHost.endsWith('/')
             ? this.options.vendureHost.slice(0, -1)
             : this.options.vendureHost; // remove appending slash
@@ -207,31 +205,6 @@ export class MollieService {
         if (molliePaymentMethodCode) {
             orderInput.method = molliePaymentMethodCode as MollieClientMethod;
         }
-        const existingMollieOrderId = (order as OrderWithMollieReference).customFields.mollieOrderId;
-        if (existingMollieOrderId) {
-            // Update order and return its checkoutUrl
-            const updateMollieOrder = await this.updateMollieOrder(
-                mollieClient,
-                orderInput,
-                existingMollieOrderId,
-            ).catch(e => {
-                Logger.error(
-                    `Failed to update Mollie order '${existingMollieOrderId}' for '${order.code}': ${(e as Error).message}`,
-                    loggerCtx,
-                );
-            });
-            const checkoutUrl = updateMollieOrder?.getCheckoutUrl();
-            if (checkoutUrl) {
-                Logger.info(
-                    `Updated Mollie order '${updateMollieOrder?.id as string}' for order '${order.code}'`,
-                    loggerCtx,
-                );
-                return {
-                    url: checkoutUrl,
-                };
-            }
-        }
-        // Otherwise create a new Mollie order
         const mollieOrder = await mollieClient.orders.create(orderInput);
         // Save async, because this shouldn't impact intent creation
         this.orderService.updateCustomFields(ctx, order.id, { mollieOrderId: mollieOrder.id }).catch(e => {
@@ -268,7 +241,7 @@ export class MollieService {
         if (!apiKey) {
             throw Error(`No apiKey found for payment ${paymentMethod.id} for channel ${ctx.channel.token}`);
         }
-        const client = createExtendedMollieClient({ apiKey });
+        const client = createMollieClient({ apiKey });
         const mollieOrder = await client.orders.get(orderId);
         if (mollieOrder.metadata?.languageCode) {
             // Recreate ctx with the original languageCode
@@ -291,6 +264,19 @@ export class MollieService {
                 `Unable to find order ${mollieOrder.orderNumber}, unable to process Mollie order ${mollieOrder.id}`,
             );
         }
+        if (order.orderPlacedAt) {
+            const paymentWithSameTransactionId = order.payments.find(
+                p => p.transactionId === mollieOrder.id && p.state === 'Settled',
+            );
+            if (!paymentWithSameTransactionId) {
+                // The order is paid for again, with another transaction ID. This means the customer paid twice
+                Logger.error(
+                    `Order '${order.code}' is already paid. Mollie order '${mollieOrder.id}' should be refunded.`,
+                    loggerCtx,
+                );
+                return;
+            }
+        }
         const statesThatRequireAction: OrderState[] = [
             'AddingItems',
             'ArrangingPayment',
@@ -414,7 +400,7 @@ export class MollieService {
             throw Error(`No apiKey configured for payment method ${paymentMethodCode}`);
         }
 
-        const client = createExtendedMollieClient({ apiKey });
+        const client = createMollieClient({ apiKey });
         const activeOrder = await this.activeOrderService.getActiveOrder(ctx, undefined);
         const additionalParams = await this.options.enabledPaymentMethodsParams?.(
             this.injector,
@@ -433,80 +419,6 @@ export class MollieService {
         }));
     }
 
-    async getVariantsWithInsufficientStock(ctx: RequestContext, order: Order): Promise<ProductVariant[]> {
-        const variantsWithInsufficientSaleableStock: ProductVariant[] = [];
-        for (const line of order.lines) {
-            const availableStock = await this.variantService.getSaleableStockLevel(ctx, line.productVariant);
-            if (line.quantity > availableStock) {
-                variantsWithInsufficientSaleableStock.push(line.productVariant);
-            }
-        }
-        return variantsWithInsufficientSaleableStock;
-    }
-
-    /**
-     * Update an existing Mollie order based on the given Vendure order.
-     */
-    async updateMollieOrder(
-        mollieClient: ExtendedMollieClient,
-        newMollieOrderInput: CreateParameters,
-        mollieOrderId: string,
-    ): Promise<MollieOrder> {
-        const existingMollieOrder = await mollieClient.orders.get(mollieOrderId);
-        const [order] = await Promise.all([
-            this.updateMollieOrderData(mollieClient, existingMollieOrder, newMollieOrderInput),
-            this.updateMollieOrderLines(mollieClient, existingMollieOrder, newMollieOrderInput.lines),
-        ]);
-        return order;
-    }
-
-    /**
-     * Update the Mollie Order data itself, excluding the order lines.
-     * So, addresses, redirect url etc
-     */
-    private async updateMollieOrderData(
-        mollieClient: ExtendedMollieClient,
-        existingMollieOrder: MollieOrder,
-        newMollieOrderInput: CreateParameters,
-    ): Promise<MollieOrder> {
-        return await mollieClient.orders.update(existingMollieOrder.id, {
-            billingAddress: newMollieOrderInput.billingAddress,
-            shippingAddress: newMollieOrderInput.shippingAddress,
-            redirectUrl: newMollieOrderInput.redirectUrl,
-        });
-    }
-
-    /**
-     * Delete all order lines of current Mollie order, and create new ones based on the new Vendure order lines
-     */
-    private async updateMollieOrderLines(
-        mollieClient: ExtendedMollieClient,
-        existingMollieOrder: MollieOrder,
-        /**
-         * These are the new order lines based on the Vendure order
-         */
-        newMollieOrderLines: CreateParameters['lines'],
-    ): Promise<MollieOrder> {
-        const manageOrderLinesInput: ManageOrderLineInput = {
-            operations: [],
-        };
-        // Cancel all previous order lines and create new ones
-        existingMollieOrder.lines.forEach(existingLine => {
-            manageOrderLinesInput.operations.push({
-                operation: 'cancel',
-                data: { id: existingLine.id },
-            });
-        });
-        // Add new order lines
-        newMollieOrderLines.forEach(newLine => {
-            manageOrderLinesInput.operations.push({
-                operation: 'add',
-                data: newLine,
-            });
-        });
-        return await mollieClient.manageOrderLines(existingMollieOrder.id, manageOrderLinesInput);
-    }
-
     /**
      * Dry run a transition to a given state.
      * As long as we don't call 'finalize', the transition never completes.

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/sentry-plugin",
-    "version": "3.0.0",
+    "version": "3.0.1",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -22,7 +22,7 @@
     },
     "devDependencies": {
         "@sentry/node": "^7.106.1",
-        "@vendure/common": "^3.0.0",
-        "@vendure/core": "^3.0.0"
+        "@vendure/common": "^3.0.1",
+        "@vendure/core": "^3.0.1"
     }
 }

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/stellate-plugin",
-    "version": "3.0.0",
+    "version": "3.0.1",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -21,7 +21,7 @@
         "node-fetch": "^2.7.0"
     },
     "devDependencies": {
-        "@vendure/common": "^3.0.0",
-        "@vendure/core": "^3.0.0"
+        "@vendure/common": "^3.0.1",
+        "@vendure/core": "^3.0.1"
     }
 }

+ 3 - 3
packages/testing/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/testing",
-    "version": "3.0.0",
+    "version": "3.0.1",
     "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": "^3.0.0",
+        "@vendure/common": "^3.0.1",
         "faker": "^4.1.0",
         "form-data": "^4.0.0",
         "graphql": "~16.9.0",
@@ -50,7 +50,7 @@
         "@types/mysql": "^2.15.26",
         "@types/node-fetch": "^2.6.4",
         "@types/pg": "^8.11.2",
-        "@vendure/core": "^3.0.0",
+        "@vendure/core": "^3.0.1",
         "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": "3.0.0",
+    "version": "3.0.1",
     "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": "^3.0.0",
-        "@vendure/common": "^3.0.0",
+        "@vendure/admin-ui": "^3.0.1",
+        "@vendure/common": "^3.0.1",
         "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": "^3.0.0",
+        "@vendure/core": "^3.0.1",
         "react": "^18.2.0",
         "react-dom": "^18.2.0",
         "rimraf": "^5.0.5",