From d16d10619dfbd3966a4709753de3d8cc37c6f2eb Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Tue, 2 Jan 2024 17:11:55 +0100 Subject: [PATCH 1/8] Feat(medusa-test-utils, utils, pricing, product, link-modules): upgrade mikro orm version to latest (#5985) * update mikro-orm version * add changeset * update product tests * add optional number serializer util * upgrade cart mikro-orm versions * clean up test --- .changeset/perfect-pugs-brake.md | 9 + packages/authentication/package.json | 8 +- packages/cart/package.json | 8 +- packages/link-modules/package.json | 4 +- packages/medusa-test-utils/package.json | 4 +- packages/pricing/package.json | 8 +- .../integration-tests/__tests__/module.ts | 6 +- .../product-module-service/products.spec.ts | 4 +- packages/product/package.json | 8 +- .../product/src/models/product-variant.ts | 19 +- packages/promotion/package.json | 8 +- packages/utils/package.json | 6 +- packages/utils/src/common/index.ts | 1 + .../src/common/optional-numeric-serializer.ts | 4 + yarn.lock | 215 ++++++++++-------- 15 files changed, 181 insertions(+), 131 deletions(-) create mode 100644 .changeset/perfect-pugs-brake.md create mode 100644 packages/utils/src/common/optional-numeric-serializer.ts diff --git a/.changeset/perfect-pugs-brake.md b/.changeset/perfect-pugs-brake.md new file mode 100644 index 0000000000000..2acc16d84032b --- /dev/null +++ b/.changeset/perfect-pugs-brake.md @@ -0,0 +1,9 @@ +--- +"medusa-test-utils": patch +"@medusajs/link-modules": patch +"@medusajs/pricing": patch +"@medusajs/product": patch +"@medusajs/utils": patch +--- + +fix(medusa-test-utils, utils, link-modules, pricing, product): upgrade mikro-orm version diff --git a/packages/authentication/package.json b/packages/authentication/package.json index 962cae64385f4..7ae37e790a788 100644 --- a/packages/authentication/package.json +++ b/packages/authentication/package.json @@ -39,7 +39,7 @@ "orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear" }, "devDependencies": { - "@mikro-orm/cli": "5.7.12", + "@mikro-orm/cli": "5.9.7", "cross-env": "^5.2.1", "jest": "^29.6.3", "medusa-test-utils": "^1.1.40", @@ -53,9 +53,9 @@ "@medusajs/modules-sdk": "^1.12.5", "@medusajs/types": "^1.11.9", "@medusajs/utils": "^1.11.2", - "@mikro-orm/core": "5.7.12", - "@mikro-orm/migrations": "5.7.12", - "@mikro-orm/postgresql": "5.7.12", + "@mikro-orm/core": "5.9.7", + "@mikro-orm/migrations": "5.9.7", + "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.0", "dotenv": "^16.1.4", "knex": "2.4.2" diff --git a/packages/cart/package.json b/packages/cart/package.json index ffa37bcbc5501..c84323c9770ee 100644 --- a/packages/cart/package.json +++ b/packages/cart/package.json @@ -39,7 +39,7 @@ "orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear" }, "devDependencies": { - "@mikro-orm/cli": "5.7.12", + "@mikro-orm/cli": "5.9.7", "cross-env": "^5.2.1", "jest": "^29.6.3", "medusa-test-utils": "^1.1.40", @@ -53,9 +53,9 @@ "@medusajs/modules-sdk": "^1.12.5", "@medusajs/types": "^1.11.9", "@medusajs/utils": "^1.11.2", - "@mikro-orm/core": "5.7.12", - "@mikro-orm/migrations": "5.7.12", - "@mikro-orm/postgresql": "5.7.12", + "@mikro-orm/core": "5.9.7", + "@mikro-orm/migrations": "5.9.7", + "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.0", "dotenv": "^16.1.4", "knex": "2.4.2" diff --git a/packages/link-modules/package.json b/packages/link-modules/package.json index 5fee5431a3ef6..89d4312bb2414 100644 --- a/packages/link-modules/package.json +++ b/packages/link-modules/package.json @@ -42,8 +42,8 @@ "@medusajs/modules-sdk": "^1.12.5", "@medusajs/types": "^1.11.9", "@medusajs/utils": "^1.11.2", - "@mikro-orm/core": "5.7.12", - "@mikro-orm/postgresql": "5.7.12", + "@mikro-orm/core": "5.9.7", + "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.0" } } diff --git a/packages/medusa-test-utils/package.json b/packages/medusa-test-utils/package.json index 30b6144fb3f2e..330003132e8d3 100644 --- a/packages/medusa-test-utils/package.json +++ b/packages/medusa-test-utils/package.json @@ -30,8 +30,8 @@ "typescript": "^5.1.6" }, "dependencies": { - "@mikro-orm/migrations": "5.7.12", - "@mikro-orm/postgresql": "5.7.12", + "@mikro-orm/migrations": "5.9.7", + "@mikro-orm/postgresql": "5.9.7", "medusa-core-utils": "^1.2.0", "pg-god": "^1.0.12", "randomatic": "^3.1.1" diff --git a/packages/pricing/package.json b/packages/pricing/package.json index e276550c3127e..b444f936684f1 100644 --- a/packages/pricing/package.json +++ b/packages/pricing/package.json @@ -39,7 +39,7 @@ "orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear" }, "devDependencies": { - "@mikro-orm/cli": "5.7.12", + "@mikro-orm/cli": "5.9.7", "cross-env": "^5.2.1", "jest": "^29.6.3", "medusa-test-utils": "^1.1.40", @@ -53,9 +53,9 @@ "@medusajs/modules-sdk": "^1.12.5", "@medusajs/types": "^1.11.9", "@medusajs/utils": "^1.11.2", - "@mikro-orm/core": "5.7.12", - "@mikro-orm/migrations": "5.7.12", - "@mikro-orm/postgresql": "5.7.12", + "@mikro-orm/core": "5.9.7", + "@mikro-orm/migrations": "5.9.7", + "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.0", "dotenv": "^16.1.4", "knex": "2.4.2" diff --git a/packages/product/integration-tests/__tests__/module.ts b/packages/product/integration-tests/__tests__/module.ts index ab89fddb3df79..8ca43c394776b 100644 --- a/packages/product/integration-tests/__tests__/module.ts +++ b/packages/product/integration-tests/__tests__/module.ts @@ -181,8 +181,10 @@ describe("Product module", function () { it("should have a connection that is the shared connection", async () => { expect( - (module as any).baseRepository_.manager_.getConnection().client - ).toEqual(sharedPgConnection) + JSON.stringify( + (module as any).baseRepository_.manager_.getConnection().client + ) + ).toEqual(JSON.stringify(sharedPgConnection)) }) }) diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts index 9f1d17c35c6e9..62dc805468c06 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts @@ -265,8 +265,8 @@ describe("ProductModuleService products", function () { sku: createdVariant.sku, allow_backorder: false, manage_inventory: true, - inventory_quantity: "100", - variant_rank: "0", + inventory_quantity: 100, + variant_rank: 0, options: expect.arrayContaining([ expect.objectContaining({ id: expect.any(String), diff --git a/packages/product/package.json b/packages/product/package.json index c925e861433f6..9e2e10893a05a 100644 --- a/packages/product/package.json +++ b/packages/product/package.json @@ -39,7 +39,7 @@ "orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear" }, "devDependencies": { - "@mikro-orm/cli": "5.7.12", + "@mikro-orm/cli": "5.9.7", "cross-env": "^5.2.1", "faker": "^6.6.6", "jest": "^29.6.3", @@ -55,9 +55,9 @@ "@medusajs/modules-sdk": "^1.12.5", "@medusajs/types": "^1.11.9", "@medusajs/utils": "^1.11.2", - "@mikro-orm/core": "5.7.12", - "@mikro-orm/migrations": "5.7.12", - "@mikro-orm/postgresql": "5.7.12", + "@mikro-orm/core": "5.9.7", + "@mikro-orm/migrations": "5.9.7", + "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.0", "dotenv": "^16.1.4", "knex": "2.4.2", diff --git a/packages/product/src/models/product-variant.ts b/packages/product/src/models/product-variant.ts index a66bc5bc0e66e..0c7b42b0dbc71 100644 --- a/packages/product/src/models/product-variant.ts +++ b/packages/product/src/models/product-variant.ts @@ -1,4 +1,8 @@ -import { DALUtils, generateEntityId } from "@medusajs/utils" +import { + DALUtils, + generateEntityId, + optionalNumericSerializer, +} from "@medusajs/utils" import { BeforeCreate, Cascade, @@ -66,7 +70,11 @@ class ProductVariant { // Note: Upon serialization, this turns to a string. This is on purpose, because you would loose // precision if you cast numeric to JS number, as JS number is a float. // Ref: https://github.com/mikro-orm/mikro-orm/issues/2295 - @Property({ columnType: "numeric", default: 100 }) + @Property({ + columnType: "numeric", + default: 100, + serializer: optionalNumericSerializer, + }) inventory_quantity?: number = 100 @Property({ columnType: "boolean", default: false }) @@ -102,7 +110,12 @@ class ProductVariant { @Property({ columnType: "jsonb", nullable: true }) metadata?: Record | null - @Property({ columnType: "numeric", nullable: true, default: 0 }) + @Property({ + columnType: "numeric", + nullable: true, + default: 0, + serializer: optionalNumericSerializer, + }) variant_rank?: number | null @Property({ columnType: "text", nullable: true }) diff --git a/packages/promotion/package.json b/packages/promotion/package.json index 10c86f3989e14..ae0f7f3b936b2 100644 --- a/packages/promotion/package.json +++ b/packages/promotion/package.json @@ -39,7 +39,7 @@ "orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear" }, "devDependencies": { - "@mikro-orm/cli": "5.7.12", + "@mikro-orm/cli": "5.9.7", "cross-env": "^5.2.1", "jest": "^29.6.3", "medusa-test-utils": "^1.1.40", @@ -53,9 +53,9 @@ "@medusajs/modules-sdk": "^1.12.5", "@medusajs/types": "^1.11.9", "@medusajs/utils": "^1.11.2", - "@mikro-orm/core": "5.7.12", - "@mikro-orm/migrations": "5.7.12", - "@mikro-orm/postgresql": "5.7.12", + "@mikro-orm/core": "5.9.7", + "@mikro-orm/migrations": "5.9.7", + "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.0", "dotenv": "^16.1.4", "knex": "2.4.2" diff --git a/packages/utils/package.json b/packages/utils/package.json index 6d67bbb153255..1e62b1057fc7c 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -31,9 +31,9 @@ }, "dependencies": { "@medusajs/types": "^1.11.9", - "@mikro-orm/core": "5.7.12", - "@mikro-orm/migrations": "5.7.12", - "@mikro-orm/postgresql": "5.7.12", + "@mikro-orm/core": "5.9.7", + "@mikro-orm/migrations": "5.9.7", + "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.1", "knex": "2.4.2", "ulid": "^2.3.0" diff --git a/packages/utils/src/common/index.ts b/packages/utils/src/common/index.ts index dd1bed27283bf..2baca9b213eba 100644 --- a/packages/utils/src/common/index.ts +++ b/packages/utils/src/common/index.ts @@ -21,6 +21,7 @@ export * from "./map-object-to" export * from "./medusa-container" export * from "./object-from-string-path" export * from "./object-to-string-path" +export * from "./optional-numeric-serializer" export * from "./promise-all" export * from "./remote-query-object-from-string" export * from "./remote-query-object-to-string" diff --git a/packages/utils/src/common/optional-numeric-serializer.ts b/packages/utils/src/common/optional-numeric-serializer.ts new file mode 100644 index 0000000000000..7b04521e28fc3 --- /dev/null +++ b/packages/utils/src/common/optional-numeric-serializer.ts @@ -0,0 +1,4 @@ +import { isDefined } from "./is-defined" + +export const optionalNumericSerializer = (value) => + isDefined(value) && value !== null ? Number(value) : value diff --git a/yarn.lock b/yarn.lock index 72ee38ebed14b..b153d61a67295 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7412,10 +7412,10 @@ __metadata: "@medusajs/modules-sdk": ^1.12.5 "@medusajs/types": ^1.11.9 "@medusajs/utils": ^1.11.2 - "@mikro-orm/cli": 5.7.12 - "@mikro-orm/core": 5.7.12 - "@mikro-orm/migrations": 5.7.12 - "@mikro-orm/postgresql": 5.7.12 + "@mikro-orm/cli": 5.9.7 + "@mikro-orm/core": 5.9.7 + "@mikro-orm/migrations": 5.9.7 + "@mikro-orm/postgresql": 5.9.7 awilix: ^8.0.0 cross-env: ^5.2.1 dotenv: ^16.1.4 @@ -7471,10 +7471,10 @@ __metadata: "@medusajs/modules-sdk": ^1.12.5 "@medusajs/types": ^1.11.9 "@medusajs/utils": ^1.11.2 - "@mikro-orm/cli": 5.7.12 - "@mikro-orm/core": 5.7.12 - "@mikro-orm/migrations": 5.7.12 - "@mikro-orm/postgresql": 5.7.12 + "@mikro-orm/cli": 5.9.7 + "@mikro-orm/core": 5.9.7 + "@mikro-orm/migrations": 5.9.7 + "@mikro-orm/postgresql": 5.9.7 awilix: ^8.0.0 cross-env: ^5.2.1 dotenv: ^16.1.4 @@ -7634,8 +7634,8 @@ __metadata: "@medusajs/modules-sdk": ^1.12.5 "@medusajs/types": ^1.11.9 "@medusajs/utils": ^1.11.2 - "@mikro-orm/core": 5.7.12 - "@mikro-orm/postgresql": 5.7.12 + "@mikro-orm/core": 5.9.7 + "@mikro-orm/postgresql": 5.9.7 awilix: ^8.0.0 cross-env: ^5.2.1 jest: ^29.6.3 @@ -7908,10 +7908,10 @@ __metadata: "@medusajs/modules-sdk": ^1.12.5 "@medusajs/types": ^1.11.9 "@medusajs/utils": ^1.11.2 - "@mikro-orm/cli": 5.7.12 - "@mikro-orm/core": 5.7.12 - "@mikro-orm/migrations": 5.7.12 - "@mikro-orm/postgresql": 5.7.12 + "@mikro-orm/cli": 5.9.7 + "@mikro-orm/core": 5.9.7 + "@mikro-orm/migrations": 5.9.7 + "@mikro-orm/postgresql": 5.9.7 awilix: ^8.0.0 cross-env: ^5.2.1 dotenv: ^16.1.4 @@ -7937,10 +7937,10 @@ __metadata: "@medusajs/modules-sdk": ^1.12.5 "@medusajs/types": ^1.11.9 "@medusajs/utils": ^1.11.2 - "@mikro-orm/cli": 5.7.12 - "@mikro-orm/core": 5.7.12 - "@mikro-orm/migrations": 5.7.12 - "@mikro-orm/postgresql": 5.7.12 + "@mikro-orm/cli": 5.9.7 + "@mikro-orm/core": 5.9.7 + "@mikro-orm/migrations": 5.9.7 + "@mikro-orm/postgresql": 5.9.7 awilix: ^8.0.0 cross-env: ^5.2.1 dotenv: ^16.1.4 @@ -7969,10 +7969,10 @@ __metadata: "@medusajs/modules-sdk": ^1.12.5 "@medusajs/types": ^1.11.9 "@medusajs/utils": ^1.11.2 - "@mikro-orm/cli": 5.7.12 - "@mikro-orm/core": 5.7.12 - "@mikro-orm/migrations": 5.7.12 - "@mikro-orm/postgresql": 5.7.12 + "@mikro-orm/cli": 5.9.7 + "@mikro-orm/core": 5.9.7 + "@mikro-orm/migrations": 5.9.7 + "@mikro-orm/postgresql": 5.9.7 awilix: ^8.0.0 cross-env: ^5.2.1 dotenv: ^16.1.4 @@ -8142,9 +8142,9 @@ __metadata: resolution: "@medusajs/utils@workspace:packages/utils" dependencies: "@medusajs/types": ^1.11.9 - "@mikro-orm/core": 5.7.12 - "@mikro-orm/migrations": 5.7.12 - "@mikro-orm/postgresql": 5.7.12 + "@mikro-orm/core": 5.9.7 + "@mikro-orm/migrations": 5.9.7 + "@mikro-orm/postgresql": 5.9.7 "@types/express": ^4.17.17 awilix: ^8.0.1 cross-env: ^5.2.1 @@ -8176,13 +8176,13 @@ __metadata: languageName: unknown linkType: soft -"@mikro-orm/cli@npm:5.7.12": - version: 5.7.12 - resolution: "@mikro-orm/cli@npm:5.7.12" +"@mikro-orm/cli@npm:5.9.7": + version: 5.9.7 + resolution: "@mikro-orm/cli@npm:5.9.7" dependencies: "@jercle/yargonaut": 1.1.5 - "@mikro-orm/core": ~5.7.12 - "@mikro-orm/knex": ~5.7.12 + "@mikro-orm/core": 5.9.7 + "@mikro-orm/knex": 5.9.7 fs-extra: 11.1.1 tsconfig-paths: 4.2.0 yargs: 17.7.2 @@ -8221,20 +8221,20 @@ __metadata: bin: mikro-orm: cli.js mikro-orm-esm: esm.js - checksum: dba36b1202a127132c325694348b5f5a44a35c4ac59eefb89367712d3026c5453fb9dd356c0d43884a736f404bf7a8ebda6383d85a7023204035740d8ecc21e0 + checksum: 2b0f430485ff4e25a0ee53d776a30832d8b53c83db8f70d2d5305039c042384714e9525169e9bfd897f1c9936242aaf80aaf9e9f917c8894517343cc14f56987 languageName: node linkType: hard -"@mikro-orm/core@npm:5.7.12, @mikro-orm/core@npm:~5.7.12": - version: 5.7.12 - resolution: "@mikro-orm/core@npm:5.7.12" +"@mikro-orm/core@npm:5.9.7": + version: 5.9.7 + resolution: "@mikro-orm/core@npm:5.9.7" dependencies: acorn-loose: 8.3.0 acorn-walk: 8.2.0 - dotenv: 16.1.4 + dotenv: 16.3.1 fs-extra: 11.1.1 globby: 11.1.0 - mikro-orm: ~5.7.12 + mikro-orm: 5.9.7 reflect-metadata: 0.1.13 peerDependencies: "@mikro-orm/better-sqlite": ^5.0.0 @@ -8268,16 +8268,16 @@ __metadata: optional: true "@mikro-orm/sqlite": optional: true - checksum: 1458e1bf069870dbf6d2a110a5c80537401e18415a491967d6ae77b9b195ed50c5e829c3409f7839163dd4bfc1b833c8f0de0466a50a798f2ddd64df5e6296fa + checksum: f58ad6b717571b6c9d4f880a886a34f7de51e7d299c93be5ac9b0733c41fdd22278eae14ca5a1e1e7efb045bc36caa83b639745fc0be7b5a696e49b13bdbbd03 languageName: node linkType: hard -"@mikro-orm/knex@npm:~5.7.12": - version: 5.7.12 - resolution: "@mikro-orm/knex@npm:5.7.12" +"@mikro-orm/knex@npm:5.9.7": + version: 5.9.7 + resolution: "@mikro-orm/knex@npm:5.9.7" dependencies: fs-extra: 11.1.1 - knex: 2.4.2 + knex: 2.5.1 sqlstring: 2.3.3 peerDependencies: "@mikro-orm/core": ^5.0.0 @@ -8306,30 +8306,30 @@ __metadata: optional: true sqlite3: optional: true - checksum: f8b334ce7d776303cebbfd3cf1181daa5b797bd287e94b54d0151866b24c18ac2a7c68a850ed27a26565a850d2e62cd4478af601deb6e24f83ec5db4a556d11b + checksum: 2cce6ef6907d0785e84a9c801980e58ddd3539dbd3dd0b0ced1e3ccddcbc5dfb704816abb97ae7d9c608d6b30d842dd1f59a6ea97ca1d854ebd55df303070500 languageName: node linkType: hard -"@mikro-orm/migrations@npm:5.7.12": - version: 5.7.12 - resolution: "@mikro-orm/migrations@npm:5.7.12" +"@mikro-orm/migrations@npm:5.9.7": + version: 5.9.7 + resolution: "@mikro-orm/migrations@npm:5.9.7" dependencies: - "@mikro-orm/knex": ~5.7.12 + "@mikro-orm/knex": 5.9.7 fs-extra: 11.1.1 - knex: 2.4.2 - umzug: 3.2.1 + knex: 2.5.1 + umzug: 3.3.1 peerDependencies: "@mikro-orm/core": ^5.0.0 - checksum: 93513f15f46f8f612fb90762712e24aa8d877bc6b59bfa903221f42e85140fcc724cb519bf74fa8be2acd40819525684acbc0d4b44a8579f1c84457f6962523b + checksum: 3b1c3e449d2aee7ed7f03153e17e2772423115b895836e4ece40c9b318849d8de9cce1ad9fa545a8ef54fa7fe5ab474c3444e2c598ebc9f3e070fe382882862a languageName: node linkType: hard -"@mikro-orm/postgresql@npm:5.7.12": - version: 5.7.12 - resolution: "@mikro-orm/postgresql@npm:5.7.12" +"@mikro-orm/postgresql@npm:5.9.7": + version: 5.9.7 + resolution: "@mikro-orm/postgresql@npm:5.9.7" dependencies: - "@mikro-orm/knex": ~5.7.12 - pg: 8.11.0 + "@mikro-orm/knex": 5.9.7 + pg: 8.11.3 peerDependencies: "@mikro-orm/core": ^5.0.0 "@mikro-orm/entity-generator": ^5.0.0 @@ -8342,7 +8342,7 @@ __metadata: optional: true "@mikro-orm/seeder": optional: true - checksum: b6aecd3eb9013720e242390f9a4b470ff45f222ccddfeb8b260a9c0682073dbe9b6cfb9f688b7fde19197748a9026073d0693d66a7a0b2de2c83abfbd1fec291 + checksum: 9380c0a5795fe6c25448a5c302877043a0239d25e087a0dec4ebca63e8f06a6af9435953a55141ac8317cb14e2b141df4c10c4615be1a1deecc5cce7127bf42c languageName: node linkType: hard @@ -24265,13 +24265,6 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:16.1.4": - version: 16.1.4 - resolution: "dotenv@npm:16.1.4" - checksum: 47cf5ce136bf2a5e8402fc9855d95848973cfee423fe0adf1a7cd565c842c51e5b8c95889b075140c26b092b74dd2a319970ff496cd7159ab3f1fc58edfc0ede - languageName: node - linkType: hard - "dotenv@npm:16.3.1, dotenv@npm:^16.0.0, dotenv@npm:^16.0.3, dotenv@npm:^16.1.4": version: 16.3.1 resolution: "dotenv@npm:16.3.1" @@ -24429,14 +24422,7 @@ __metadata: languageName: node linkType: hard -"emittery@npm:^0.12.1": - version: 0.12.1 - resolution: "emittery@npm:0.12.1" - checksum: 7a8395bdcebd6bd42054469c93f453308f93d67a81f8fe08f7047c824b4623794f03aefd0a23e73d967bb3b9f722ba7eff216c808bb80caaa7d13c42227e06c8 - languageName: node - linkType: hard - -"emittery@npm:^0.13.1": +"emittery@npm:^0.13.0, emittery@npm:^0.13.1": version: 0.13.1 resolution: "emittery@npm:0.13.1" checksum: 1573d0ae29ab34661b6c63251ff8f5facd24ccf6a823f19417ae8ba8c88ea450325788c67f16c99edec8de4b52ce93a10fe441ece389fd156e88ee7dab9bfa35 @@ -27358,16 +27344,6 @@ __metadata: languageName: node linkType: hard -"fs-jetpack@npm:^4.3.1": - version: 4.3.1 - resolution: "fs-jetpack@npm:4.3.1" - dependencies: - minimatch: ^3.0.2 - rimraf: ^2.6.3 - checksum: 5d27e829233de005505417bae2f55412ae65ff63a57b68ac6d3cd8dde29ed9f0797c2a83356d20237bf74f516db8e40636c5fc238b49b4414b3d9339e60f7914 - languageName: node - linkType: hard - "fs-merger@npm:^3.2.1": version: 3.2.1 resolution: "fs-merger@npm:3.2.1" @@ -34399,6 +34375,45 @@ __metadata: languageName: node linkType: hard +"knex@npm:2.5.1": + version: 2.5.1 + resolution: "knex@npm:2.5.1" + dependencies: + colorette: 2.0.19 + commander: ^10.0.0 + debug: 4.3.4 + escalade: ^3.1.1 + esm: ^3.2.25 + get-package-type: ^0.1.0 + getopts: 2.3.0 + interpret: ^2.2.0 + lodash: ^4.17.21 + pg-connection-string: 2.6.1 + rechoir: ^0.8.0 + resolve-from: ^5.0.0 + tarn: ^3.0.2 + tildify: 2.0.0 + peerDependenciesMeta: + better-sqlite3: + optional: true + mysql: + optional: true + mysql2: + optional: true + pg: + optional: true + pg-native: + optional: true + sqlite3: + optional: true + tedious: + optional: true + bin: + knex: bin/cli.js + checksum: 33580641feaa93074bdc10e78f06382e5017d46a3cd22821c09057e7da303c35772add39a110fffe685c2f438a78751a0eb6f50aad1c4bdb032e8ec9b7879b69 + languageName: node + linkType: hard + "kuler@npm:^2.0.0": version: 2.0.0 resolution: "kuler@npm:2.0.0" @@ -36458,8 +36473,8 @@ __metadata: version: 0.0.0-use.local resolution: "medusa-test-utils@workspace:packages/medusa-test-utils" dependencies: - "@mikro-orm/migrations": 5.7.12 - "@mikro-orm/postgresql": 5.7.12 + "@mikro-orm/migrations": 5.9.7 + "@mikro-orm/postgresql": 5.9.7 cross-env: ^5.2.1 jest: ^25.5.4 medusa-core-utils: ^1.2.0 @@ -36710,10 +36725,10 @@ __metadata: languageName: node linkType: hard -"mikro-orm@npm:~5.7.12": - version: 5.7.12 - resolution: "mikro-orm@npm:5.7.12" - checksum: 6b71e7b9803b7cbd934c6830df7fc010201ef48a9efaebbce98f3613896114cd464bf5a1d0746b42b14aea92e29a593f75775482d64e4cf856ffdbe341379edf +"mikro-orm@npm:5.9.7": + version: 5.9.7 + resolution: "mikro-orm@npm:5.9.7" + checksum: 01eb2b71e5f830c064749779d30ed1772e28420cfedeafbbbce8599714eae308af467014001e0404d7ba43de9f1f979a05fd6b267a89c8f7e1e8ec6013fb5120 languageName: node linkType: hard @@ -39388,7 +39403,7 @@ __metadata: languageName: node linkType: hard -"pg-connection-string@npm:^2.5.0": +"pg-connection-string@npm:2.6.1, pg-connection-string@npm:^2.5.0": version: 2.6.1 resolution: "pg-connection-string@npm:2.6.1" checksum: e5a71a2da143b8dc17143a9db7737679b210643771aa678d3bc60c7bc70da11bbb8e2d531be91c8c4eddd6ac6046307811e793f5850b9ba595a11785c948a417 @@ -39688,7 +39703,7 @@ __metadata: languageName: node linkType: hard -"pony-cause@npm:^2.1.2": +"pony-cause@npm:^2.1.4": version: 2.1.10 resolution: "pony-cause@npm:2.1.10" checksum: 55ad0ca52039895f273c69e55fc9fe882deff38689dc5962558bfa16cce0ea7cb5bb7b67d0c43ec9c3e7edeb81f81ee8c1113014930d77b2cbac5adc4ac7fb64 @@ -47734,13 +47749,20 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^2.18.0, type-fest@npm:^2.19.0, type-fest@npm:~2.19": +"type-fest@npm:^2.19.0, type-fest@npm:~2.19": version: 2.19.0 resolution: "type-fest@npm:2.19.0" checksum: a5a7ecf2e654251613218c215c7493574594951c08e52ab9881c9df6a6da0aeca7528c213c622bc374b4e0cb5c443aa3ab758da4e3c959783ce884c3194e12cb languageName: node linkType: hard +"type-fest@npm:^3.0.0": + version: 3.13.1 + resolution: "type-fest@npm:3.13.1" + checksum: 547d22186f73a8c04590b70dcf63baff390078c75ea8acd366bbd510fd0646e348bd1970e47ecf795b7cff0b41d26e9c475c1fedd6ef5c45c82075fbf916b629 + languageName: node + linkType: hard + "type-fest@npm:^3.6.0": version: 3.12.0 resolution: "type-fest@npm:3.12.0" @@ -47994,17 +48016,16 @@ __metadata: languageName: node linkType: hard -"umzug@npm:3.2.1": - version: 3.2.1 - resolution: "umzug@npm:3.2.1" +"umzug@npm:3.3.1": + version: 3.3.1 + resolution: "umzug@npm:3.3.1" dependencies: "@rushstack/ts-command-line": ^4.12.2 - emittery: ^0.12.1 - fs-jetpack: ^4.3.1 + emittery: ^0.13.0 glob: ^8.0.3 - pony-cause: ^2.1.2 - type-fest: ^2.18.0 - checksum: ff5d417c5f0211e8c3c2529c347313ecef5db3ff4b219c71098e09884674387ac14870749ecf42ac26aabcf7559207b855fb0016eb8c50f7cd979f816d4b1545 + pony-cause: ^2.1.4 + type-fest: ^3.0.0 + checksum: 52ace90860f9e2adc9f33c786582ca758651f2de8894b5bda7af2353bec8906234dbb456eb37513ec9ceeb45eabfcd286d4abced871cc45c01015a05e4247187 languageName: node linkType: hard From 42cc8ae3f89ed7d642e51654d1a3cca011f13155 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Wed, 3 Jan 2024 09:54:48 +0100 Subject: [PATCH 2/8] feat(types,utils): added promotion create with rules and application target rules (#5957) * feat(types,utils): added promotion create with rules * chore: add rules to promotion and application method * chore: use common code for rule and values * chore: address pr reviews * chore: fix test --- .changeset/breezy-horses-destroy.md | 6 + .../promotion-module/promotion.spec.ts | 224 +++++++++++- packages/promotion/src/loaders/container.ts | 12 + .../.snapshot-medusa-promotion.json | 338 +++++++++++++++--- .../src/migrations/Migration20231221104256.ts | 34 -- .../src/migrations/Migration20240102130345.ts | 77 ++++ .../src/models/application-method.ts | 32 +- packages/promotion/src/models/index.ts | 2 + .../src/models/promotion-rule-value.ts | 37 ++ .../promotion/src/models/promotion-rule.ts | 83 +++++ packages/promotion/src/models/promotion.ts | 22 +- packages/promotion/src/repositories/index.ts | 2 + .../src/repositories/promotion-rule-value.ts | 128 +++++++ .../src/repositories/promotion-rule.ts | 124 +++++++ packages/promotion/src/services/index.ts | 2 + .../src/services/promotion-module.ts | 128 +++++-- .../src/services/promotion-rule-value.ts | 108 ++++++ .../promotion/src/services/promotion-rule.ts | 105 ++++++ packages/promotion/src/types/index.ts | 2 + .../src/types/promotion-rule-value.ts | 12 + .../promotion/src/types/promotion-rule.ts | 11 + .../promotion/src/utils/validations/index.ts | 1 + .../src/utils/validations/promotion-rule.ts | 39 ++ packages/promotion/tsconfig.json | 4 +- .../promotion/common/application-method.ts | 2 + packages/types/src/promotion/common/index.ts | 2 + .../promotion/common/promotion-rule-value.ts | 20 ++ .../src/promotion/common/promotion-rule.ts | 30 ++ .../types/src/promotion/common/promotion.ts | 2 + .../src/common/__tests__/is-present.spec.ts | 61 ++++ packages/utils/src/common/index.ts | 3 +- packages/utils/src/common/is-present.ts | 23 ++ packages/utils/src/promotion/index.ts | 10 + 33 files changed, 1562 insertions(+), 124 deletions(-) create mode 100644 .changeset/breezy-horses-destroy.md delete mode 100644 packages/promotion/src/migrations/Migration20231221104256.ts create mode 100644 packages/promotion/src/migrations/Migration20240102130345.ts create mode 100644 packages/promotion/src/models/promotion-rule-value.ts create mode 100644 packages/promotion/src/models/promotion-rule.ts create mode 100644 packages/promotion/src/repositories/promotion-rule-value.ts create mode 100644 packages/promotion/src/repositories/promotion-rule.ts create mode 100644 packages/promotion/src/services/promotion-rule-value.ts create mode 100644 packages/promotion/src/services/promotion-rule.ts create mode 100644 packages/promotion/src/types/promotion-rule-value.ts create mode 100644 packages/promotion/src/types/promotion-rule.ts create mode 100644 packages/promotion/src/utils/validations/promotion-rule.ts create mode 100644 packages/types/src/promotion/common/promotion-rule-value.ts create mode 100644 packages/types/src/promotion/common/promotion-rule.ts create mode 100644 packages/utils/src/common/__tests__/is-present.spec.ts create mode 100644 packages/utils/src/common/is-present.ts diff --git a/.changeset/breezy-horses-destroy.md b/.changeset/breezy-horses-destroy.md new file mode 100644 index 0000000000000..18ff3ec77c2f6 --- /dev/null +++ b/.changeset/breezy-horses-destroy.md @@ -0,0 +1,6 @@ +--- +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +feat(types,utils): added promotion create with rules diff --git a/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts b/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts index 1f64f0352c8f7..5c5e8742ef2f7 100644 --- a/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts +++ b/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts @@ -75,15 +75,82 @@ describe("Promotion Service", () => { }, ]) - const [promotion] = await service.list({ - id: [createdPromotion.id], - }) + const [promotion] = await service.list( + { + id: [createdPromotion.id], + }, + { + relations: ["application_method"], + } + ) + + expect(promotion).toEqual( + expect.objectContaining({ + code: "PROMOTION_TEST", + is_automatic: false, + type: "standard", + application_method: expect.objectContaining({ + type: "fixed", + target_type: "order", + value: 100, + }), + }) + ) + }) + + it("should create a promotion with order application method with rules successfully", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + application_method: { + type: "fixed", + target_type: "order", + value: 100, + target_rules: [ + { + attribute: "product_id", + operator: "eq", + values: ["prod_tshirt"], + }, + ], + }, + }, + ]) + + const [promotion] = await service.list( + { + id: [createdPromotion.id], + }, + { + relations: [ + "application_method", + "application_method.target_rules.values", + ], + } + ) expect(promotion).toEqual( expect.objectContaining({ code: "PROMOTION_TEST", is_automatic: false, type: "standard", + application_method: expect.objectContaining({ + type: "fixed", + target_type: "order", + value: 100, + target_rules: [ + expect.objectContaining({ + attribute: "product_id", + operator: "eq", + values: expect.arrayContaining([ + expect.objectContaining({ + value: "prod_tshirt", + }), + ]), + }), + ], + }), }) ) }) @@ -128,5 +195,156 @@ describe("Promotion Service", () => { "application_method.max_quantity is required when application_method.allocation is 'each'" ) }) + + it("should create a promotion with rules successfully", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer_group_id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + }, + ]) + + const [promotion] = await service.list( + { + id: [createdPromotion.id], + }, + { + relations: ["rules", "rules.values"], + } + ) + + expect(promotion).toEqual( + expect.objectContaining({ + code: "PROMOTION_TEST", + is_automatic: false, + type: "standard", + rules: [ + expect.objectContaining({ + attribute: "customer_group_id", + operator: "in", + values: expect.arrayContaining([ + expect.objectContaining({ + value: "VIP", + }), + expect.objectContaining({ + value: "top100", + }), + ]), + }), + ], + }) + ) + }) + + it("should create a promotion with rules with single value successfully", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer_group_id", + operator: "eq", + values: "VIP", + }, + ], + }, + ]) + + const [promotion] = await service.list( + { + id: [createdPromotion.id], + }, + { + relations: ["rules", "rules.values"], + } + ) + + expect(promotion).toEqual( + expect.objectContaining({ + code: "PROMOTION_TEST", + is_automatic: false, + type: "standard", + rules: [ + expect.objectContaining({ + attribute: "customer_group_id", + operator: "eq", + values: expect.arrayContaining([ + expect.objectContaining({ + value: "VIP", + }), + ]), + }), + ], + }) + ) + }) + + it("should throw an error when rule attribute is invalid", async () => { + const error = await service + .create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "", + operator: "eq", + values: "VIP", + } as any, + ], + }, + ]) + .catch((e) => e) + + expect(error.message).toContain("rules[].attribute is a required field") + }) + + it("should throw an error when rule operator is invalid", async () => { + let error = await service + .create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer_group", + operator: "", + values: "VIP", + } as any, + ], + }, + ]) + .catch((e) => e) + + expect(error.message).toContain("rules[].operator is a required field") + + error = await service + .create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer_group", + operator: "doesnotexist", + values: "VIP", + } as any, + ], + }, + ]) + .catch((e) => e) + + expect(error.message).toContain( + "rules[].operator (doesnotexist) is invalid. It should be one of gte, lte, gt, lt, eq, ne, in" + ) + }) }) }) diff --git a/packages/promotion/src/loaders/container.ts b/packages/promotion/src/loaders/container.ts index aa9eac90d7e62..39f3881115883 100644 --- a/packages/promotion/src/loaders/container.ts +++ b/packages/promotion/src/loaders/container.ts @@ -19,6 +19,12 @@ export default async ({ container.register({ promotionService: asClass(defaultServices.PromotionService).singleton(), + promotionRuleService: asClass( + defaultServices.PromotionRuleService + ).singleton(), + promotionRuleValueService: asClass( + defaultServices.PromotionRuleValueService + ).singleton(), applicationMethodService: asClass( defaultServices.ApplicationMethodService ).singleton(), @@ -44,5 +50,11 @@ function loadDefaultRepositories({ container }) { promotionRepository: asClass( defaultRepositories.PromotionRepository ).singleton(), + promotionRuleRepository: asClass( + defaultRepositories.PromotionRuleRepository + ).singleton(), + promotionRuleValueRepository: asClass( + defaultRepositories.PromotionRuleValueRepository + ).singleton(), }) } diff --git a/packages/promotion/src/migrations/.snapshot-medusa-promotion.json b/packages/promotion/src/migrations/.snapshot-medusa-promotion.json index bb21f5e21de22..70f2559a81453 100644 --- a/packages/promotion/src/migrations/.snapshot-medusa-promotion.json +++ b/packages/promotion/src/migrations/.snapshot-medusa-promotion.json @@ -1,7 +1,5 @@ { - "namespaces": [ - "public" - ], + "namespaces": ["public"], "name": "public", "tables": [ { @@ -41,10 +39,7 @@ "autoincrement": false, "primary": false, "nullable": false, - "enumItems": [ - "standard", - "buyget" - ], + "enumItems": ["standard", "buyget"], "mappedType": "enum" }, "created_at": { @@ -84,18 +79,14 @@ "schema": "public", "indexes": [ { - "columnNames": [ - "code" - ], + "columnNames": ["code"], "composite": false, "keyName": "IDX_promotion_code", "primary": false, "unique": false }, { - "columnNames": [ - "type" - ], + "columnNames": ["type"], "composite": false, "keyName": "IDX_promotion_type", "primary": false, @@ -103,18 +94,14 @@ }, { "keyName": "IDX_promotion_code_unique", - "columnNames": [ - "code" - ], + "columnNames": ["code"], "composite": false, "primary": false, "unique": true }, { "keyName": "promotion_pkey", - "columnNames": [ - "id" - ], + "columnNames": ["id"], "composite": false, "primary": true, "unique": true @@ -159,10 +146,7 @@ "autoincrement": false, "primary": false, "nullable": false, - "enumItems": [ - "fixed", - "percentage" - ], + "enumItems": ["fixed", "percentage"], "mappedType": "enum" }, "target_type": { @@ -172,11 +156,7 @@ "autoincrement": false, "primary": false, "nullable": false, - "enumItems": [ - "order", - "shipping", - "item" - ], + "enumItems": ["order", "shipping", "item"], "mappedType": "enum" }, "allocation": { @@ -186,10 +166,7 @@ "autoincrement": false, "primary": false, "nullable": true, - "enumItems": [ - "each", - "across" - ], + "enumItems": ["each", "across"], "mappedType": "enum" }, "promotion_id": { @@ -238,36 +215,28 @@ "schema": "public", "indexes": [ { - "columnNames": [ - "type" - ], + "columnNames": ["type"], "composite": false, "keyName": "IDX_application_method_type", "primary": false, "unique": false }, { - "columnNames": [ - "target_type" - ], + "columnNames": ["target_type"], "composite": false, "keyName": "IDX_application_method_target_type", "primary": false, "unique": false }, { - "columnNames": [ - "allocation" - ], + "columnNames": ["allocation"], "composite": false, "keyName": "IDX_application_method_allocation", "primary": false, "unique": false }, { - "columnNames": [ - "promotion_id" - ], + "columnNames": ["promotion_id"], "composite": false, "keyName": "application_method_promotion_id_unique", "primary": false, @@ -275,9 +244,7 @@ }, { "keyName": "application_method_pkey", - "columnNames": [ - "id" - ], + "columnNames": ["id"], "composite": false, "primary": true, "unique": true @@ -287,17 +254,282 @@ "foreignKeys": { "application_method_promotion_id_foreign": { "constraintName": "application_method_promotion_id_foreign", - "columnNames": [ - "promotion_id" - ], + "columnNames": ["promotion_id"], "localTableName": "public.application_method", - "referencedColumnNames": [ - "id" - ], + "referencedColumnNames": ["id"], "referencedTableName": "public.promotion", "updateRule": "cascade" } } + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "description": { + "name": "description", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "attribute": { + "name": "attribute", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "operator": { + "name": "operator", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "enumItems": ["gte", "lte", "gt", "lt", "eq", "ne", "in"], + "mappedType": "enum" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "promotion_rule", + "schema": "public", + "indexes": [ + { + "columnNames": ["attribute"], + "composite": false, + "keyName": "IDX_promotion_rule_attribute", + "primary": false, + "unique": false + }, + { + "columnNames": ["operator"], + "composite": false, + "keyName": "IDX_promotion_rule_operator", + "primary": false, + "unique": false + }, + { + "keyName": "promotion_rule_pkey", + "columnNames": ["id"], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {} + }, + { + "columns": { + "promotion_id": { + "name": "promotion_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "promotion_rule_id": { + "name": "promotion_rule_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + } + }, + "name": "promotion_promotion_rule", + "schema": "public", + "indexes": [ + { + "keyName": "promotion_promotion_rule_pkey", + "columnNames": ["promotion_id", "promotion_rule_id"], + "composite": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "promotion_promotion_rule_promotion_id_foreign": { + "constraintName": "promotion_promotion_rule_promotion_id_foreign", + "columnNames": ["promotion_id"], + "localTableName": "public.promotion_promotion_rule", + "referencedColumnNames": ["id"], + "referencedTableName": "public.promotion", + "deleteRule": "cascade", + "updateRule": "cascade" + }, + "promotion_promotion_rule_promotion_rule_id_foreign": { + "constraintName": "promotion_promotion_rule_promotion_rule_id_foreign", + "columnNames": ["promotion_rule_id"], + "localTableName": "public.promotion_promotion_rule", + "referencedColumnNames": ["id"], + "referencedTableName": "public.promotion_rule", + "deleteRule": "cascade", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "application_method_id": { + "name": "application_method_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "promotion_rule_id": { + "name": "promotion_rule_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + } + }, + "name": "application_method_promotion_rule", + "schema": "public", + "indexes": [ + { + "keyName": "application_method_promotion_rule_pkey", + "columnNames": ["application_method_id", "promotion_rule_id"], + "composite": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "application_method_promotion_rule_application_method_id_foreign": { + "constraintName": "application_method_promotion_rule_application_method_id_foreign", + "columnNames": ["application_method_id"], + "localTableName": "public.application_method_promotion_rule", + "referencedColumnNames": ["id"], + "referencedTableName": "public.application_method", + "deleteRule": "cascade", + "updateRule": "cascade" + }, + "application_method_promotion_rule_promotion_rule_id_foreign": { + "constraintName": "application_method_promotion_rule_promotion_rule_id_foreign", + "columnNames": ["promotion_rule_id"], + "localTableName": "public.application_method_promotion_rule", + "referencedColumnNames": ["id"], + "referencedTableName": "public.promotion_rule", + "deleteRule": "cascade", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "promotion_rule_id": { + "name": "promotion_rule_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "value": { + "name": "value", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + } + }, + "name": "promotion_rule_value", + "schema": "public", + "indexes": [ + { + "columnNames": ["promotion_rule_id"], + "composite": false, + "keyName": "IDX_promotion_rule_promotion_rule_value_id", + "primary": false, + "unique": false + }, + { + "keyName": "promotion_rule_value_pkey", + "columnNames": ["id"], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "promotion_rule_value_promotion_rule_id_foreign": { + "constraintName": "promotion_rule_value_promotion_rule_id_foreign", + "columnNames": ["promotion_rule_id"], + "localTableName": "public.promotion_rule_value", + "referencedColumnNames": ["id"], + "referencedTableName": "public.promotion_rule", + "deleteRule": "cascade", + "updateRule": "cascade" + } + } } ] } diff --git a/packages/promotion/src/migrations/Migration20231221104256.ts b/packages/promotion/src/migrations/Migration20231221104256.ts deleted file mode 100644 index edd403da5669d..0000000000000 --- a/packages/promotion/src/migrations/Migration20231221104256.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Migration } from "@mikro-orm/migrations" - -export class Migration20231221104256 extends Migration { - async up(): Promise { - this.addSql( - 'create table "promotion" ("id" text not null, "code" text not null, "is_automatic" boolean not null default false, "type" text check ("type" in (\'standard\', \'buyget\')) not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "promotion_pkey" primary key ("id"));' - ) - this.addSql('create index "IDX_promotion_code" on "promotion" ("code");') - this.addSql('create index "IDX_promotion_type" on "promotion" ("type");') - this.addSql( - 'alter table "promotion" add constraint "IDX_promotion_code_unique" unique ("code");' - ) - - this.addSql( - 'create table "application_method" ("id" text not null, "value" numeric null, "max_quantity" numeric null, "type" text check ("type" in (\'fixed\', \'percentage\')) not null, "target_type" text check ("target_type" in (\'order\', \'shipping\', \'item\')) not null, "allocation" text check ("allocation" in (\'each\', \'across\')) null, "promotion_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "application_method_pkey" primary key ("id"));' - ) - this.addSql( - 'create index "IDX_application_method_type" on "application_method" ("type");' - ) - this.addSql( - 'create index "IDX_application_method_target_type" on "application_method" ("target_type");' - ) - this.addSql( - 'create index "IDX_application_method_allocation" on "application_method" ("allocation");' - ) - this.addSql( - 'alter table "application_method" add constraint "application_method_promotion_id_unique" unique ("promotion_id");' - ) - - this.addSql( - 'alter table "application_method" add constraint "application_method_promotion_id_foreign" foreign key ("promotion_id") references "promotion" ("id") on update cascade;' - ) - } -} diff --git a/packages/promotion/src/migrations/Migration20240102130345.ts b/packages/promotion/src/migrations/Migration20240102130345.ts new file mode 100644 index 0000000000000..878399a637c5e --- /dev/null +++ b/packages/promotion/src/migrations/Migration20240102130345.ts @@ -0,0 +1,77 @@ +import { Migration } from "@mikro-orm/migrations" + +export class Migration20240102130345 extends Migration { + async up(): Promise { + this.addSql( + 'create table "promotion" ("id" text not null, "code" text not null, "is_automatic" boolean not null default false, "type" text check ("type" in (\'standard\', \'buyget\')) not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "promotion_pkey" primary key ("id"));' + ) + this.addSql('create index "IDX_promotion_code" on "promotion" ("code");') + this.addSql('create index "IDX_promotion_type" on "promotion" ("type");') + this.addSql( + 'alter table "promotion" add constraint "IDX_promotion_code_unique" unique ("code");' + ) + + this.addSql( + 'create table "application_method" ("id" text not null, "value" numeric null, "max_quantity" numeric null, "type" text check ("type" in (\'fixed\', \'percentage\')) not null, "target_type" text check ("target_type" in (\'order\', \'shipping\', \'item\')) not null, "allocation" text check ("allocation" in (\'each\', \'across\')) null, "promotion_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "application_method_pkey" primary key ("id"));' + ) + this.addSql( + 'create index "IDX_application_method_type" on "application_method" ("type");' + ) + this.addSql( + 'create index "IDX_application_method_target_type" on "application_method" ("target_type");' + ) + this.addSql( + 'create index "IDX_application_method_allocation" on "application_method" ("allocation");' + ) + this.addSql( + 'alter table "application_method" add constraint "application_method_promotion_id_unique" unique ("promotion_id");' + ) + + this.addSql( + 'create table "promotion_rule" ("id" text not null, "description" text null, "attribute" text not null, "operator" text check ("operator" in (\'gte\', \'lte\', \'gt\', \'lt\', \'eq\', \'ne\', \'in\')) not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "promotion_rule_pkey" primary key ("id"));' + ) + this.addSql( + 'create index "IDX_promotion_rule_attribute" on "promotion_rule" ("attribute");' + ) + this.addSql( + 'create index "IDX_promotion_rule_operator" on "promotion_rule" ("operator");' + ) + + this.addSql( + 'create table "promotion_promotion_rule" ("promotion_id" text not null, "promotion_rule_id" text not null, constraint "promotion_promotion_rule_pkey" primary key ("promotion_id", "promotion_rule_id"));' + ) + + this.addSql( + 'create table "application_method_promotion_rule" ("application_method_id" text not null, "promotion_rule_id" text not null, constraint "application_method_promotion_rule_pkey" primary key ("application_method_id", "promotion_rule_id"));' + ) + + this.addSql( + 'create table "promotion_rule_value" ("id" text not null, "promotion_rule_id" text not null, "value" text not null, constraint "promotion_rule_value_pkey" primary key ("id"));' + ) + this.addSql( + 'create index "IDX_promotion_rule_promotion_rule_value_id" on "promotion_rule_value" ("promotion_rule_id");' + ) + + this.addSql( + 'alter table "application_method" add constraint "application_method_promotion_id_foreign" foreign key ("promotion_id") references "promotion" ("id") on update cascade;' + ) + + this.addSql( + 'alter table "promotion_promotion_rule" add constraint "promotion_promotion_rule_promotion_id_foreign" foreign key ("promotion_id") references "promotion" ("id") on update cascade on delete cascade;' + ) + this.addSql( + 'alter table "promotion_promotion_rule" add constraint "promotion_promotion_rule_promotion_rule_id_foreign" foreign key ("promotion_rule_id") references "promotion_rule" ("id") on update cascade on delete cascade;' + ) + + this.addSql( + 'alter table "application_method_promotion_rule" add constraint "application_method_promotion_rule_application_method_id_foreign" foreign key ("application_method_id") references "application_method" ("id") on update cascade on delete cascade;' + ) + this.addSql( + 'alter table "application_method_promotion_rule" add constraint "application_method_promotion_rule_promotion_rule_id_foreign" foreign key ("promotion_rule_id") references "promotion_rule" ("id") on update cascade on delete cascade;' + ) + + this.addSql( + 'alter table "promotion_rule_value" add constraint "promotion_rule_value_promotion_rule_id_foreign" foreign key ("promotion_rule_id") references "promotion_rule" ("id") on update cascade on delete cascade;' + ) + } +} diff --git a/packages/promotion/src/models/application-method.ts b/packages/promotion/src/models/application-method.ts index 712c661fc2ec8..24a0dc4b098c2 100644 --- a/packages/promotion/src/models/application-method.ts +++ b/packages/promotion/src/models/application-method.ts @@ -6,9 +6,11 @@ import { import { PromotionUtils, generateEntityId } from "@medusajs/utils" import { BeforeCreate, + Collection, Entity, Enum, Index, + ManyToMany, OnInit, OneToOne, OptionalProps, @@ -16,8 +18,15 @@ import { Property, } from "@mikro-orm/core" import Promotion from "./promotion" +import PromotionRule from "./promotion-rule" -type OptionalFields = "value" | "max_quantity" | "allocation" +type OptionalFields = + | "value" + | "max_quantity" + | "allocation" + | "created_at" + | "updated_at" + | "deleted_at" @Entity() export default class ApplicationMethod { [OptionalProps]?: OptionalFields @@ -26,10 +35,10 @@ export default class ApplicationMethod { id!: string @Property({ columnType: "numeric", nullable: true, serializer: Number }) - value?: number + value?: number | null @Property({ columnType: "numeric", nullable: true, serializer: Number }) - max_quantity?: number + max_quantity?: number | null @Index({ name: "IDX_application_method_type" }) @Enum(() => PromotionUtils.ApplicationMethodType) @@ -51,12 +60,19 @@ export default class ApplicationMethod { }) promotion: Promotion + @ManyToMany(() => PromotionRule, "application_methods", { + owner: true, + pivotTable: "application_method_promotion_rule", + cascade: ["soft-remove"] as any, + }) + target_rules = new Collection(this) + @Property({ onCreate: () => new Date(), columnType: "timestamptz", defaultRaw: "now()", }) - created_at?: Date + created_at: Date @Property({ onCreate: () => new Date(), @@ -64,18 +80,18 @@ export default class ApplicationMethod { columnType: "timestamptz", defaultRaw: "now()", }) - updated_at?: Date + updated_at: Date @Property({ columnType: "timestamptz", nullable: true }) - deleted_at?: Date + deleted_at: Date | null @BeforeCreate() onCreate() { - this.id = generateEntityId(this.id, "app_method") + this.id = generateEntityId(this.id, "proappmet") } @OnInit() onInit() { - this.id = generateEntityId(this.id, "promo") + this.id = generateEntityId(this.id, "proappmet") } } diff --git a/packages/promotion/src/models/index.ts b/packages/promotion/src/models/index.ts index 0ee3108f94207..1f6f773839eaf 100644 --- a/packages/promotion/src/models/index.ts +++ b/packages/promotion/src/models/index.ts @@ -1,2 +1,4 @@ export { default as ApplicationMethod } from "./application-method" export { default as Promotion } from "./promotion" +export { default as PromotionRule } from "./promotion-rule" +export { default as PromotionRuleValue } from "./promotion-rule-value" diff --git a/packages/promotion/src/models/promotion-rule-value.ts b/packages/promotion/src/models/promotion-rule-value.ts new file mode 100644 index 0000000000000..64c9cb6eab8a7 --- /dev/null +++ b/packages/promotion/src/models/promotion-rule-value.ts @@ -0,0 +1,37 @@ +import { + BeforeCreate, + Entity, + ManyToOne, + OnInit, + PrimaryKey, + Property, +} from "@mikro-orm/core" + +import { generateEntityId } from "@medusajs/utils" +import PromotionRule from "./promotion-rule" + +@Entity() +export default class PromotionRuleValue { + @PrimaryKey({ columnType: "text" }) + id!: string + + @ManyToOne(() => PromotionRule, { + onDelete: "cascade", + fieldName: "promotion_rule_id", + index: "IDX_promotion_rule_promotion_rule_value_id", + }) + promotion_rule: PromotionRule + + @Property({ columnType: "text" }) + value: string + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "prorulval") + } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "prorulval") + } +} diff --git a/packages/promotion/src/models/promotion-rule.ts b/packages/promotion/src/models/promotion-rule.ts new file mode 100644 index 0000000000000..dfe7311cd2b89 --- /dev/null +++ b/packages/promotion/src/models/promotion-rule.ts @@ -0,0 +1,83 @@ +import { PromotionRuleOperatorValues } from "@medusajs/types" +import { PromotionUtils, generateEntityId } from "@medusajs/utils" +import { + BeforeCreate, + Cascade, + Collection, + Entity, + Enum, + Index, + ManyToMany, + OnInit, + OneToMany, + OptionalProps, + PrimaryKey, + Property, +} from "@mikro-orm/core" +import ApplicationMethod from "./application-method" +import Promotion from "./promotion" +import PromotionRuleValue from "./promotion-rule-value" + +type OptionalFields = "description" | "created_at" | "updated_at" | "deleted_at" +type OptionalRelations = "values" | "promotions" + +@Entity() +export default class PromotionRule { + [OptionalProps]?: OptionalFields | OptionalRelations + + @PrimaryKey({ columnType: "text" }) + id!: string + + @Property({ columnType: "text", nullable: true }) + description: string | null + + @Index({ name: "IDX_promotion_rule_attribute" }) + @Property({ columnType: "text" }) + attribute: string + + @Index({ name: "IDX_promotion_rule_operator" }) + @Enum(() => PromotionUtils.PromotionRuleOperator) + operator: PromotionRuleOperatorValues + + @OneToMany(() => PromotionRuleValue, (prv) => prv.promotion_rule, { + cascade: [Cascade.REMOVE], + }) + values = new Collection(this) + + @ManyToMany(() => Promotion, (promotion) => promotion.rules) + promotions = new Collection(this) + + @ManyToMany( + () => ApplicationMethod, + (applicationMethod) => applicationMethod.target_rules + ) + application_methods = new Collection(this) + + @Property({ + onCreate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date + + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date | null + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "prorul") + } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "prorul") + } +} diff --git a/packages/promotion/src/models/promotion.ts b/packages/promotion/src/models/promotion.ts index 1b395f5fe2757..6e585684806d4 100644 --- a/packages/promotion/src/models/promotion.ts +++ b/packages/promotion/src/models/promotion.ts @@ -2,9 +2,11 @@ import { PromotionType } from "@medusajs/types" import { PromotionUtils, generateEntityId } from "@medusajs/utils" import { BeforeCreate, + Collection, Entity, Enum, Index, + ManyToMany, OnInit, OneToOne, OptionalProps, @@ -13,8 +15,13 @@ import { Unique, } from "@mikro-orm/core" import ApplicationMethod from "./application-method" +import PromotionRule from "./promotion-rule" -type OptionalFields = "is_automatic" +type OptionalFields = + | "is_automatic" + | "created_at" + | "updated_at" + | "deleted_at" type OptionalRelations = "application_method" @Entity() export default class Promotion { @@ -44,12 +51,19 @@ export default class Promotion { }) application_method: ApplicationMethod + @ManyToMany(() => PromotionRule, "promotions", { + owner: true, + pivotTable: "promotion_promotion_rule", + cascade: ["soft-remove"] as any, + }) + rules = new Collection(this) + @Property({ onCreate: () => new Date(), columnType: "timestamptz", defaultRaw: "now()", }) - created_at?: Date + created_at: Date @Property({ onCreate: () => new Date(), @@ -57,10 +71,10 @@ export default class Promotion { columnType: "timestamptz", defaultRaw: "now()", }) - updated_at?: Date + updated_at: Date @Property({ columnType: "timestamptz", nullable: true }) - deleted_at?: Date + deleted_at: Date | null @BeforeCreate() onCreate() { diff --git a/packages/promotion/src/repositories/index.ts b/packages/promotion/src/repositories/index.ts index 5b9bb3a67f993..421433c704667 100644 --- a/packages/promotion/src/repositories/index.ts +++ b/packages/promotion/src/repositories/index.ts @@ -1,3 +1,5 @@ export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils" export { ApplicationMethodRepository } from "./application-method" export { PromotionRepository } from "./promotion" +export { PromotionRuleRepository } from "./promotion-rule" +export { PromotionRuleValueRepository } from "./promotion-rule-value" diff --git a/packages/promotion/src/repositories/promotion-rule-value.ts b/packages/promotion/src/repositories/promotion-rule-value.ts new file mode 100644 index 0000000000000..beb5e9ef92c03 --- /dev/null +++ b/packages/promotion/src/repositories/promotion-rule-value.ts @@ -0,0 +1,128 @@ +import { Context, DAL } from "@medusajs/types" +import { DALUtils, MedusaError, arrayDifference } from "@medusajs/utils" +import { + FilterQuery as MikroFilterQuery, + FindOptions as MikroOptions, +} from "@mikro-orm/core" + +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { PromotionRuleValue } from "@models" +import { + CreatePromotionRuleValueDTO, + UpdatePromotionRuleValueDTO, +} from "@types" + +export class PromotionRuleValueRepository extends DALUtils.MikroOrmBaseRepository { + protected readonly manager_: SqlEntityManager + + constructor({ manager }: { manager: SqlEntityManager }) { + // @ts-ignore + // eslint-disable-next-line prefer-rest-params + super(...arguments) + this.manager_ = manager + } + + async find( + findOptions: DAL.FindOptions = { where: {} }, + context: Context = {} + ): Promise { + const manager = this.getActiveManager(context) + + const findOptions_ = { ...findOptions } + findOptions_.options ??= {} + + return await manager.find( + PromotionRuleValue, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + ) + } + + async findAndCount( + findOptions: DAL.FindOptions = { where: {} }, + context: Context = {} + ): Promise<[PromotionRuleValue[], number]> { + const manager = this.getActiveManager(context) + + const findOptions_ = { ...findOptions } + findOptions_.options ??= {} + + return await manager.findAndCount( + PromotionRuleValue, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + ) + } + + async delete(ids: string[], context: Context = {}): Promise { + const manager = this.getActiveManager(context) + await manager.nativeDelete(PromotionRuleValue, { id: { $in: ids } }, {}) + } + + async create( + data: CreatePromotionRuleValueDTO[], + context: Context = {} + ): Promise { + const manager = this.getActiveManager(context) + + const promotionRuleValue = data.map((promotionRuleValue) => { + return manager.create(PromotionRuleValue, promotionRuleValue) + }) + + manager.persist(promotionRuleValue) + + return promotionRuleValue + } + + async update( + data: UpdatePromotionRuleValueDTO[], + context: Context = {} + ): Promise { + const manager = this.getActiveManager(context) + const promotionRuleValueIds = data.map( + (promotionRuleValue) => promotionRuleValue.id + ) + const existingPromotionRuleValues = await this.find( + { + where: { + id: { + $in: promotionRuleValueIds, + }, + }, + }, + context + ) + + const dataAndExistingIdDifference = arrayDifference( + data.map((d) => d.id), + existingPromotionRuleValues.map((plr) => plr.id) + ) + + if (dataAndExistingIdDifference.length) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `PromotionRuleValue with id(s) "${dataAndExistingIdDifference.join( + ", " + )}" not found` + ) + } + + const existingPromotionRuleValueMap = new Map( + existingPromotionRuleValues.map<[string, PromotionRuleValue]>( + (promotionRuleValue) => [promotionRuleValue.id, promotionRuleValue] + ) + ) + + const promotionRuleValue = data.map((promotionRuleValueData) => { + const existingPromotionRuleValue = existingPromotionRuleValueMap.get( + promotionRuleValueData.id + )! + + return manager.assign(existingPromotionRuleValue, promotionRuleValueData) + }) + + manager.persist(promotionRuleValue) + + return promotionRuleValue + } +} diff --git a/packages/promotion/src/repositories/promotion-rule.ts b/packages/promotion/src/repositories/promotion-rule.ts new file mode 100644 index 0000000000000..784c6d0a23347 --- /dev/null +++ b/packages/promotion/src/repositories/promotion-rule.ts @@ -0,0 +1,124 @@ +import { Context, DAL } from "@medusajs/types" +import { DALUtils, MedusaError, arrayDifference } from "@medusajs/utils" +import { + FilterQuery as MikroFilterQuery, + FindOptions as MikroOptions, +} from "@mikro-orm/core" + +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { PromotionRule } from "@models" +import { CreatePromotionRuleDTO, UpdatePromotionRuleDTO } from "@types" + +export class PromotionRuleRepository extends DALUtils.MikroOrmBaseRepository { + protected readonly manager_: SqlEntityManager + + constructor({ manager }: { manager: SqlEntityManager }) { + // @ts-ignore + // eslint-disable-next-line prefer-rest-params + super(...arguments) + this.manager_ = manager + } + + async find( + findOptions: DAL.FindOptions = { where: {} }, + context: Context = {} + ): Promise { + const manager = this.getActiveManager(context) + + const findOptions_ = { ...findOptions } + findOptions_.options ??= {} + + return await manager.find( + PromotionRule, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + ) + } + + async findAndCount( + findOptions: DAL.FindOptions = { where: {} }, + context: Context = {} + ): Promise<[PromotionRule[], number]> { + const manager = this.getActiveManager(context) + + const findOptions_ = { ...findOptions } + findOptions_.options ??= {} + + return await manager.findAndCount( + PromotionRule, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + ) + } + + async delete(ids: string[], context: Context = {}): Promise { + const manager = this.getActiveManager(context) + await manager.nativeDelete(PromotionRule, { id: { $in: ids } }, {}) + } + + async create( + data: CreatePromotionRuleDTO[], + context: Context = {} + ): Promise { + const manager = this.getActiveManager(context) + + const promotionRule = data.map((promotionRule) => { + return manager.create(PromotionRule, promotionRule) + }) + + manager.persist(promotionRule) + + return promotionRule + } + + async update( + data: UpdatePromotionRuleDTO[], + context: Context = {} + ): Promise { + const manager = this.getActiveManager(context) + const promotionRuleIds = data.map((promotionRule) => promotionRule.id) + const existingPromotionRules = await this.find( + { + where: { + id: { + $in: promotionRuleIds, + }, + }, + }, + context + ) + + const dataAndExistingIdDifference = arrayDifference( + data.map((d) => d.id), + existingPromotionRules.map((plr) => plr.id) + ) + + if (dataAndExistingIdDifference.length) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `PromotionRule with id(s) "${dataAndExistingIdDifference.join( + ", " + )}" not found` + ) + } + + const existingPromotionRuleMap = new Map( + existingPromotionRules.map<[string, PromotionRule]>((promotionRule) => [ + promotionRule.id, + promotionRule, + ]) + ) + + const promotionRule = data.map((promotionRuleData) => { + const existingPromotionRule = existingPromotionRuleMap.get( + promotionRuleData.id + )! + + return manager.assign(existingPromotionRule, promotionRuleData) + }) + + manager.persist(promotionRule) + + return promotionRule + } +} diff --git a/packages/promotion/src/services/index.ts b/packages/promotion/src/services/index.ts index 891d81673c3c7..e18681c6a4757 100644 --- a/packages/promotion/src/services/index.ts +++ b/packages/promotion/src/services/index.ts @@ -1,3 +1,5 @@ export { default as ApplicationMethodService } from "./application-method" export { default as PromotionService } from "./promotion" export { default as PromotionModuleService } from "./promotion-module" +export { default as PromotionRuleService } from "./promotion-rule" +export { default as PromotionRuleValueService } from "./promotion-rule-value" diff --git a/packages/promotion/src/services/promotion-module.ts b/packages/promotion/src/services/promotion-module.ts index aa6dadd764d7b..dcf4b548182d4 100644 --- a/packages/promotion/src/services/promotion-module.ts +++ b/packages/promotion/src/services/promotion-module.ts @@ -11,16 +11,26 @@ import { InjectTransactionManager, MedusaContext, } from "@medusajs/utils" -import { Promotion } from "@models" -import { ApplicationMethodService, PromotionService } from "@services" +import { ApplicationMethod, Promotion } from "@models" +import { + ApplicationMethodService, + PromotionRuleService, + PromotionRuleValueService, + PromotionService, +} from "@services" import { joinerConfig } from "../joiner-config" import { CreateApplicationMethodDTO, CreatePromotionDTO } from "../types" -import { validateApplicationMethodAttributes } from "../utils" +import { + validateApplicationMethodAttributes, + validatePromotionRuleAttributes, +} from "../utils" type InjectedDependencies = { baseRepository: DAL.RepositoryService promotionService: PromotionService applicationMethodService: ApplicationMethodService + promotionRuleService: PromotionRuleService + promotionRuleValueService: PromotionRuleValueService } export default class PromotionModuleService< @@ -30,18 +40,24 @@ export default class PromotionModuleService< protected baseRepository_: DAL.RepositoryService protected promotionService_: PromotionService protected applicationMethodService_: ApplicationMethodService + protected promotionRuleService_: PromotionRuleService + protected promotionRuleValueService_: PromotionRuleValueService constructor( { baseRepository, promotionService, applicationMethodService, + promotionRuleService, + promotionRuleValueService, }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { this.baseRepository_ = baseRepository this.promotionService_ = promotionService this.applicationMethodService_ = applicationMethodService + this.promotionRuleService_ = promotionRuleService + this.promotionRuleValueService_ = promotionRuleValueService } __joinerConfig(): ModuleJoinerConfig { @@ -78,7 +94,7 @@ export default class PromotionModuleService< return await this.list( { id: promotions.map((p) => p!.id) }, { - relations: ["application_method"], + relations: ["application_method", "rules", "rules.values"], }, sharedContext ) @@ -89,15 +105,26 @@ export default class PromotionModuleService< data: PromotionTypes.CreatePromotionDTO[], @MedusaContext() sharedContext: Context = {} ) { + const promotionsData: CreatePromotionDTO[] = [] + const applicationMethodsData: CreateApplicationMethodDTO[] = [] + const promotionCodeApplicationMethodDataMap = new Map< string, PromotionTypes.CreateApplicationMethodDTO >() - const promotionsData: CreatePromotionDTO[] = [] - const applicationMethodsData: CreateApplicationMethodDTO[] = [] + + const promotionCodeRulesDataMap = new Map< + string, + PromotionTypes.CreatePromotionRuleDTO[] + >() + const applicationMethodRuleMap = new Map< + string, + PromotionTypes.CreatePromotionRuleDTO[] + >() for (const { application_method: applicationMethodData, + rules: rulesData, ...promotionData } of data) { if (applicationMethodData) { @@ -107,33 +134,94 @@ export default class PromotionModuleService< ) } + if (rulesData) { + promotionCodeRulesDataMap.set(promotionData.code, rulesData) + } + promotionsData.push(promotionData) } const createdPromotions = await this.promotionService_.create( - data, + promotionsData, sharedContext ) for (const promotion of createdPromotions) { - const data = promotionCodeApplicationMethodDataMap.get(promotion.code) - - if (!data) continue - - const applicationMethodData = { - ...data, - promotion, + const applMethodData = promotionCodeApplicationMethodDataMap.get( + promotion.code + ) + + if (applMethodData) { + const { + target_rules: targetRulesData = [], + ...applicationMethodWithoutRules + } = applMethodData + const applicationMethodData = { + ...applicationMethodWithoutRules, + promotion, + } + + validateApplicationMethodAttributes(applicationMethodData) + applicationMethodsData.push(applicationMethodData) + + if (targetRulesData.length) { + applicationMethodRuleMap.set(promotion.id, targetRulesData) + } } - validateApplicationMethodAttributes(applicationMethodData) - applicationMethodsData.push(applicationMethodData) + await this.createPromotionRulesAndValues( + promotionCodeRulesDataMap.get(promotion.code) || [], + "promotions", + promotion, + sharedContext + ) } - await this.applicationMethodService_.create( - applicationMethodsData, - sharedContext - ) + const createdApplicationMethods = + await this.applicationMethodService_.create( + applicationMethodsData, + sharedContext + ) + + for (const applicationMethod of createdApplicationMethods) { + await this.createPromotionRulesAndValues( + applicationMethodRuleMap.get(applicationMethod.promotion.id) || [], + "application_methods", + applicationMethod, + sharedContext + ) + } return createdPromotions } + + protected async createPromotionRulesAndValues( + rulesData: PromotionTypes.CreatePromotionRuleDTO[], + relationName: "promotions" | "application_methods", + relation: Promotion | ApplicationMethod, + sharedContext: Context + ) { + validatePromotionRuleAttributes(rulesData) + + for (const ruleData of rulesData) { + const { values, ...rest } = ruleData + const promotionRuleData = { + ...rest, + [relationName]: [relation], + } + + const [createdPromotionRule] = await this.promotionRuleService_.create( + [promotionRuleData], + sharedContext + ) + + const ruleValues = Array.isArray(values) ? values : [values] + const promotionRuleValuesData = ruleValues.map((ruleValue) => ({ + value: ruleValue, + promotion_rule: createdPromotionRule, + })) + + await this.promotionRuleValueService_.create(promotionRuleValuesData) + } + } } diff --git a/packages/promotion/src/services/promotion-rule-value.ts b/packages/promotion/src/services/promotion-rule-value.ts new file mode 100644 index 0000000000000..e54227f32b425 --- /dev/null +++ b/packages/promotion/src/services/promotion-rule-value.ts @@ -0,0 +1,108 @@ +import { Context, DAL, FindConfig, PromotionTypes } from "@medusajs/types" +import { + InjectManager, + InjectTransactionManager, + MedusaContext, + ModulesSdkUtils, + retrieveEntity, +} from "@medusajs/utils" +import { PromotionRuleValue } from "@models" +import { PromotionRuleValueRepository } from "@repositories" +import { + CreatePromotionRuleValueDTO, + UpdatePromotionRuleValueDTO, +} from "../types" + +type InjectedDependencies = { + promotionRuleValueRepository: DAL.RepositoryService +} + +export default class PromotionRuleValueService< + TEntity extends PromotionRuleValue = PromotionRuleValue +> { + protected readonly promotionRuleValueRepository_: DAL.RepositoryService + + constructor({ promotionRuleValueRepository }: InjectedDependencies) { + this.promotionRuleValueRepository_ = promotionRuleValueRepository + } + + @InjectManager("promotionRuleValueRepository_") + async retrieve( + promotionRuleValueId: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await retrieveEntity< + PromotionRuleValue, + PromotionTypes.PromotionRuleValueDTO + >({ + id: promotionRuleValueId, + entityName: PromotionRuleValue.name, + repository: this.promotionRuleValueRepository_, + config, + sharedContext, + })) as TEntity + } + + @InjectManager("promotionRuleValueRepository_") + async list( + filters: PromotionTypes.FilterablePromotionRuleValueProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const queryOptions = ModulesSdkUtils.buildQuery( + filters, + config + ) + + return (await this.promotionRuleValueRepository_.find( + queryOptions, + sharedContext + )) as TEntity[] + } + + @InjectManager("promotionRuleValueRepository_") + async listAndCount( + filters: PromotionTypes.FilterablePromotionRuleValueProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise<[TEntity[], number]> { + const queryOptions = ModulesSdkUtils.buildQuery( + filters, + config + ) + + return (await this.promotionRuleValueRepository_.findAndCount( + queryOptions, + sharedContext + )) as [TEntity[], number] + } + + @InjectTransactionManager("promotionRuleValueRepository_") + async create( + data: CreatePromotionRuleValueDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await ( + this.promotionRuleValueRepository_ as PromotionRuleValueRepository + ).create(data, sharedContext)) as TEntity[] + } + + @InjectTransactionManager("promotionRuleValueRepository_") + async update( + data: UpdatePromotionRuleValueDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await ( + this.promotionRuleValueRepository_ as PromotionRuleValueRepository + ).update(data, sharedContext)) as TEntity[] + } + + @InjectTransactionManager("promotionRuleValueRepository_") + async delete( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.promotionRuleValueRepository_.delete(ids, sharedContext) + } +} diff --git a/packages/promotion/src/services/promotion-rule.ts b/packages/promotion/src/services/promotion-rule.ts new file mode 100644 index 0000000000000..6c8fbd5c2468a --- /dev/null +++ b/packages/promotion/src/services/promotion-rule.ts @@ -0,0 +1,105 @@ +import { Context, DAL, FindConfig, PromotionTypes } from "@medusajs/types" +import { + InjectManager, + InjectTransactionManager, + MedusaContext, + ModulesSdkUtils, + retrieveEntity, +} from "@medusajs/utils" +import { PromotionRule } from "@models" +import { PromotionRuleRepository } from "@repositories" +import { CreatePromotionRuleDTO, UpdatePromotionRuleDTO } from "../types" + +type InjectedDependencies = { + promotionRuleRepository: DAL.RepositoryService +} + +export default class PromotionRuleService< + TEntity extends PromotionRule = PromotionRule +> { + protected readonly promotionRuleRepository_: DAL.RepositoryService + + constructor({ promotionRuleRepository }: InjectedDependencies) { + this.promotionRuleRepository_ = promotionRuleRepository + } + + @InjectManager("promotionRuleRepository_") + async retrieve( + promotionRuleId: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await retrieveEntity< + PromotionRule, + PromotionTypes.PromotionRuleDTO + >({ + id: promotionRuleId, + entityName: PromotionRule.name, + repository: this.promotionRuleRepository_, + config, + sharedContext, + })) as TEntity + } + + @InjectManager("promotionRuleRepository_") + async list( + filters: PromotionTypes.FilterablePromotionRuleProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const queryOptions = ModulesSdkUtils.buildQuery( + filters, + config + ) + + return (await this.promotionRuleRepository_.find( + queryOptions, + sharedContext + )) as TEntity[] + } + + @InjectManager("promotionRuleRepository_") + async listAndCount( + filters: PromotionTypes.FilterablePromotionRuleProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise<[TEntity[], number]> { + const queryOptions = ModulesSdkUtils.buildQuery( + filters, + config + ) + + return (await this.promotionRuleRepository_.findAndCount( + queryOptions, + sharedContext + )) as [TEntity[], number] + } + + @InjectTransactionManager("promotionRuleRepository_") + async create( + data: CreatePromotionRuleDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await ( + this.promotionRuleRepository_ as PromotionRuleRepository + ).create(data, sharedContext)) as TEntity[] + } + + @InjectTransactionManager("promotionRuleRepository_") + async update( + data: UpdatePromotionRuleDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await ( + this.promotionRuleRepository_ as PromotionRuleRepository + ).update(data, sharedContext)) as TEntity[] + } + + @InjectTransactionManager("promotionRuleRepository_") + async delete( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.promotionRuleRepository_.delete(ids, sharedContext) + } +} diff --git a/packages/promotion/src/types/index.ts b/packages/promotion/src/types/index.ts index c35015eca94a0..3891291eebefa 100644 --- a/packages/promotion/src/types/index.ts +++ b/packages/promotion/src/types/index.ts @@ -6,3 +6,5 @@ export type InitializeModuleInjectableDependencies = { export * from "./application-method" export * from "./promotion" +export * from "./promotion-rule" +export * from "./promotion-rule-value" diff --git a/packages/promotion/src/types/promotion-rule-value.ts b/packages/promotion/src/types/promotion-rule-value.ts new file mode 100644 index 0000000000000..0b644e88af6c6 --- /dev/null +++ b/packages/promotion/src/types/promotion-rule-value.ts @@ -0,0 +1,12 @@ +import { PromotionRuleDTO } from "@medusajs/types" + +export interface CreatePromotionRuleValueDTO { + value: any + promotion_rule: string | PromotionRuleDTO +} + +export interface UpdatePromotionRuleValueDTO { + id: string + value: any + promotion_rule: string | PromotionRuleDTO +} diff --git a/packages/promotion/src/types/promotion-rule.ts b/packages/promotion/src/types/promotion-rule.ts new file mode 100644 index 0000000000000..0a43960c8462d --- /dev/null +++ b/packages/promotion/src/types/promotion-rule.ts @@ -0,0 +1,11 @@ +import { PromotionRuleOperatorValues } from "@medusajs/types" + +export interface CreatePromotionRuleDTO { + description?: string + attribute: string + operator: PromotionRuleOperatorValues +} + +export interface UpdatePromotionRuleDTO { + id: string +} diff --git a/packages/promotion/src/utils/validations/index.ts b/packages/promotion/src/utils/validations/index.ts index d5886d02f7a6d..1853716844a73 100644 --- a/packages/promotion/src/utils/validations/index.ts +++ b/packages/promotion/src/utils/validations/index.ts @@ -1 +1,2 @@ export * from "./application-method" +export * from "./promotion-rule" diff --git a/packages/promotion/src/utils/validations/promotion-rule.ts b/packages/promotion/src/utils/validations/promotion-rule.ts new file mode 100644 index 0000000000000..9c6a5eb8439e9 --- /dev/null +++ b/packages/promotion/src/utils/validations/promotion-rule.ts @@ -0,0 +1,39 @@ +import { PromotionRuleOperatorValues } from "@medusajs/types" +import { MedusaError, PromotionRuleOperator, isPresent } from "@medusajs/utils" +import { CreatePromotionRuleDTO } from "../../types" + +export function validatePromotionRuleAttributes( + promotionRulesData: CreatePromotionRuleDTO[] +) { + const errors: string[] = [] + + for (const promotionRuleData of promotionRulesData) { + if (!isPresent(promotionRuleData.attribute)) { + errors.push("rules[].attribute is a required field") + } + + if (!isPresent(promotionRuleData.operator)) { + errors.push("rules[].operator is a required field") + } + + if (isPresent(promotionRuleData.operator)) { + const allowedOperators: PromotionRuleOperatorValues[] = Object.values( + PromotionRuleOperator + ) + + if (!allowedOperators.includes(promotionRuleData.operator)) { + errors.push( + `rules[].operator (${ + promotionRuleData.operator + }) is invalid. It should be one of ${allowedOperators.join(", ")}` + ) + } + } else { + errors.push("rules[].operator is a required field") + } + } + + if (!errors.length) return + + throw new MedusaError(MedusaError.Types.INVALID_DATA, errors.join(", ")) +} diff --git a/packages/promotion/tsconfig.json b/packages/promotion/tsconfig.json index bd71a38e32132..f829219827d57 100644 --- a/packages/promotion/tsconfig.json +++ b/packages/promotion/tsconfig.json @@ -23,7 +23,9 @@ "paths": { "@models": ["./src/models"], "@services": ["./src/services"], - "@repositories": ["./src/repositories"] + "@repositories": ["./src/repositories"], + "@types": ["./src/types"], + "@utils": ["./src/utils"] } }, "include": ["src"], diff --git a/packages/types/src/promotion/common/application-method.ts b/packages/types/src/promotion/common/application-method.ts index 9020ab339c225..a288e2fa6bfde 100644 --- a/packages/types/src/promotion/common/application-method.ts +++ b/packages/types/src/promotion/common/application-method.ts @@ -1,5 +1,6 @@ import { BaseFilterable } from "../../dal" import { PromotionDTO } from "./promotion" +import { CreatePromotionRuleDTO } from "./promotion-rule" export type ApplicationMethodType = "fixed" | "percentage" export type ApplicationMethodTargetType = "order" | "shipping" | "item" @@ -16,6 +17,7 @@ export interface CreateApplicationMethodDTO { value?: number max_quantity?: number promotion?: PromotionDTO | string + target_rules?: CreatePromotionRuleDTO[] } export interface UpdateApplicationMethodDTO { diff --git a/packages/types/src/promotion/common/index.ts b/packages/types/src/promotion/common/index.ts index 47aac754520ab..7bb35ddb07f22 100644 --- a/packages/types/src/promotion/common/index.ts +++ b/packages/types/src/promotion/common/index.ts @@ -1,2 +1,4 @@ export * from "./application-method" export * from "./promotion" +export * from "./promotion-rule" +export * from "./promotion-rule-value" diff --git a/packages/types/src/promotion/common/promotion-rule-value.ts b/packages/types/src/promotion/common/promotion-rule-value.ts new file mode 100644 index 0000000000000..ffca6450fc245 --- /dev/null +++ b/packages/types/src/promotion/common/promotion-rule-value.ts @@ -0,0 +1,20 @@ +import { BaseFilterable } from "../../dal" +import { PromotionRuleDTO } from "./promotion-rule" + +export interface PromotionRuleValueDTO { + id: string +} + +export interface CreatePromotionRuleValueDTO { + value: any + promotion_rule: PromotionRuleDTO +} + +export interface UpdatePromotionRuleValueDTO { + id: string +} + +export interface FilterablePromotionRuleValueProps + extends BaseFilterable { + id?: string[] +} diff --git a/packages/types/src/promotion/common/promotion-rule.ts b/packages/types/src/promotion/common/promotion-rule.ts new file mode 100644 index 0000000000000..4b5b5fb4bc77e --- /dev/null +++ b/packages/types/src/promotion/common/promotion-rule.ts @@ -0,0 +1,30 @@ +import { BaseFilterable } from "../../dal" + +export type PromotionRuleOperatorValues = + | "gt" + | "lt" + | "eq" + | "ne" + | "in" + | "lte" + | "gte" + +export interface PromotionRuleDTO { + id: string +} + +export interface CreatePromotionRuleDTO { + description?: string + attribute: string + operator: PromotionRuleOperatorValues + values: string[] | string +} + +export interface UpdatePromotionRuleDTO { + id: string +} + +export interface FilterablePromotionRuleProps + extends BaseFilterable { + id?: string[] +} diff --git a/packages/types/src/promotion/common/promotion.ts b/packages/types/src/promotion/common/promotion.ts index 4cb00464ca88f..e2dd6834f5a33 100644 --- a/packages/types/src/promotion/common/promotion.ts +++ b/packages/types/src/promotion/common/promotion.ts @@ -1,5 +1,6 @@ import { BaseFilterable } from "../../dal" import { CreateApplicationMethodDTO } from "./application-method" +import { CreatePromotionRuleDTO } from "./promotion-rule" export type PromotionType = "standard" | "buyget" @@ -12,6 +13,7 @@ export interface CreatePromotionDTO { type: PromotionType is_automatic?: boolean application_method?: CreateApplicationMethodDTO + rules?: CreatePromotionRuleDTO[] } export interface UpdatePromotionDTO { diff --git a/packages/utils/src/common/__tests__/is-present.spec.ts b/packages/utils/src/common/__tests__/is-present.spec.ts new file mode 100644 index 0000000000000..516ea29a7370c --- /dev/null +++ b/packages/utils/src/common/__tests__/is-present.spec.ts @@ -0,0 +1,61 @@ +import { isPresent } from "../is-present" + +describe("isPresent", function () { + it("should return true or false for different types of data", function () { + const expectations = [ + { + input: null, + output: false, + }, + { + input: undefined, + output: false, + }, + { + input: "Testing", + output: true, + }, + { + input: "", + output: false, + }, + { + input: {}, + output: false, + }, + { + input: { test: 1 }, + output: true, + }, + { + input: [], + output: false, + }, + { + input: [{ test: 1 }], + output: true, + }, + { + input: new Map([["test", "test"]]), + output: true, + }, + { + input: new Map([]), + output: false, + }, + + { + input: new Set(["test"]), + output: true, + }, + { + input: new Set([]), + output: false, + }, + ] + + expectations.forEach((expectation) => { + expect(isPresent(expectation.input)).toEqual(expectation.output) + }) + }) +}) diff --git a/packages/utils/src/common/index.ts b/packages/utils/src/common/index.ts index 2baca9b213eba..7c5def8d4fa68 100644 --- a/packages/utils/src/common/index.ts +++ b/packages/utils/src/common/index.ts @@ -8,14 +8,15 @@ export * from "./deep-equal-obj" export * from "./errors" export * from "./generate-entity-id" export * from "./get-config-file" +export * from "./get-iso-string-from-date" export * from "./group-by" export * from "./handle-postgres-database-error" export * from "./is-date" export * from "./is-defined" export * from "./is-email" export * from "./is-object" +export * from "./is-present" export * from "./is-string" -export * from "./get-iso-string-from-date" export * from "./lower-case-first" export * from "./map-object-to" export * from "./medusa-container" diff --git a/packages/utils/src/common/is-present.ts b/packages/utils/src/common/is-present.ts new file mode 100644 index 0000000000000..d5f43372b61bc --- /dev/null +++ b/packages/utils/src/common/is-present.ts @@ -0,0 +1,23 @@ +import { isDefined } from "./is-defined" +import { isObject } from "./is-object" +import { isString } from "./is-string" + +export function isPresent(value: any): boolean { + if (!isDefined(value) || value === null) { + return false + } + + if (isString(value) || Array.isArray(value)) { + return value.length > 0 + } + + if (value instanceof Map || value instanceof Set) { + return value.size > 0 + } + + if (isObject(value)) { + return Object.keys(value).length > 0 + } + + return true +} diff --git a/packages/utils/src/promotion/index.ts b/packages/utils/src/promotion/index.ts index 4dcff9e8d2a0b..3e298f2568020 100644 --- a/packages/utils/src/promotion/index.ts +++ b/packages/utils/src/promotion/index.ts @@ -18,3 +18,13 @@ export enum ApplicationMethodAllocation { EACH = "each", ACROSS = "across", } + +export enum PromotionRuleOperator { + GTE = "gte", + LTE = "lte", + GT = "gt", + LT = "lt", + EQ = "eq", + NE = "ne", + IN = "in", +} From 278b7fb203f505ce163efe38055e9f36388985ea Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Wed, 3 Jan 2024 14:06:07 +0100 Subject: [PATCH 3/8] fix(medusa): Update cart sales channel should not remove all line items (#5980) --- .changeset/rude-boats-ring.md | 5 ++ .../__tests__/store/cart/ff-sales-channels.js | 52 +++++++++++++++-- .../medusa/src/services/__tests__/cart.js | 6 +- .../src/services/__tests__/line-item.js | 58 ++++++++++--------- packages/medusa/src/services/cart.ts | 44 +++++++------- packages/medusa/src/services/line-item.ts | 51 +++++++++------- 6 files changed, 140 insertions(+), 76 deletions(-) create mode 100644 .changeset/rude-boats-ring.md diff --git a/.changeset/rude-boats-ring.md b/.changeset/rude-boats-ring.md new file mode 100644 index 0000000000000..a2330c8be3d8e --- /dev/null +++ b/.changeset/rude-boats-ring.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +fix(medusa): Update cart sales channel should not remove all line items diff --git a/integration-tests/api/__tests__/store/cart/ff-sales-channels.js b/integration-tests/api/__tests__/store/cart/ff-sales-channels.js index 0fbf62d56d7db..3ee68e6e5c956 100644 --- a/integration-tests/api/__tests__/store/cart/ff-sales-channels.js +++ b/integration-tests/api/__tests__/store/cart/ff-sales-channels.js @@ -42,6 +42,11 @@ describe("[MEDUSA_FF_SALES_CHANNELS] /store/carts", () => { describe("POST /store/carts/:id", () => { let product + let product2 + let product3 + + const salesChannelId = "sales-channel" + const defaultSalesChannelId = "default-sales-channel" beforeEach(async () => { await simpleRegionFactory(dbConnection, { @@ -55,13 +60,13 @@ describe("[MEDUSA_FF_SALES_CHANNELS] /store/carts", () => { product = await simpleProductFactory(dbConnection, { sales_channels: [ { - id: "sales-channel", + id: salesChannelId, name: "Sales channel", description: "Sales channel", is_disabled: false, }, { - id: "default-sales-channel", + id: defaultSalesChannelId, name: "Main sales channel", description: "Main sales channel", is_default: true, @@ -69,6 +74,10 @@ describe("[MEDUSA_FF_SALES_CHANNELS] /store/carts", () => { }, ], }) + + product2 = await simpleProductFactory(dbConnection) + + product3 = await simpleProductFactory(dbConnection) }) afterEach(async () => { @@ -91,7 +100,7 @@ describe("[MEDUSA_FF_SALES_CHANNELS] /store/carts", () => { quantity: 1, }, ], - sales_channel_id: "sales-channel", + sales_channel_id: salesChannelId, }) const cart = createCartRes.data.cart @@ -110,7 +119,7 @@ describe("[MEDUSA_FF_SALES_CHANNELS] /store/carts", () => { expect(createdOrder.status).toEqual(200) expect(createdOrder.data.data).toEqual( expect.objectContaining({ - sales_channel_id: "sales-channel", + sales_channel_id: salesChannelId, }) ) }) @@ -152,5 +161,40 @@ describe("[MEDUSA_FF_SALES_CHANNELS] /store/carts", () => { }) ) }) + + it("should remove the line items that does not belong to the new sales channel", async () => { + const api = useApi() + + let createCartRes = await api.post("/store/carts", { + region_id: "test-region", + items: [ + { + variant_id: product.variants[0].id, + quantity: 1, + }, + { + variant_id: product2.variants[0].id, + quantity: 1, + }, + { + variant_id: product3.variants[0].id, + quantity: 1, + }, + ], + sales_channel_id: defaultSalesChannelId, + }) + + let items = createCartRes.data.cart.items + expect(items).toHaveLength(3) + + const cartId = createCartRes.data.cart.id + createCartRes = await api.post(`/store/carts/${cartId}`, { + sales_channel_id: salesChannelId, + }) + + items = createCartRes.data.cart.items + expect(items).toHaveLength(1) + expect(items.map((i) => i.variant.id)).toEqual([product.variants[0].id]) + }) }) }) diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js index e0438788a682e..3026ae7953984 100644 --- a/packages/medusa/src/services/__tests__/cart.js +++ b/packages/medusa/src/services/__tests__/cart.js @@ -744,9 +744,9 @@ describe("CartService", () => { ) expect(lineItemService.delete).toHaveBeenCalledTimes(1) - expect(lineItemService.delete).toHaveBeenCalledWith( - IdMap.getId("itemToRemove") - ) + expect(lineItemService.delete).toHaveBeenCalledWith([ + IdMap.getId("itemToRemove"), + ]) expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledTimes(1) expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledWith({ diff --git a/packages/medusa/src/services/__tests__/line-item.js b/packages/medusa/src/services/__tests__/line-item.js index 57efa430a1d19..f2737cd03f708 100644 --- a/packages/medusa/src/services/__tests__/line-item.js +++ b/packages/medusa/src/services/__tests__/line-item.js @@ -267,21 +267,23 @@ const unknownVariantId = "unknown-variant" }) describe("delete", () => { const lineItemRepository = MockRepository({ - findOne: () => - Promise.resolve({ - id: IdMap.getId("test-line-item"), - variant_id: IdMap.getId("test-variant"), - variant: { - id: IdMap.getId("test-variant"), - title: "Test variant", + find: () => + Promise.resolve([ + { + id: IdMap.getId("test-line-item"), + variant_id: IdMap.getId("test-variant"), + variant: { + id: IdMap.getId("test-variant"), + title: "Test variant", + }, + cart_id: IdMap.getId("test-cart"), + title: "Test product", + description: "Test variant", + thumbnail: "", + unit_price: 50, + quantity: 1, }, - cart_id: IdMap.getId("test-cart"), - title: "Test product", - description: "Test variant", - thumbnail: "", - unit_price: 50, - quantity: 1, - }), + ]), }) const lineItemService = new LineItemService({ @@ -297,20 +299,22 @@ const unknownVariantId = "unknown-variant" await lineItemService.delete(IdMap.getId("test-line-item")) expect(lineItemRepository.remove).toHaveBeenCalledTimes(1) - expect(lineItemRepository.remove).toHaveBeenCalledWith({ - id: IdMap.getId("test-line-item"), - variant_id: IdMap.getId("test-variant"), - variant: { - id: IdMap.getId("test-variant"), - title: "Test variant", + expect(lineItemRepository.remove).toHaveBeenCalledWith([ + { + id: IdMap.getId("test-line-item"), + variant_id: IdMap.getId("test-variant"), + variant: { + id: IdMap.getId("test-variant"), + title: "Test variant", + }, + cart_id: IdMap.getId("test-cart"), + title: "Test product", + description: "Test variant", + thumbnail: "", + unit_price: 50, + quantity: 1, }, - cart_id: IdMap.getId("test-cart"), - title: "Test product", - description: "Test variant", - thumbnail: "", - unit_price: 50, - quantity: 1, - }) + ]) }) }) }) diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index 7fd82f14453e9..25e7bf30a1319 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -560,23 +560,29 @@ class CartService extends TransactionBaseService { /** * Removes a line item from the cart. * @param cartId - the id of the cart that we will remove from - * @param lineItemId - the line item to remove. + * @param lineItemId - the line item(s) to remove. * @return the result of the update operation */ - async removeLineItem(cartId: string, lineItemId: string): Promise { + async removeLineItem( + cartId: string, + lineItemId: string | string[] + ): Promise { return await this.atomicPhase_( async (transactionManager: EntityManager) => { const cart = await this.retrieve(cartId, { - relations: [ - "items.variant.product.profiles", - "payment_sessions", - "shipping_methods", - ], + relations: ["items.variant.product.profiles", "shipping_methods"], }) - const lineItem = cart.items.find((item) => item.id === lineItemId) - if (!lineItem) { - return cart + const lineItemIdsToRemove = new Set( + Array.isArray(lineItemId) ? lineItemId : [lineItemId] + ) + + const lineItems = cart.items.filter((item) => + lineItemIdsToRemove.has(item.id) + ) + + if (!lineItems.length) { + return } if (cart.shipping_methods?.length) { @@ -599,12 +605,11 @@ class CartService extends TransactionBaseService { await this.lineItemService_ .withTransaction(transactionManager) - .delete(lineItem.id) + .delete([...lineItemIdsToRemove]) const result = await this.retrieve(cartId, { relations: [ "items.variant.product.profiles", - "discounts", "discounts.rule", "region", ], @@ -618,8 +623,6 @@ class CartService extends TransactionBaseService { .emit(CartService.Events.UPDATED, { id: cart.id, }) - - return this.retrieve(cartId) } ) } @@ -1423,14 +1426,13 @@ class CartService extends TransactionBaseService { return !productIdsToKeep.has(item.variant.product_id) }) - if (itemsToRemove.length) { - const results = await promiseAll( - itemsToRemove.map(async (item) => { - return this.removeLineItem(cart.id, item.id) - }) - ) - cart.items = results.pop()?.items ?? [] + if (!itemsToRemove.length) { + return } + + const itemIdsToRemove = new Set(itemsToRemove.map((item) => item.id)) + await this.removeLineItem(cart.id, [...itemIdsToRemove]) + cart.items = cart.items.filter((item) => !itemIdsToRemove.has(item.id)) } /** diff --git a/packages/medusa/src/services/line-item.ts b/packages/medusa/src/services/line-item.ts index cb89ec787efaf..d70fb6d101840 100644 --- a/packages/medusa/src/services/line-item.ts +++ b/packages/medusa/src/services/line-item.ts @@ -1,28 +1,27 @@ -import {MedusaError} from "medusa-core-utils" -import {EntityManager, In} from "typeorm" -import {DeepPartial} from "typeorm/common/DeepPartial" +import { MedusaError } from "medusa-core-utils" +import { EntityManager, In } from "typeorm" +import { DeepPartial } from "typeorm/common/DeepPartial" import { FlagRouter, MedusaV2Flag, - selectorConstraintsToString + selectorConstraintsToString, } from "@medusajs/utils" -import {TransactionBaseService} from "../interfaces" -import TaxInclusivePricingFeatureFlag - from "../loaders/feature-flags/tax-inclusive-pricing" +import { TransactionBaseService } from "../interfaces" +import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing" import { LineItem, LineItemAdjustment, LineItemTaxLine, ProductVariant, } from "../models" -import {CartRepository} from "../repositories/cart" -import {LineItemRepository} from "../repositories/line-item" -import {LineItemTaxLineRepository} from "../repositories/line-item-tax-line" -import {FindConfig, Selector} from "../types/common" -import {GenerateInputData, GenerateLineItemContext} from "../types/line-item" -import {ProductVariantPricing} from "../types/pricing" -import {buildQuery, isString, setMetadata} from "../utils" +import { CartRepository } from "../repositories/cart" +import { LineItemRepository } from "../repositories/line-item" +import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line" +import { FindConfig, Selector } from "../types/common" +import { GenerateInputData, GenerateLineItemContext } from "../types/line-item" +import { ProductVariantPricing } from "../types/pricing" +import { buildQuery, isString, setMetadata } from "../utils" import { PricingService, ProductService, @@ -484,23 +483,33 @@ class LineItemService extends TransactionBaseService { ) } + async delete(ids: string[]): Promise + async delete(id: string): Promise + /** * Deletes a line item. * @param id - the id of the line item to delete * @return the result of the delete operation */ - async delete(id: string): Promise { + async delete(id: string | string[]): Promise { + const ids = Array.isArray(id) ? id : [id] + return await this.atomicPhase_( async (transactionManager: EntityManager) => { const lineItemRepository = transactionManager.withRepository( this.lineItemRepository_ ) - return await lineItemRepository - .findOne({ where: { id } }) - .then( - async (lineItem) => lineItem && lineItemRepository.remove(lineItem) - ) + const lineItems = await lineItemRepository.find({ + where: { id: In(ids) }, + }) + + if (!lineItems?.length) { + return Array.isArray(id) ? [] : void 0 + } + + const removedItems = await lineItemRepository.remove(lineItems) + return Array.isArray(id) ? removedItems : removedItems[0] } ) } @@ -511,7 +520,7 @@ class LineItemService extends TransactionBaseService { * @param id - the id of the line item to delete * @return the result of the delete operation */ - async deleteWithTaxLines(id: string): Promise { + async deleteWithTaxLines(id: string): Promise { return await this.atomicPhase_( async (transactionManager: EntityManager) => { await this.taxProviderService_ From fe007d01bd827f0e09ee545e48cef18913540c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Wed, 3 Jan 2024 14:07:54 +0100 Subject: [PATCH 4/8] feat(medusa, link-modules): sales channel <> order link (#5810) --- .changeset/swift-carpets-count.md | 8 +++ .../__snapshots__/index.js.snap | 14 +++- .../__tests__/medusa-plugin-sendgrid/index.js | 5 +- .../detach-sales-channel-from-products.ts | 7 +- .../link-modules/src/definitions/index.ts | 1 + .../src/definitions/order-sales-channel.ts | 66 ++++++++++++++++++ packages/link-modules/src/links.ts | 6 ++ .../src/joiner-configs/order-service.ts | 15 +++++ ...1701860329931-order-sales-channels-link.ts | 43 ++++++++++++ .../medusa/src/models/order-sales-channel.ts | 29 ++++++++ packages/medusa/src/models/order.ts | 67 +++++++++++++++++-- packages/medusa/src/models/sales-channel.ts | 21 +++++- .../medusa/src/services/__tests__/order.js | 2 + packages/medusa/src/services/order.ts | 27 +++++++- .../src/utils/feature-flag-decorators.ts | 2 +- 15 files changed, 293 insertions(+), 20 deletions(-) create mode 100644 .changeset/swift-carpets-count.md create mode 100644 packages/link-modules/src/definitions/order-sales-channel.ts create mode 100644 packages/medusa/src/joiner-configs/order-service.ts create mode 100644 packages/medusa/src/migrations/1701860329931-order-sales-channels-link.ts create mode 100644 packages/medusa/src/models/order-sales-channel.ts diff --git a/.changeset/swift-carpets-count.md b/.changeset/swift-carpets-count.md new file mode 100644 index 0000000000000..5df38898bb6c2 --- /dev/null +++ b/.changeset/swift-carpets-count.md @@ -0,0 +1,8 @@ +--- +"@medusajs/link-modules": patch +"@medusajs/core-flows": patch +"@medusajs/medusa": patch +"@medusajs/utils": patch +--- + +feat: sales channel <> order link diff --git a/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/__snapshots__/index.js.snap b/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/__snapshots__/index.js.snap index f4bc04256014d..30f77a12fb669 100644 --- a/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/__snapshots__/index.js.snap +++ b/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/__snapshots__/index.js.snap @@ -425,7 +425,9 @@ Object { ], "locale": null, "order": Object { + "afterLoad": [Function], "beforeInsert": [Function], + "beforeUpdate": [Function], "billing_address_id": null, "canceled_at": null, "cart_id": null, @@ -753,7 +755,9 @@ Object { exports[`medusa-plugin-sendgrid order canceled data 1`] = ` Object { + "afterLoad": [Function], "beforeInsert": [Function], + "beforeUpdate": [Function], "billing_address": null, "billing_address_id": null, "canceled_at": Any, @@ -982,7 +986,9 @@ Object { exports[`medusa-plugin-sendgrid order placed data 1`] = ` Object { + "afterLoad": [Function], "beforeInsert": [Function], + "beforeUpdate": [Function], "billing_address": null, "billing_address_id": null, "canceled_at": null, @@ -1236,7 +1242,9 @@ Object { }, "locale": null, "order": Object { + "afterLoad": [Function], "beforeInsert": [Function], + "beforeUpdate": [Function], "billing_address": null, "billing_address_id": null, "canceled_at": null, @@ -1612,7 +1620,9 @@ Object { ], "locale": null, "order": Object { + "afterLoad": [Function], "beforeInsert": [Function], + "beforeUpdate": [Function], "billing_address_id": null, "canceled_at": null, "cart_id": null, @@ -2078,7 +2088,9 @@ Object { ], "locale": null, "order": Object { + "afterLoad": [Function], "beforeInsert": [Function], + "beforeUpdate": [Function], "billing_address_id": null, "canceled_at": null, "cart_id": null, @@ -2533,4 +2545,4 @@ Object { "tracking_links": Array [], "tracking_number": "", } -`; \ No newline at end of file +`; diff --git a/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/index.js b/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/index.js index 06f628ceb739e..bc29f61351c94 100644 --- a/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/index.js +++ b/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/index.js @@ -4,10 +4,7 @@ const { startBootstrapApp, } = require("../../../environment-helpers/bootstrap-app") const { initDb, useDb } = require("../../../environment-helpers/use-db") -const { - useApi, - useExpressServer, -} = require("../../../environment-helpers/use-api") +const { useApi } = require("../../../environment-helpers/use-api") const adminSeeder = require("../../../helpers/admin-seeder") diff --git a/packages/core-flows/src/handlers/product/detach-sales-channel-from-products.ts b/packages/core-flows/src/handlers/product/detach-sales-channel-from-products.ts index 72ecdcc1aa2fa..31d029ab9ec1a 100644 --- a/packages/core-flows/src/handlers/product/detach-sales-channel-from-products.ts +++ b/packages/core-flows/src/handlers/product/detach-sales-channel-from-products.ts @@ -38,14 +38,13 @@ export async function detachSalesChannelFromProducts({ if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { const remoteLink = container.resolve("remoteLink") - const promises: Promise[] = [] for (const [ salesChannelId, productIds, ] of salesChannelIdProductIdsMap.entries()) { - productIds.forEach((id) => - promises.push( + await promiseAll( + productIds.map((id) => remoteLink.dismiss({ [Modules.PRODUCT]: { product_id: id, @@ -57,8 +56,6 @@ export async function detachSalesChannelFromProducts({ ) ) } - - return } else { await promiseAll( Array.from(salesChannelIdProductIdsMap.entries()).map( diff --git a/packages/link-modules/src/definitions/index.ts b/packages/link-modules/src/definitions/index.ts index 3d18bac1ba50c..14cf0ca3d5762 100644 --- a/packages/link-modules/src/definitions/index.ts +++ b/packages/link-modules/src/definitions/index.ts @@ -4,3 +4,4 @@ export * from "./product-variant-price-set" export * from "./product-shipping-profile" export * from "./product-sales-channel" export * from "./cart-sales-channel" +export * from "./order-sales-channel" diff --git a/packages/link-modules/src/definitions/order-sales-channel.ts b/packages/link-modules/src/definitions/order-sales-channel.ts new file mode 100644 index 0000000000000..5ddc4678b1e4e --- /dev/null +++ b/packages/link-modules/src/definitions/order-sales-channel.ts @@ -0,0 +1,66 @@ +import { ModuleJoinerConfig } from "@medusajs/types" + +import { LINKS } from "../links" + +export const OrderSalesChannel: ModuleJoinerConfig = { + serviceName: LINKS.OrderSalesChannel, + isLink: true, + databaseConfig: { + tableName: "order_sales_channel", + idPrefix: "ordersc", + }, + alias: [ + { + name: "order_sales_channel", + }, + { + name: "order_sales_channels", + }, + ], + primaryKeys: ["id", "order_id", "sales_channel_id"], + relationships: [ + { + serviceName: "orderService", + isInternalService: true, + primaryKey: "id", + foreignKey: "order_id", + alias: "order", + }, + { + serviceName: "salesChannelService", + isInternalService: true, + primaryKey: "id", + foreignKey: "sales_channel_id", + alias: "sales_channel", + }, + ], + extends: [ + { + serviceName: "orderService", + fieldAlias: { + sales_channel: "sales_channel_link.sales_channel", + }, + relationship: { + serviceName: LINKS.OrderSalesChannel, + isInternalService: true, + primaryKey: "order_id", + foreignKey: "id", + alias: "sales_channel_link", + }, + }, + { + serviceName: "salesChannelService", + fieldAlias: { + orders: "order_link.order", + }, + relationship: { + serviceName: LINKS.OrderSalesChannel, + isInternalService: true, + primaryKey: "sales_channel_id", + foreignKey: "id", + alias: "order_link", + isList: true, + }, + }, + ], +} diff --git a/packages/link-modules/src/links.ts b/packages/link-modules/src/links.ts index 117777eae4f45..c38b0ed7158b8 100644 --- a/packages/link-modules/src/links.ts +++ b/packages/link-modules/src/links.ts @@ -34,4 +34,10 @@ export const LINKS = { "salesChannelService", "sales_channel_id" ), + OrderSalesChannel: composeLinkName( + "orderService", + "order_id", + "salesChannelService", + "sales_channel_id" + ), } diff --git a/packages/medusa/src/joiner-configs/order-service.ts b/packages/medusa/src/joiner-configs/order-service.ts new file mode 100644 index 0000000000000..3a5e3dc5feb6f --- /dev/null +++ b/packages/medusa/src/joiner-configs/order-service.ts @@ -0,0 +1,15 @@ +import { ModuleJoinerConfig } from "@medusajs/types" + +export default { + serviceName: "orderService", + primaryKeys: ["id"], + linkableKeys: { order_id: "Order" }, + alias: [ + { + name: "order", + }, + { + name: "orders", + }, + ], +} as ModuleJoinerConfig diff --git a/packages/medusa/src/migrations/1701860329931-order-sales-channels-link.ts b/packages/medusa/src/migrations/1701860329931-order-sales-channels-link.ts new file mode 100644 index 0000000000000..d9477d4094a7c --- /dev/null +++ b/packages/medusa/src/migrations/1701860329931-order-sales-channels-link.ts @@ -0,0 +1,43 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { MedusaV2Flag } from "@medusajs/utils" + +import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" + +export const featureFlag = [SalesChannelFeatureFlag.key, MedusaV2Flag.key] + +export class OrderSalesChannelLink1701860329931 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "order_sales_channel" + ( + "id" character varying NOT NULL, + "order_id" character varying NOT NULL, + "sales_channel_id" character varying NOT NULL, + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + "deleted_at" TIMESTAMP WITH TIME ZONE, + CONSTRAINT "order_sales_channel_pk" PRIMARY KEY ("order_id", "sales_channel_id"), + CONSTRAINT "order_sales_channel_order_id_unique" UNIQUE ("order_id") + ); + CREATE INDEX IF NOT EXISTS "IDX_id_order_sales_channel" ON "order_sales_channel" ("id"); + + insert into "order_sales_channel" (id, order_id, sales_channel_id) + (select 'ordersc_' || substr(md5(random()::text), 0, 27), id, sales_channel_id from "order" WHERE sales_channel_id IS NOT NULL); + + ALTER TABLE "order" DROP CONSTRAINT IF EXISTS "FK_6ff7e874f01b478c115fdd462eb"; + `) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE "order" + SET "sales_channel_id" = "order_sales_channel"."sales_channel_id" + FROM "order_sales_channel" + WHERE "order"."id" = "order_sales_channel"."order_id"; + + DROP TABLE IF EXISTS "order_sales_channel"; + + ALTER TABLE "order" ADD CONSTRAINT "FK_6ff7e874f01b478c115fdd462eb" FOREIGN KEY ("sales_channel_id") REFERENCES "sales_channel"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + `) + } +} diff --git a/packages/medusa/src/models/order-sales-channel.ts b/packages/medusa/src/models/order-sales-channel.ts new file mode 100644 index 0000000000000..497f464169871 --- /dev/null +++ b/packages/medusa/src/models/order-sales-channel.ts @@ -0,0 +1,29 @@ +import { BeforeInsert, Column, Index, PrimaryColumn } from "typeorm" +import { MedusaV2Flag, SalesChannelFeatureFlag } from "@medusajs/utils" + +import { generateEntityId } from "../utils" +import { SoftDeletableEntity } from "../interfaces" +import { FeatureFlagEntity } from "../utils/feature-flag-decorators" + +@FeatureFlagEntity([MedusaV2Flag.key, SalesChannelFeatureFlag.key]) +export class OrderSalesChannel extends SoftDeletableEntity { + @Column() + id: string + + @Index("order_sales_channel_order_id_unique", { + unique: true, + }) + @PrimaryColumn() + order_id: string + + @PrimaryColumn() + sales_channel_id: string + + /** + * @apiIgnore + */ + @BeforeInsert() + private beforeInsert(): void { + this.id = generateEntityId(this.id, "ordersc") + } +} diff --git a/packages/medusa/src/models/order.ts b/packages/medusa/src/models/order.ts index 7891b53748479..c5141e4cfbb1e 100644 --- a/packages/medusa/src/models/order.ts +++ b/packages/medusa/src/models/order.ts @@ -1,5 +1,7 @@ import { + AfterLoad, BeforeInsert, + BeforeUpdate, Column, Entity, Generated, @@ -12,7 +14,10 @@ import { OneToOne, } from "typeorm" import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column" -import { FeatureFlagColumn, FeatureFlagDecorators, } from "../utils/feature-flag-decorators" +import { + FeatureFlagColumn, + FeatureFlagDecorators, +} from "../utils/feature-flag-decorators" import { BaseEntity } from "../interfaces/models/base-entity" import { generateEntityId } from "../utils/generate-entity-id" @@ -36,10 +41,11 @@ import { Return } from "./return" import { SalesChannel } from "./sales-channel" import { ShippingMethod } from "./shipping-method" import { Swap } from "./swap" +import { MedusaV2Flag } from "@medusajs/utils" /** * @enum - * + * * The order's status. */ export enum OrderStatus { @@ -48,7 +54,7 @@ export enum OrderStatus { */ PENDING = "pending", /** - * The order is completed, meaning that + * The order is completed, meaning that * the items have been fulfilled and the payment * has been captured. */ @@ -69,7 +75,7 @@ export enum OrderStatus { /** * @enum - * + * * The order's fulfillment status. */ export enum FulfillmentStatus { @@ -78,7 +84,7 @@ export enum FulfillmentStatus { */ NOT_FULFILLED = "not_fulfilled", /** - * Some of the order's items, but not all, are fulfilled. + * Some of the order's items, but not all, are fulfilled. */ PARTIALLY_FULFILLED = "partially_fulfilled", /** @@ -113,7 +119,7 @@ export enum FulfillmentStatus { /** * @enum - * + * * The order's payment status. */ export enum PaymentStatus { @@ -321,6 +327,25 @@ export class Order extends BaseEntity { ]) sales_channel: SalesChannel + @FeatureFlagDecorators( + [MedusaV2Flag.key, "sales_channels"], + [ + ManyToMany(() => SalesChannel, { cascade: ["remove", "soft-remove"] }), + JoinTable({ + name: "order_sales_channel", + joinColumn: { + name: "cart_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "sales_channel_id", + referencedColumnName: "id", + }, + }), + ] + ) + sales_channels?: SalesChannel[] + // Total fields shipping_total: number shipping_tax_total: number | null @@ -345,6 +370,12 @@ export class Order extends BaseEntity { private async beforeInsert(): Promise { this.id = generateEntityId(this.id, "order") + if (this.sales_channel_id || this.sales_channel) { + this.sales_channels = [ + { id: this.sales_channel_id || this.sales_channel?.id }, + ] as SalesChannel[] + } + if (process.env.NODE_ENV === "development" && !this.display_id) { const disId = await manualAutoIncrement("order") @@ -353,6 +384,30 @@ export class Order extends BaseEntity { } } } + + /** + * @apiIgnore + */ + @BeforeUpdate() + private beforeUpdate(): void { + if (this.sales_channel_id || this.sales_channel) { + this.sales_channels = [ + { id: this.sales_channel_id || this.sales_channel?.id }, + ] as SalesChannel[] + } + } + + /** + * @apiIgnore + */ + @AfterLoad() + private afterLoad(): void { + if (this.sales_channels) { + this.sales_channel = this.sales_channels?.[0] + this.sales_channel_id = this.sales_channel?.id + delete this.sales_channels + } + } } /** diff --git a/packages/medusa/src/models/sales-channel.ts b/packages/medusa/src/models/sales-channel.ts index b7fcea59b7713..6a71b6ad2654a 100644 --- a/packages/medusa/src/models/sales-channel.ts +++ b/packages/medusa/src/models/sales-channel.ts @@ -1,15 +1,16 @@ import { BeforeInsert, Column, JoinTable, ManyToMany, OneToMany } from "typeorm" -import { MedusaV2Flag } from "@medusajs/utils" import { FeatureFlagDecorators, FeatureFlagEntity, } from "../utils/feature-flag-decorators" +import { MedusaV2Flag } from "@medusajs/utils" import { SoftDeletableEntity } from "../interfaces" import { DbAwareColumn, generateEntityId } from "../utils" import { SalesChannelLocation } from "./sales-channel-location" import { Product } from "./product" import { Cart } from "./cart" +import { Order } from "./order" @FeatureFlagEntity("sales_channels") export class SalesChannel extends SoftDeletableEntity { @@ -55,6 +56,24 @@ export class SalesChannel extends SoftDeletableEntity { ]) carts: Cart[] + @FeatureFlagDecorators(MedusaV2Flag.key, + [ + ManyToMany(() => Order), + JoinTable({ + name: "order_sales_channel", + joinColumn: { + name: "sales_channel_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "order_id", + referencedColumnName: "id", + }, + }), + ] + ) + orders: Order[] + @OneToMany( () => SalesChannelLocation, (scLocation) => scLocation.sales_channel, diff --git a/packages/medusa/src/services/__tests__/order.js b/packages/medusa/src/services/__tests__/order.js index d51d3f2fdac8d..3e28b46b24dbc 100644 --- a/packages/medusa/src/services/__tests__/order.js +++ b/packages/medusa/src/services/__tests__/order.js @@ -1,4 +1,5 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" +import { FlagRouter } from "@medusajs/utils" import { LineItemServiceMock } from "../__mocks__/line-item" import { newTotalsServiceMock } from "../__mocks__/new-totals" import { ProductVariantInventoryServiceMock } from "../__mocks__/product-variant-inventory" @@ -151,6 +152,7 @@ describe("OrderService", () => { eventBusService, cartService, productVariantInventoryService, + featureFlagRouter: new FlagRouter({}), }) beforeEach(async () => { diff --git a/packages/medusa/src/services/order.ts b/packages/medusa/src/services/order.ts index 278ceaccaa7fd..c72337c8d89b8 100644 --- a/packages/medusa/src/services/order.ts +++ b/packages/medusa/src/services/order.ts @@ -5,6 +5,7 @@ import { FlagRouter, isDefined, MedusaError, + MedusaV2Flag, promiseAll, selectorConstraintsToString, } from "@medusajs/utils" import { @@ -64,6 +65,7 @@ import { TotalsContext, UpdateOrderInput } from "../types/orders" import { CreateShippingMethodDto } from "../types/shipping-options" import { buildQuery, isString, setMetadata } from "../utils" import EventBusService from "./event-bus" +import { RemoteLink } from "@medusajs/modules-sdk" export const ORDER_CART_ALREADY_EXISTS_ERROR = "Order from cart already exists" @@ -90,6 +92,7 @@ type InjectedDependencies = { eventBusService: EventBusService featureFlagRouter: FlagRouter productVariantInventoryService: ProductVariantInventoryService + remoteLink: RemoteLink } class OrderService extends TransactionBaseService { @@ -132,6 +135,7 @@ class OrderService extends TransactionBaseService { protected readonly inventoryService_: IInventoryService protected readonly eventBus_: EventBusService protected readonly featureFlagRouter_: FlagRouter + protected remoteLink_: RemoteLink // eslint-disable-next-line max-len protected readonly productVariantInventoryService_: ProductVariantInventoryService @@ -150,6 +154,7 @@ class OrderService extends TransactionBaseService { taxProviderService, regionService, cartService, + remoteLink, addressRepository, giftCardService, draftOrderService, @@ -180,6 +185,7 @@ class OrderService extends TransactionBaseService { this.draftOrderService_ = draftOrderService this.featureFlagRouter_ = featureFlagRouter this.productVariantInventoryService_ = productVariantInventoryService + this.remoteLink_ = remoteLink } /** @@ -693,7 +699,8 @@ class OrderService extends TransactionBaseService { if ( cart.sales_channel_id && - this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key) + this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key) && + !this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key) ) { toCreate.sales_channel_id = cart.sales_channel_id } @@ -710,6 +717,22 @@ class OrderService extends TransactionBaseService { const rawOrder = orderRepo.create(toCreate) const order = await orderRepo.save(rawOrder) + if ( + this.featureFlagRouter_.isFeatureEnabled([ + SalesChannelFeatureFlag.key, + MedusaV2Flag.key, + ]) + ) { + await this.remoteLink_.create({ + orderService: { + order_id: order.id, + }, + salesChannelService: { + sales_channel_id: cart.sales_channel_id as string, + }, + }) + } + if (total !== 0 && payment) { await this.paymentProviderService_ .withTransaction(manager) @@ -2082,7 +2105,7 @@ class OrderService extends TransactionBaseService { relationSet.add("shipping_methods.tax_lines") relationSet.add("region") relationSet.add("payments") - + return Array.from(relationSet.values()) } } diff --git a/packages/medusa/src/utils/feature-flag-decorators.ts b/packages/medusa/src/utils/feature-flag-decorators.ts index 5bf28caaa4821..472d14a2d197b 100644 --- a/packages/medusa/src/utils/feature-flag-decorators.ts +++ b/packages/medusa/src/utils/feature-flag-decorators.ts @@ -51,7 +51,7 @@ export function FeatureFlagDecorators( } export function FeatureFlagClassDecorators( - featureFlag: string, + featureFlag: string | string[], decorators: ClassDecorator[] ): ClassDecorator { return function (target) { From d2ef0cff2bed5b2b32fbc0ea274493223301fcd5 Mon Sep 17 00:00:00 2001 From: olivermrbl Date: Wed, 3 Jan 2024 15:34:30 +0100 Subject: [PATCH 5/8] chore(changesets): Use correct medusa-react package --- .changeset/hot-dingos-pay.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/hot-dingos-pay.md b/.changeset/hot-dingos-pay.md index 5fbe430c057f5..93050b95bdec6 100644 --- a/.changeset/hot-dingos-pay.md +++ b/.changeset/hot-dingos-pay.md @@ -1,5 +1,5 @@ --- -"@medusajs/medusa-react": patch +"medusa-react": patch "@medusajs/types": patch "@medusajs/modules-sdk": patch --- From 1468646b9915387c46ad3039b18f9905c8f6486d Mon Sep 17 00:00:00 2001 From: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Thu, 4 Jan 2024 08:40:45 +0100 Subject: [PATCH 6/8] chore(workflows): Use Node 16.14 in CLI workflow (#5993) * make packages private * add changeset * Update test-cli-with-database.yml * revert --- .github/workflows/test-cli-with-database.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-cli-with-database.yml b/.github/workflows/test-cli-with-database.yml index 0a2eb01b0c79d..400b89e8ae933 100644 --- a/.github/workflows/test-cli-with-database.yml +++ b/.github/workflows/test-cli-with-database.yml @@ -45,6 +45,7 @@ jobs: uses: ./.github/actions/setup-server with: cache-extension: "cli-test" + node-version: "16.14" - name: Install Medusa cli run: npm i -g @medusajs/medusa-cli From 3550750975a0c9359fd887929377733606ef03af Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Thu, 4 Jan 2024 09:02:49 +0100 Subject: [PATCH 7/8] feat(pricing, types, utils): Soft delete pricing entities (#5858) --- .changeset/fuzzy-ears-look.md | 8 + .../pricing-module/money-amount.spec.ts | 93 +++++ packages/pricing/src/joiner-config.ts | 1 + .../migrations/.snapshot-medusa-pricing.json | 366 ++++++++++++++---- .../src/migrations/Migration20240103140327.ts | 55 +++ packages/pricing/src/models/money-amount.ts | 30 +- .../src/models/price-list-rule-value.ts | 8 +- .../pricing/src/models/price-list-rule.ts | 6 + packages/pricing/src/models/price-list.ts | 28 +- packages/pricing/src/models/price-rule.ts | 38 +- .../models/price-set-money-amount-rules.ts | 10 +- .../src/models/price-set-money-amount.ts | 43 +- .../pricing/src/models/price-set-rule-type.ts | 7 +- packages/pricing/src/models/price-set.ts | 6 + packages/pricing/src/models/rule-type.ts | 10 +- .../pricing/src/repositories/money-amount.ts | 4 +- .../price-set-money-amount-rules.ts | 6 +- packages/pricing/src/scripts/seed.ts | 18 +- packages/pricing/src/services/money-amount.ts | 16 + .../src/services/price-set-money-amount.ts | 6 +- .../pricing/src/services/pricing-module.ts | 47 ++- .../src/types/services/money-amount.ts | 3 + .../pricing/src/types/services/price-rule.ts | 3 + .../types/services/price-set-money-amount.ts | 3 + .../types/src/pricing/common/money-amount.ts | 14 +- .../types/src/pricing/common/price-rule.ts | 12 + .../pricing/common/price-set-money-amount.ts | 22 +- packages/types/src/pricing/service.ts | 53 +++ .../src/dal/mikro-orm/mikro-orm-repository.ts | 2 +- packages/utils/src/dal/mikro-orm/utils.ts | 28 +- 30 files changed, 791 insertions(+), 155 deletions(-) create mode 100644 .changeset/fuzzy-ears-look.md create mode 100644 packages/pricing/src/migrations/Migration20240103140327.ts diff --git a/.changeset/fuzzy-ears-look.md b/.changeset/fuzzy-ears-look.md new file mode 100644 index 0000000000000..6d3e77cffb3e2 --- /dev/null +++ b/.changeset/fuzzy-ears-look.md @@ -0,0 +1,8 @@ +--- +"@medusajs/pricing": patch +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +feat(pricing, types): add soft deletion for money-amounts +feat(utils): cascade soft delete across 1:1 relationships \ No newline at end of file diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/money-amount.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/money-amount.spec.ts index 010dd0521980e..2b8fd005c8e50 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/money-amount.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/money-amount.spec.ts @@ -6,6 +6,11 @@ import { initialize } from "../../../../src" import { createCurrencies } from "../../../__fixtures__/currency" import { createMoneyAmounts } from "../../../__fixtures__/money-amount" import { DB_URL, MikroOrmWrapper } from "../../../utils" +import { createPriceSetMoneyAmounts } from "../../../__fixtures__/price-set-money-amount" +import { createPriceSets } from "../../../__fixtures__/price-set" +import { createRuleTypes } from "../../../__fixtures__/rule-type" +import { createPriceRules } from "../../../__fixtures__/price-rule" +import { createPriceSetMoneyAmountRules } from "../../../__fixtures__/price-set-money-amount-rules" jest.setTimeout(30000) @@ -264,6 +269,94 @@ describe("PricingModule Service - MoneyAmount", () => { }) }) + describe("softDeleteMoneyAmounts", () => { + const id = "money-amount-USD" + + it("should softDelete priceSetMoneyAmount and PriceRule when soft-deleting money amount", async () => { + await createPriceSets(testManager) + await createRuleTypes(testManager) + await createPriceSetMoneyAmounts(testManager) + await createPriceRules(testManager) + await createPriceSetMoneyAmountRules(testManager) + await service.softDeleteMoneyAmounts([id]) + + const [moneyAmount] = await service.listMoneyAmounts( + { + id: [id], + }, + { + relations: [ + "price_set_money_amount", + "price_set_money_amount.price_rules", + ], + withDeleted: true, + } + ) + + expect(moneyAmount).toBeTruthy() + + const deletedAt = moneyAmount.deleted_at + + expect(moneyAmount).toEqual( + expect.objectContaining({ + deleted_at: deletedAt, + price_set_money_amount: expect.objectContaining({ + deleted_at: deletedAt, + price_rules: [ + expect.objectContaining({ + deleted_at: deletedAt, + }), + ], + }), + }) + ) + }) + }) + + describe("restoreDeletedMoneyAmounts", () => { + const id = "money-amount-USD" + + it("should restore softDeleted priceSetMoneyAmount and PriceRule when restoring soft-deleting money amount", async () => { + await createPriceSets(testManager) + await createRuleTypes(testManager) + await createPriceSetMoneyAmounts(testManager) + await createPriceRules(testManager) + await createPriceSetMoneyAmountRules(testManager) + await service.softDeleteMoneyAmounts([id]) + await service.restoreDeletedMoneyAmounts([id]) + + const [moneyAmount] = await service.listMoneyAmounts( + { + id: [id], + }, + { + relations: [ + "price_set_money_amount", + "price_set_money_amount.price_rules", + ], + } + ) + + expect(moneyAmount).toBeTruthy() + + const deletedAt = null + + expect(moneyAmount).toEqual( + expect.objectContaining({ + deleted_at: deletedAt, + price_set_money_amount: expect.objectContaining({ + deleted_at: deletedAt, + price_rules: [ + expect.objectContaining({ + deleted_at: deletedAt, + }), + ], + }), + }) + ) + }) + }) + describe("updateMoneyAmounts", () => { const id = "money-amount-USD" diff --git a/packages/pricing/src/joiner-config.ts b/packages/pricing/src/joiner-config.ts index 7be13f7c8b88a..bc3342e90e4c2 100644 --- a/packages/pricing/src/joiner-config.ts +++ b/packages/pricing/src/joiner-config.ts @@ -16,6 +16,7 @@ export const LinkableKeys = { price_list_id: PriceList.name, price_set_money_amount_id: PriceSetMoneyAmount.name, } + const entityLinkableKeysMap: MapToConfig = {} Object.entries(LinkableKeys).forEach(([key, value]) => { entityLinkableKeysMap[value] ??= [] diff --git a/packages/pricing/src/migrations/.snapshot-medusa-pricing.json b/packages/pricing/src/migrations/.snapshot-medusa-pricing.json index 403a6b2e356ee..8cf12f0ba0a04 100644 --- a/packages/pricing/src/migrations/.snapshot-medusa-pricing.json +++ b/packages/pricing/src/migrations/.snapshot-medusa-pricing.json @@ -1,5 +1,7 @@ { - "namespaces": ["public"], + "namespaces": [ + "public" + ], "name": "public", "tables": [ { @@ -46,7 +48,9 @@ "indexes": [ { "keyName": "currency_pkey", - "columnNames": ["code"], + "columnNames": [ + "code" + ], "composite": false, "primary": true, "unique": true @@ -139,14 +143,18 @@ "schema": "public", "indexes": [ { - "columnNames": ["currency_code"], + "columnNames": [ + "currency_code" + ], "composite": false, "keyName": "IDX_money_amount_currency_code", "primary": false, "unique": false }, { - "columnNames": ["deleted_at"], + "columnNames": [ + "deleted_at" + ], "composite": false, "keyName": "IDX_money_amount_deleted_at", "primary": false, @@ -154,7 +162,9 @@ }, { "keyName": "money_amount_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -164,9 +174,13 @@ "foreignKeys": { "money_amount_currency_code_foreign": { "constraintName": "money_amount_currency_code_foreign", - "columnNames": ["currency_code"], + "columnNames": [ + "currency_code" + ], "localTableName": "public.money_amount", - "referencedColumnNames": ["code"], + "referencedColumnNames": [ + "code" + ], "referencedTableName": "public.currency", "deleteRule": "set null", "updateRule": "cascade" @@ -210,7 +224,10 @@ "primary": false, "nullable": false, "default": "'draft'", - "enumItems": ["active", "draft"], + "enumItems": [ + "active", + "draft" + ], "mappedType": "enum" }, "type": { @@ -221,7 +238,10 @@ "primary": false, "nullable": false, "default": "'sale'", - "enumItems": ["sale", "override"], + "enumItems": [ + "sale", + "override" + ], "mappedType": "enum" }, "starts_at": { @@ -291,7 +311,9 @@ "schema": "public", "indexes": [ { - "columnNames": ["deleted_at"], + "columnNames": [ + "deleted_at" + ], "composite": false, "keyName": "IDX_price_list_deleted_at", "primary": false, @@ -299,7 +321,9 @@ }, { "keyName": "price_list_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -325,7 +349,9 @@ "indexes": [ { "keyName": "price_set_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -390,42 +416,93 @@ "primary": false, "nullable": true, "mappedType": "text" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" } }, "name": "price_set_money_amount", "schema": "public", "indexes": [ { - "columnNames": ["price_set_id"], + "columnNames": [ + "price_set_id" + ], "composite": false, "keyName": "IDX_price_set_money_amount_price_set_id", "primary": false, "unique": false }, { - "columnNames": ["money_amount_id"], + "columnNames": [ + "money_amount_id" + ], "composite": false, "keyName": "IDX_price_set_money_amount_money_amount_id", "primary": false, "unique": false }, { - "columnNames": ["money_amount_id"], + "columnNames": [ + "money_amount_id" + ], "composite": false, "keyName": "price_set_money_amount_money_amount_id_unique", "primary": false, "unique": true }, { - "columnNames": ["price_list_id"], + "columnNames": [ + "price_list_id" + ], "composite": false, "keyName": "IDX_price_rule_price_list_id", "primary": false, "unique": false }, + { + "columnNames": [ + "deleted_at" + ], + "composite": false, + "keyName": "IDX_price_set_money_amount_deleted_at", + "primary": false, + "unique": false + }, { "keyName": "price_set_money_amount_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -435,30 +512,41 @@ "foreignKeys": { "price_set_money_amount_price_set_id_foreign": { "constraintName": "price_set_money_amount_price_set_id_foreign", - "columnNames": ["price_set_id"], + "columnNames": [ + "price_set_id" + ], "localTableName": "public.price_set_money_amount", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.price_set", "deleteRule": "cascade", "updateRule": "cascade" }, "price_set_money_amount_money_amount_id_foreign": { "constraintName": "price_set_money_amount_money_amount_id_foreign", - "columnNames": ["money_amount_id"], + "columnNames": [ + "money_amount_id" + ], "localTableName": "public.price_set_money_amount", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.money_amount", "deleteRule": "cascade", "updateRule": "cascade" }, "price_set_money_amount_price_list_id_foreign": { "constraintName": "price_set_money_amount_price_list_id_foreign", - "columnNames": ["price_list_id"], + "columnNames": [ + "price_list_id" + ], "localTableName": "public.price_set_money_amount", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.price_list", - "deleteRule": "cascade", - "updateRule": "cascade" + "deleteRule": "cascade" } } }, @@ -506,7 +594,9 @@ "schema": "public", "indexes": [ { - "columnNames": ["rule_attribute"], + "columnNames": [ + "rule_attribute" + ], "composite": false, "keyName": "IDX_rule_type_rule_attribute", "primary": false, @@ -514,7 +604,9 @@ }, { "keyName": "rule_type_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -557,14 +649,18 @@ "schema": "public", "indexes": [ { - "columnNames": ["price_set_id"], + "columnNames": [ + "price_set_id" + ], "composite": false, "keyName": "IDX_price_set_rule_type_price_set_id", "primary": false, "unique": false }, { - "columnNames": ["rule_type_id"], + "columnNames": [ + "rule_type_id" + ], "composite": false, "keyName": "IDX_price_set_rule_type_rule_type_id", "primary": false, @@ -572,7 +668,9 @@ }, { "keyName": "price_set_rule_type_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -582,18 +680,26 @@ "foreignKeys": { "price_set_rule_type_price_set_id_foreign": { "constraintName": "price_set_rule_type_price_set_id_foreign", - "columnNames": ["price_set_id"], + "columnNames": [ + "price_set_id" + ], "localTableName": "public.price_set_rule_type", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.price_set", "deleteRule": "cascade", "updateRule": "cascade" }, "price_set_rule_type_rule_type_id_foreign": { "constraintName": "price_set_rule_type_rule_type_id_foreign", - "columnNames": ["rule_type_id"], + "columnNames": [ + "rule_type_id" + ], "localTableName": "public.price_set_rule_type", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.rule_type", "updateRule": "cascade" } @@ -642,14 +748,18 @@ "schema": "public", "indexes": [ { - "columnNames": ["price_set_money_amount_id"], + "columnNames": [ + "price_set_money_amount_id" + ], "composite": false, "keyName": "IDX_price_set_money_amount_rules_price_set_money_amount_id", "primary": false, "unique": false }, { - "columnNames": ["rule_type_id"], + "columnNames": [ + "rule_type_id" + ], "composite": false, "keyName": "IDX_price_set_money_amount_rules_rule_type_id", "primary": false, @@ -657,7 +767,9 @@ }, { "keyName": "price_set_money_amount_rules_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -667,18 +779,26 @@ "foreignKeys": { "price_set_money_amount_rules_price_set_money_amount_id_foreign": { "constraintName": "price_set_money_amount_rules_price_set_money_amount_id_foreign", - "columnNames": ["price_set_money_amount_id"], + "columnNames": [ + "price_set_money_amount_id" + ], "localTableName": "public.price_set_money_amount_rules", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.price_set_money_amount", "deleteRule": "cascade", "updateRule": "cascade" }, "price_set_money_amount_rules_rule_type_id_foreign": { "constraintName": "price_set_money_amount_rules_rule_type_id_foreign", - "columnNames": ["rule_type_id"], + "columnNames": [ + "rule_type_id" + ], "localTableName": "public.price_set_money_amount_rules", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.rule_type", "updateRule": "cascade" } @@ -713,16 +833,6 @@ "nullable": false, "mappedType": "text" }, - "is_dynamic": { - "name": "is_dynamic", - "type": "boolean", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "default": "false", - "mappedType": "boolean" - }, "value": { "name": "value", "type": "text", @@ -750,35 +860,84 @@ "primary": false, "nullable": false, "mappedType": "text" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" } }, "name": "price_rule", "schema": "public", "indexes": [ { - "columnNames": ["price_set_id"], + "columnNames": [ + "price_set_id" + ], "composite": false, "keyName": "IDX_price_rule_price_set_id", "primary": false, "unique": false }, { - "columnNames": ["rule_type_id"], + "columnNames": [ + "rule_type_id" + ], "composite": false, "keyName": "IDX_price_rule_rule_type_id", "primary": false, "unique": false }, { - "columnNames": ["price_set_money_amount_id"], + "columnNames": [ + "price_set_money_amount_id" + ], "composite": false, "keyName": "IDX_price_rule_price_set_money_amount_id", "primary": false, "unique": false }, + { + "columnNames": [ + "deleted_at" + ], + "composite": false, + "keyName": "IDX_price_rule_deleted_at", + "primary": false, + "unique": false + }, { "keyName": "price_rule_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -788,26 +947,38 @@ "foreignKeys": { "price_rule_price_set_id_foreign": { "constraintName": "price_rule_price_set_id_foreign", - "columnNames": ["price_set_id"], + "columnNames": [ + "price_set_id" + ], "localTableName": "public.price_rule", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.price_set", "deleteRule": "cascade", "updateRule": "cascade" }, "price_rule_rule_type_id_foreign": { "constraintName": "price_rule_rule_type_id_foreign", - "columnNames": ["rule_type_id"], + "columnNames": [ + "rule_type_id" + ], "localTableName": "public.price_rule", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.rule_type", "updateRule": "cascade" }, "price_rule_price_set_money_amount_id_foreign": { "constraintName": "price_rule_price_set_money_amount_id_foreign", - "columnNames": ["price_set_money_amount_id"], + "columnNames": [ + "price_set_money_amount_id" + ], "localTableName": "public.price_rule", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.price_set_money_amount", "deleteRule": "cascade", "updateRule": "cascade" @@ -825,8 +996,8 @@ "nullable": false, "mappedType": "text" }, - "rule_type_id": { - "name": "rule_type_id", + "price_list_id": { + "name": "price_list_id", "type": "text", "unsigned": false, "autoincrement": false, @@ -834,8 +1005,8 @@ "nullable": false, "mappedType": "text" }, - "price_list_id": { - "name": "price_list_id", + "rule_type_id": { + "name": "rule_type_id", "type": "text", "unsigned": false, "autoincrement": false, @@ -848,29 +1019,38 @@ "schema": "public", "indexes": [ { - "columnNames": ["rule_type_id"], + "columnNames": [ + "price_list_id" + ], "composite": false, - "keyName": "IDX_price_list_rule_rule_type_id", + "keyName": "IDX_price_list_rule_price_list_id", "primary": false, "unique": false }, { - "columnNames": ["price_list_id"], + "columnNames": [ + "rule_type_id" + ], "composite": false, - "keyName": "IDX_price_list_rule_price_list_id", + "keyName": "IDX_price_list_rule_rule_type_id", "primary": false, "unique": false }, { "keyName": "IDX_price_list_rule_rule_type_id_price_list_id_unique", - "columnNames": ["price_list_id", "rule_type_id"], + "columnNames": [ + "price_list_id", + "rule_type_id" + ], "composite": true, "primary": false, "unique": true }, { "keyName": "price_list_rule_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -878,21 +1058,29 @@ ], "checks": [], "foreignKeys": { - "price_list_rule_rule_type_id_foreign": { - "constraintName": "price_list_rule_rule_type_id_foreign", - "columnNames": ["rule_type_id"], - "localTableName": "public.price_list_rule", - "referencedColumnNames": ["id"], - "referencedTableName": "public.rule_type", - "updateRule": "cascade" - }, "price_list_rule_price_list_id_foreign": { "constraintName": "price_list_rule_price_list_id_foreign", - "columnNames": ["price_list_id"], + "columnNames": [ + "price_list_id" + ], "localTableName": "public.price_list_rule", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.price_list", "updateRule": "cascade" + }, + "price_list_rule_rule_type_id_foreign": { + "constraintName": "price_list_rule_rule_type_id_foreign", + "columnNames": [ + "rule_type_id" + ], + "localTableName": "public.price_list_rule", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.rule_type", + "updateRule": "cascade" } } }, @@ -930,7 +1118,9 @@ "schema": "public", "indexes": [ { - "columnNames": ["price_list_rule_id"], + "columnNames": [ + "price_list_rule_id" + ], "composite": false, "keyName": "IDX_price_list_rule_price_list_rule_value_id", "primary": false, @@ -938,7 +1128,9 @@ }, { "keyName": "price_list_rule_value_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -948,9 +1140,13 @@ "foreignKeys": { "price_list_rule_value_price_list_rule_id_foreign": { "constraintName": "price_list_rule_value_price_list_rule_id_foreign", - "columnNames": ["price_list_rule_id"], + "columnNames": [ + "price_list_rule_id" + ], "localTableName": "public.price_list_rule_value", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.price_list_rule", "deleteRule": "cascade", "updateRule": "cascade" diff --git a/packages/pricing/src/migrations/Migration20240103140327.ts b/packages/pricing/src/migrations/Migration20240103140327.ts new file mode 100644 index 0000000000000..c53c56d874c2d --- /dev/null +++ b/packages/pricing/src/migrations/Migration20240103140327.ts @@ -0,0 +1,55 @@ +import { Migration } from "@mikro-orm/migrations" + +export class Migration20240103140327 extends Migration { + async up(): Promise { + this.addSql( + 'alter table "price_set_money_amount" drop constraint "price_set_money_amount_price_list_id_foreign";' + ) + + this.addSql( + 'alter table "price_set_money_amount" add column "created_at" timestamptz not null default now(), add column "updated_at" timestamptz not null default now(), add column "deleted_at" timestamptz null;' + ) + this.addSql( + 'alter table "price_set_money_amount" add constraint "price_set_money_amount_price_list_id_foreign" foreign key ("price_list_id") references "price_list" ("id") on delete cascade;' + ) + this.addSql( + 'create index "IDX_price_set_money_amount_deleted_at" on "price_set_money_amount" ("deleted_at");' + ) + + this.addSql( + 'alter table "price_rule" add column "created_at" timestamptz not null default now(), add column "updated_at" timestamptz not null default now(), add column "deleted_at" timestamptz null;' + ) + this.addSql('alter table "price_rule" drop column "is_dynamic";') + this.addSql( + 'create index "IDX_price_rule_deleted_at" on "price_rule" ("deleted_at");' + ) + } + + async down(): Promise { + this.addSql( + 'alter table "price_set_money_amount" drop constraint "price_set_money_amount_price_list_id_foreign";' + ) + + this.addSql('drop index "IDX_price_set_money_amount_deleted_at";') + this.addSql( + 'alter table "price_set_money_amount" drop column "created_at";' + ) + this.addSql( + 'alter table "price_set_money_amount" drop column "updated_at";' + ) + this.addSql( + 'alter table "price_set_money_amount" drop column "deleted_at";' + ) + this.addSql( + 'alter table "price_set_money_amount" add constraint "price_set_money_amount_price_list_id_foreign" foreign key ("price_list_id") references "price_list" ("id") on update cascade on delete cascade;' + ) + + this.addSql( + 'alter table "price_rule" add column "is_dynamic" boolean not null default false;' + ) + this.addSql('drop index "IDX_price_rule_deleted_at";') + this.addSql('alter table "price_rule" drop column "created_at";') + this.addSql('alter table "price_rule" drop column "updated_at";') + this.addSql('alter table "price_rule" drop column "deleted_at";') + } +} diff --git a/packages/pricing/src/models/money-amount.ts b/packages/pricing/src/models/money-amount.ts index dc6c2b5a47b36..f4a2c1f2647ba 100644 --- a/packages/pricing/src/models/money-amount.ts +++ b/packages/pricing/src/models/money-amount.ts @@ -1,12 +1,14 @@ -import { generateEntityId } from "@medusajs/utils" +import { DALUtils, generateEntityId } from "@medusajs/utils" import { BeforeCreate, Collection, Entity, + Filter, Index, ManyToMany, ManyToOne, OneToOne, + OnInit, OptionalProps, PrimaryKey, Property, @@ -17,18 +19,20 @@ import { PriceSetMoneyAmount } from "./index" import PriceSet from "./price-set" @Entity() +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) class MoneyAmount { [OptionalProps]?: | "created_at" | "updated_at" | "deleted_at" | "price_set_money_amount" + | "amount" @PrimaryKey({ columnType: "text" }) id!: string @Property({ columnType: "text", nullable: true }) - currency_code?: string + currency_code: string | null @ManyToMany({ entity: () => PriceSet, @@ -39,6 +43,7 @@ class MoneyAmount { @OneToOne({ entity: () => PriceSetMoneyAmount, mappedBy: (psma) => psma.money_amount, + cascade: ["soft-remove"] as any, }) price_set_money_amount: PriceSetMoneyAmount @@ -47,16 +52,20 @@ class MoneyAmount { index: "IDX_money_amount_currency_code", fieldName: "currency_code", }) - currency?: Currency + currency: Currency - @Property({ columnType: "numeric", nullable: true, serializer: Number }) - amount?: number + @Property({ + columnType: "numeric", + nullable: true, + serializer: Number, + }) + amount: number | null @Property({ columnType: "numeric", nullable: true }) - min_quantity?: number | null + min_quantity: number | null @Property({ columnType: "numeric", nullable: true }) - max_quantity?: number | null + max_quantity: number | null @Property({ onCreate: () => new Date(), @@ -75,12 +84,17 @@ class MoneyAmount { @Index({ name: "IDX_money_amount_deleted_at" }) @Property({ columnType: "timestamptz", nullable: true }) - deleted_at: Date + deleted_at: Date | null @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "ma") } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "ma") + } } export default MoneyAmount diff --git a/packages/pricing/src/models/price-list-rule-value.ts b/packages/pricing/src/models/price-list-rule-value.ts index c2c9e5d9ac07c..58f7cb0bfcd29 100644 --- a/packages/pricing/src/models/price-list-rule-value.ts +++ b/packages/pricing/src/models/price-list-rule-value.ts @@ -2,6 +2,7 @@ import { BeforeCreate, Entity, ManyToOne, + OnInit, PrimaryKey, Property, } from "@mikro-orm/core" @@ -16,7 +17,7 @@ export default class PriceListRuleValue { @ManyToOne(() => PriceListRule, { onDelete: "cascade", - fieldName: 'price_list_rule_id', + fieldName: "price_list_rule_id", index: "IDX_price_list_rule_price_list_rule_value_id", }) price_list_rule: PriceListRule @@ -28,4 +29,9 @@ export default class PriceListRuleValue { onCreate() { this.id = generateEntityId(this.id, "plrv") } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "plrv") + } } diff --git a/packages/pricing/src/models/price-list-rule.ts b/packages/pricing/src/models/price-list-rule.ts index edbdbde218027..239048f18b0fe 100644 --- a/packages/pricing/src/models/price-list-rule.ts +++ b/packages/pricing/src/models/price-list-rule.ts @@ -5,6 +5,7 @@ import { Entity, ManyToOne, OneToMany, + OnInit, OptionalProps, PrimaryKey, Unique, @@ -54,4 +55,9 @@ export default class PriceListRule { beforeCreate() { this.id = generateEntityId(this.id, "plrule") } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "plrule") + } } diff --git a/packages/pricing/src/models/price-list.ts b/packages/pricing/src/models/price-list.ts index ca6ba9081213d..89468074e417b 100644 --- a/packages/pricing/src/models/price-list.ts +++ b/packages/pricing/src/models/price-list.ts @@ -1,4 +1,8 @@ -import { generateEntityId } from "@medusajs/utils" +import { + generateEntityId, + PriceListStatus, + PriceListType, +} from "@medusajs/utils" import { BeforeCreate, Cascade, @@ -8,12 +12,11 @@ import { Index, ManyToMany, OneToMany, + OnInit, OptionalProps, PrimaryKey, Property, } from "@mikro-orm/core" - -import { PriceListStatus, PriceListType } from "@medusajs/utils" import PriceListRule from "./price-list-rule" import PriceSetMoneyAmount from "./price-set-money-amount" import RuleType from "./rule-type" @@ -47,22 +50,22 @@ export default class PriceList { description: string @Enum({ items: () => PriceListStatus, default: PriceListStatus.DRAFT }) - status?: PriceListStatus + status: PriceListStatus @Enum({ items: () => PriceListType, default: PriceListType.SALE }) - type?: PriceListType + type: PriceListType @Property({ columnType: "timestamptz", nullable: true, }) - starts_at?: Date | null + starts_at: Date | null @Property({ columnType: "timestamptz", nullable: true, }) - ends_at?: Date | null + ends_at: Date | null @OneToMany(() => PriceSetMoneyAmount, (psma) => psma.price_list, { cascade: [Cascade.REMOVE], @@ -89,7 +92,7 @@ export default class PriceList { columnType: "timestamptz", defaultRaw: "now()", }) - created_at?: Date + created_at: Date @Property({ onCreate: () => new Date(), @@ -97,14 +100,19 @@ export default class PriceList { columnType: "timestamptz", defaultRaw: "now()", }) - updated_at?: Date + updated_at: Date @Index({ name: "IDX_price_list_deleted_at" }) @Property({ columnType: "timestamptz", nullable: true }) - deleted_at?: Date + deleted_at: Date | null @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "plist") } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "plist") + } } diff --git a/packages/pricing/src/models/price-rule.ts b/packages/pricing/src/models/price-rule.ts index a1d786759bda1..d0e6a7c310465 100644 --- a/packages/pricing/src/models/price-rule.ts +++ b/packages/pricing/src/models/price-rule.ts @@ -1,21 +1,31 @@ import { BeforeCreate, Entity, + Filter, + Index, ManyToOne, + OnInit, OptionalProps, PrimaryKey, Property, } from "@mikro-orm/core" +import { DALUtils, generateEntityId } from "@medusajs/utils" -import { generateEntityId } from "@medusajs/utils" import PriceSet from "./price-set" import PriceSetMoneyAmount from "./price-set-money-amount" import RuleType from "./rule-type" -type OptionalFields = "id" | "is_dynamic" | "priority" +type OptionalFields = + | "id" + | "is_dynamic" + | "priority" + | "created_at" + | "updated_at" + | "deleted_at" type OptionalRelations = "price_set" | "rule_type" | "price_set_money_amount" @Entity() +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) export default class PriceRule { [OptionalProps]: OptionalFields | OptionalRelations @@ -54,8 +64,32 @@ export default class PriceRule { }) price_set_money_amount: PriceSetMoneyAmount + @Property({ + onCreate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date + + @Index({ name: "IDX_price_rule_deleted_at" }) + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date | null + @BeforeCreate() beforeCreate() { this.id = generateEntityId(this.id, "prule") } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "prule") + } } diff --git a/packages/pricing/src/models/price-set-money-amount-rules.ts b/packages/pricing/src/models/price-set-money-amount-rules.ts index c4040d5ce0c2f..184ced8ebc006 100644 --- a/packages/pricing/src/models/price-set-money-amount-rules.ts +++ b/packages/pricing/src/models/price-set-money-amount-rules.ts @@ -3,6 +3,7 @@ import { BeforeCreate, Entity, ManyToOne, + OnInit, PrimaryKey, Property, } from "@mikro-orm/core" @@ -19,12 +20,12 @@ export default class PriceSetMoneyAmountRules { onDelete: "cascade", index: "IDX_price_set_money_amount_rules_price_set_money_amount_id", }) - price_set_money_amount?: PriceSetMoneyAmount | string + price_set_money_amount: PriceSetMoneyAmount @ManyToOne(() => RuleType, { index: "IDX_price_set_money_amount_rules_rule_type_id", }) - rule_type?: RuleType | string + rule_type: RuleType @Property({ columnType: "text" }) value: string @@ -33,4 +34,9 @@ export default class PriceSetMoneyAmountRules { onCreate() { this.id = generateEntityId(this.id, "psmar") } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "psmar") + } } diff --git a/packages/pricing/src/models/price-set-money-amount.ts b/packages/pricing/src/models/price-set-money-amount.ts index 0c6479d22d880..4c381e78b1ec0 100644 --- a/packages/pricing/src/models/price-set-money-amount.ts +++ b/packages/pricing/src/models/price-set-money-amount.ts @@ -1,15 +1,20 @@ -import { generateEntityId } from "@medusajs/utils" import { BeforeCreate, + Cascade, Collection, Entity, + Filter, + Index, ManyToOne, + OnInit, OneToMany, OneToOne, + OptionalProps, PrimaryKey, PrimaryKeyType, Property, } from "@mikro-orm/core" +import { DALUtils, generateEntityId } from "@medusajs/utils" import MoneyAmount from "./money-amount" import PriceList from "./price-list" @@ -18,7 +23,10 @@ import PriceSet from "./price-set" import PriceSetMoneyAmountRules from "./price-set-money-amount-rules" @Entity() +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) export default class PriceSetMoneyAmount { + [OptionalProps]?: "created_at" | "updated_at" | "deleted_at" + @PrimaryKey({ columnType: "text" }) id!: string @@ -29,20 +37,21 @@ export default class PriceSetMoneyAmount { onDelete: "cascade", index: "IDX_price_set_money_amount_price_set_id", }) - price_set?: PriceSet + price_set: PriceSet @OneToOne(() => MoneyAmount, { onDelete: "cascade", index: "IDX_price_set_money_amount_money_amount_id", }) - money_amount?: MoneyAmount + money_amount: MoneyAmount @Property({ columnType: "integer", default: 0 }) - rules_count?: number + rules_count: number @OneToMany({ entity: () => PriceRule, mappedBy: (pr) => pr.price_set_money_amount, + cascade: ["soft-remove"] as any, }) price_rules = new Collection(this) @@ -55,15 +64,39 @@ export default class PriceSetMoneyAmount { @ManyToOne(() => PriceList, { index: "IDX_price_rule_price_list_id", onDelete: "cascade", + cascade: [Cascade.REMOVE, "soft-remove"] as any, nullable: true, }) - price_list?: PriceList + price_list: PriceList | null + + @Property({ + onCreate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date + + @Property({ columnType: "timestamptz", nullable: true , index: "IDX_price_set_money_amount_deleted_at"}) + deleted_at: Date | null @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "psma") } + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "psma") + } + [PrimaryKeyType]?: [string, string] constructor(money_amount: MoneyAmount, price_set: PriceSet) { diff --git a/packages/pricing/src/models/price-set-rule-type.ts b/packages/pricing/src/models/price-set-rule-type.ts index f1921f5d5844b..0cb27b3b2d846 100644 --- a/packages/pricing/src/models/price-set-rule-type.ts +++ b/packages/pricing/src/models/price-set-rule-type.ts @@ -2,8 +2,8 @@ import { BeforeCreate, Entity, ManyToOne, + OnInit, PrimaryKey, - PrimaryKeyType, } from "@mikro-orm/core" import PriceSet from "./price-set" @@ -30,4 +30,9 @@ export default class PriceSetRuleType { onCreate() { this.id = generateEntityId(this.id, "psrt") } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "psrt") + } } diff --git a/packages/pricing/src/models/price-set.ts b/packages/pricing/src/models/price-set.ts index 3b4d7a50ea1b3..ebe2264081fa4 100644 --- a/packages/pricing/src/models/price-set.ts +++ b/packages/pricing/src/models/price-set.ts @@ -6,6 +6,7 @@ import { Entity, ManyToMany, OneToMany, + OnInit, OptionalProps, PrimaryKey, } from "@mikro-orm/core" @@ -50,4 +51,9 @@ export default class PriceSet { onCreate() { this.id = generateEntityId(this.id, "pset") } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "pset") + } } diff --git a/packages/pricing/src/models/rule-type.ts b/packages/pricing/src/models/rule-type.ts index d393faada638d..10c2926f172e2 100644 --- a/packages/pricing/src/models/rule-type.ts +++ b/packages/pricing/src/models/rule-type.ts @@ -4,6 +4,7 @@ import { Collection, Entity, ManyToMany, + OnInit, OptionalProps, PrimaryKey, Property, @@ -28,13 +29,18 @@ class RuleType { @Property({ columnType: "integer", default: 0 }) default_priority: number - @ManyToMany(() => PriceSet, priceSet => priceSet.rule_types) - price_sets = new Collection(this); + @ManyToMany(() => PriceSet, (priceSet) => priceSet.rule_types) + price_sets = new Collection(this) @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "rul-typ") } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "rul-typ") + } } export default RuleType diff --git a/packages/pricing/src/repositories/money-amount.ts b/packages/pricing/src/repositories/money-amount.ts index 097558504d88e..e2ce6afebe54b 100644 --- a/packages/pricing/src/repositories/money-amount.ts +++ b/packages/pricing/src/repositories/money-amount.ts @@ -1,9 +1,9 @@ import { Context, DAL } from "@medusajs/types" import { DALUtils, MedusaError } from "@medusajs/utils" import { - LoadStrategy, FilterQuery as MikroFilterQuery, FindOptions as MikroOptions, + LoadStrategy, } from "@mikro-orm/core" import { MoneyAmount } from "@models" @@ -72,7 +72,7 @@ export class MoneyAmountRepository extends DALUtils.MikroOrmBaseRepository { const manager = this.getActiveManager(context) const moneyAmounts = data.map((moneyAmountData) => { - return manager.create(MoneyAmount, moneyAmountData) + return manager.create(MoneyAmount, moneyAmountData as any) }) manager.persist(moneyAmounts) diff --git a/packages/pricing/src/repositories/price-set-money-amount-rules.ts b/packages/pricing/src/repositories/price-set-money-amount-rules.ts index 1e7756930e3f9..811e11a0d7abc 100644 --- a/packages/pricing/src/repositories/price-set-money-amount-rules.ts +++ b/packages/pricing/src/repositories/price-set-money-amount-rules.ts @@ -1,9 +1,9 @@ import { Context, DAL } from "@medusajs/types" import { DALUtils, MedusaError } from "@medusajs/utils" import { - LoadStrategy, FilterQuery as MikroFilterQuery, FindOptions as MikroOptions, + LoadStrategy, } from "@mikro-orm/core" import { PriceSetMoneyAmountRules } from "@models" @@ -72,7 +72,7 @@ export class PriceSetMoneyAmountRulesRepository extends DALUtils.MikroOrmBaseRep const manager = this.getActiveManager(context) const psmar = data.map((psmarData) => { - return manager.create(PriceSetMoneyAmountRules, psmarData) + return manager.create(PriceSetMoneyAmountRules, psmarData as any) }) manager.persist(psmar) @@ -114,7 +114,7 @@ export class PriceSetMoneyAmountRulesRepository extends DALUtils.MikroOrmBaseRep ) } - return manager.assign(existingRecord, psmarData) + return manager.assign(existingRecord, psmarData as any) }) manager.persist(psmar) diff --git a/packages/pricing/src/scripts/seed.ts b/packages/pricing/src/scripts/seed.ts index 6aac156a3fe29..bc1a6b6f057cf 100644 --- a/packages/pricing/src/scripts/seed.ts +++ b/packages/pricing/src/scripts/seed.ts @@ -1,7 +1,7 @@ import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types" import { DALUtils, ModulesSdkUtils } from "@medusajs/utils" import { EntitySchema, RequiredEntityData } from "@mikro-orm/core" -import { SqlEntityManager } from "@mikro-orm/postgresql" +import { PostgreSqlDriver, SqlEntityManager } from "@mikro-orm/postgresql" import * as PricingModels from "@models" import { EOL } from "os" import { resolve } from "path" @@ -49,10 +49,10 @@ export async function run({ try { logger.info("Inserting price_sets, currencies & money_amounts") - await createCurrencies(manager, currenciesData) - await createMoneyAmounts(manager, moneyAmountsData) - await createPriceSets(manager, priceSetsData) - await createPriceSetMoneyAmounts(manager, priceSetMoneyAmountsData) + await createCurrencies(manager as any, currenciesData) + await createMoneyAmounts(manager as any, moneyAmountsData) + await createPriceSets(manager as any, priceSetsData) + await createPriceSetMoneyAmounts(manager as any, priceSetMoneyAmountsData) } catch (e) { logger.error( `Failed to insert the seed data in the PostgreSQL database ${dbData.clientUrl}.${EOL}${e}` @@ -63,7 +63,7 @@ export async function run({ } async function createCurrencies( - manager: SqlEntityManager, + manager: SqlEntityManager, data: RequiredEntityData[] ) { const currencies = data.map((currencyData) => { @@ -76,7 +76,7 @@ async function createCurrencies( } async function createMoneyAmounts( - manager: SqlEntityManager, + manager: SqlEntityManager, data: RequiredEntityData[] ) { const moneyAmounts = data.map((moneyAmountData) => { @@ -89,7 +89,7 @@ async function createMoneyAmounts( } async function createPriceSets( - manager: SqlEntityManager, + manager: SqlEntityManager, data: RequiredEntityData[] ) { const priceSets = data.map((priceSetData) => { @@ -102,7 +102,7 @@ async function createPriceSets( } async function createPriceSetMoneyAmounts( - manager: SqlEntityManager, + manager: SqlEntityManager, data: RequiredEntityData[] ) { const priceSetMoneyAmounts = data.map((priceSetMoneyAmountData) => { diff --git a/packages/pricing/src/services/money-amount.ts b/packages/pricing/src/services/money-amount.ts index 40f05d6cd69aa..640eacd1f7c94 100644 --- a/packages/pricing/src/services/money-amount.ts +++ b/packages/pricing/src/services/money-amount.ts @@ -101,4 +101,20 @@ export default class MoneyAmountService< ): Promise { await this.moneyAmountRepository_.delete(ids, sharedContext) } + + @InjectTransactionManager("moneyAmountRepository_") + async softDelete( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.moneyAmountRepository_.softDelete(ids, sharedContext) + } + + @InjectTransactionManager("moneyAmountRepository_") + async restore( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise<[TEntity[], Record]> { + return await this.moneyAmountRepository_.restore(ids, sharedContext) + } } diff --git a/packages/pricing/src/services/price-set-money-amount.ts b/packages/pricing/src/services/price-set-money-amount.ts index ddc39855e0d1f..ff21ef8a22899 100644 --- a/packages/pricing/src/services/price-set-money-amount.ts +++ b/packages/pricing/src/services/price-set-money-amount.ts @@ -44,7 +44,7 @@ export default class PriceSetMoneyAmountService< @InjectManager("priceSetMoneyAmountRepository_") async list( filters: ServiceTypes.FilterablePriceSetMoneyAmountProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise { return (await this.priceSetMoneyAmountRepository_.find( @@ -56,7 +56,7 @@ export default class PriceSetMoneyAmountService< @InjectManager("priceSetMoneyAmountRepository_") async listAndCount( filters: ServiceTypes.FilterablePriceSetMoneyAmountProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise<[TEntity[], number]> { return (await this.priceSetMoneyAmountRepository_.findAndCount( @@ -67,7 +67,7 @@ export default class PriceSetMoneyAmountService< private buildQueryForList( filters: ServiceTypes.FilterablePriceSetMoneyAmountProps = {}, - config: FindConfig = {} + config: FindConfig = {} ) { const queryOptions = ModulesSdkUtils.buildQuery(filters, config) diff --git a/packages/pricing/src/services/pricing-module.ts b/packages/pricing/src/services/pricing-module.ts index 68906fd1c6582..68d00fbc473fa 100644 --- a/packages/pricing/src/services/pricing-module.ts +++ b/packages/pricing/src/services/pricing-module.ts @@ -13,6 +13,7 @@ import { PricingFilters, PricingRepositoryService, PricingTypes, + RestoreReturn, RuleTypeDTO, } from "@medusajs/types" import { @@ -24,6 +25,7 @@ import { arrayDifference, deduplicate, groupBy, + mapObjectTo, removeNullish, } from "@medusajs/utils" @@ -54,7 +56,11 @@ import { PriceSetService, RuleTypeService, } from "@services" -import { joinerConfig } from "../joiner-config" +import { + LinkableKeys, + entityNameToLinkableKeysMap, + joinerConfig, +} from "../joiner-config" import { validatePriceListDates } from "@utils" import { ServiceTypes } from "@types" import { CreatePriceListRuleValueDTO } from "src/types/services" @@ -861,6 +867,41 @@ export default class PricingModuleService< await this.moneyAmountService_.delete(ids, sharedContext) } + @InjectTransactionManager("baseRepository_") + async softDeleteMoneyAmounts( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.moneyAmountService_.softDelete(ids, sharedContext) + } + + @InjectTransactionManager("baseRepository_") + async restoreDeletedMoneyAmounts< + TReturnableLinkableKeys extends string = Lowercase< + keyof typeof LinkableKeys + > + >( + ids: string[], + { returnLinkableKeys }: RestoreReturn = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise, string[]> | void> { + const [_, cascadedEntitiesMap] = await this.moneyAmountService_.restore( + ids, + sharedContext + ) + + let mappedCascadedEntitiesMap + if (returnLinkableKeys) { + mappedCascadedEntitiesMap = mapObjectTo< + Record, string[]> + >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { + pick: returnLinkableKeys, + }) + } + + return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 + } + @InjectManager("baseRepository_") async retrieveCurrency( code: string, @@ -1132,7 +1173,7 @@ export default class PricingModuleService< ) return this.baseRepository_.serialize< - PricingTypes.PriceSetMoneyAmountRulesDTO[] + PricingTypes.PriceSetMoneyAmountDTO[] >(records, { populate: true, }) @@ -1153,7 +1194,7 @@ export default class PricingModuleService< return [ await this.baseRepository_.serialize< - PricingTypes.PriceSetMoneyAmountRulesDTO[] + PricingTypes.PriceSetMoneyAmountDTO[] >(records, { populate: true, }), diff --git a/packages/pricing/src/types/services/money-amount.ts b/packages/pricing/src/types/services/money-amount.ts index 1252696a8085c..70f41fb2dc807 100644 --- a/packages/pricing/src/types/services/money-amount.ts +++ b/packages/pricing/src/types/services/money-amount.ts @@ -30,6 +30,9 @@ export interface MoneyAmountDTO { min_quantity?: number max_quantity?: number price_set_money_amount?: PriceSetMoneyAmountDTO + created_at: Date + updated_at: Date + deleted_at: Date | null } export interface FilterableMoneyAmountProps diff --git a/packages/pricing/src/types/services/price-rule.ts b/packages/pricing/src/types/services/price-rule.ts index 3ee33e43c2c3c..d3db234d02b24 100644 --- a/packages/pricing/src/types/services/price-rule.ts +++ b/packages/pricing/src/types/services/price-rule.ts @@ -33,6 +33,9 @@ export interface PriceRuleDTO { priority: number price_set_money_amount_id: string price_list_id: string + created_at: Date + updated_at: Date + deleted_at: Date | null } export interface FilterablePriceRuleProps diff --git a/packages/pricing/src/types/services/price-set-money-amount.ts b/packages/pricing/src/types/services/price-set-money-amount.ts index dd6fa09cc73b6..11fcee3e5934e 100644 --- a/packages/pricing/src/types/services/price-set-money-amount.ts +++ b/packages/pricing/src/types/services/price-set-money-amount.ts @@ -36,4 +36,7 @@ export interface PriceSetMoneyAmountDTO { price_set_id?: string price_rules?: PriceRuleDTO[] money_amount?: MoneyAmountDTO + created_at: Date + updated_at: Date + deleted_at: Date | null } diff --git a/packages/types/src/pricing/common/money-amount.ts b/packages/types/src/pricing/common/money-amount.ts index 160abead7bcb5..2270139fa99e7 100644 --- a/packages/types/src/pricing/common/money-amount.ts +++ b/packages/types/src/pricing/common/money-amount.ts @@ -18,7 +18,7 @@ export interface MoneyAmountDTO { currency_code?: string /** * The money amount's currency. - * + * * @expandable */ currency?: CurrencyDTO @@ -38,6 +38,18 @@ export interface MoneyAmountDTO { * The details of the relation between the money amount and its associated price set. */ price_set_money_amount?: PriceSetMoneyAmountDTO + /** + * When the money_amount was created. + */ + created_at: Date + /** + * When the money_amount was updated. + */ + updated_at: Date + /** + * When the money_amount was deleted. + */ + deleted_at: null | Date } /** diff --git a/packages/types/src/pricing/common/price-rule.ts b/packages/types/src/pricing/common/price-rule.ts index 61ff8176c1a2f..52556f0025eed 100644 --- a/packages/types/src/pricing/common/price-rule.ts +++ b/packages/types/src/pricing/common/price-rule.ts @@ -48,6 +48,18 @@ export interface PriceRuleDTO { * The ID of the associated price list. */ price_list_id: string + /** + * When the price_rule was created. + */ + created_at: Date + /** + * When the price_rule was updated. + */ + updated_at: Date + /** + * When the price_rule was deleted. + */ + deleted_at: null | Date } /** diff --git a/packages/types/src/pricing/common/price-set-money-amount.ts b/packages/types/src/pricing/common/price-set-money-amount.ts index c207f04bcfe8f..80ae6490cd4d7 100644 --- a/packages/types/src/pricing/common/price-set-money-amount.ts +++ b/packages/types/src/pricing/common/price-set-money-amount.ts @@ -1,8 +1,8 @@ -import { BaseFilterable } from "../../dal" -import { MoneyAmountDTO } from "./money-amount" -import { PriceListDTO } from "./price-list" -import { PriceRuleDTO } from "./price-rule" -import { PriceSetDTO } from "./price-set" +import { BaseFilterable } from "../../dal"; +import { MoneyAmountDTO } from "./money-amount"; +import { PriceListDTO } from "./price-list"; +import { PriceRuleDTO } from "./price-rule"; +import { PriceSetDTO } from "./price-set"; /** * @interface @@ -46,6 +46,18 @@ export interface PriceSetMoneyAmountDTO { * @expandable */ money_amount?: MoneyAmountDTO + /** + * When the price_set_money_amount was created. + */ + created_at: Date + /** + * When the price_set_money_amount was updated. + */ + updated_at: Date + /** + * When the price_set_money_amount was deleted. + */ + deleted_at: null | Date } export interface UpdatePriceSetMoneyAmountDTO { diff --git a/packages/types/src/pricing/service.ts b/packages/types/src/pricing/service.ts index 0480950494182..9c9a7a10e3c69 100644 --- a/packages/types/src/pricing/service.ts +++ b/packages/types/src/pricing/service.ts @@ -47,6 +47,7 @@ import { import { FindConfig } from "../common" import { ModuleJoinerConfig } from "../modules-sdk" import { Context } from "../shared-context" +import { RestoreReturn } from "../dal" export interface IPricingModuleService { /** @@ -1274,6 +1275,58 @@ export interface IPricingModuleService { */ deleteMoneyAmounts(ids: string[], sharedContext?: Context): Promise + /** + * This method soft deletes money amounts by their IDs. + * + * @param {string[]} ids - The IDs of the money amounts to delete. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} Resolves when the money amounts are successfully deleted. + * + * @example + * import { + * initialize as initializePricingModule, + * } from "@medusajs/pricing" + * + * async function softDeleteMoneyAmounts (moneyAmountIds: string[]) { + * const pricingService = await initializePricingModule() + * + * await pricingService.softDeleteMoneyAmounts( + * moneyAmountIds + * ) + * } + */ + softDeleteMoneyAmounts(ids: string[], sharedContext?: Context): Promise + + /** + * This method restores soft deleted money amounts by their IDs. + * + * @param {string[]} ids - The IDs of the money amounts to delete. + * @param {RestoreReturn} config - + * Configurations determining which relations to restore along with each of the money amounts. You can pass to its `returnLinkableKeys` + * property any of the money_amount's relation attribute names, such as `price_set_money_amount_id`. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise | void>} + * An object that includes the IDs of related records that were restored, such as the ID of associated product variants. The object's keys are the ID attribute names of the product entity's relations, such as `variant_id`, and its value is an array of strings, each being the ID of the record associated with the product through this relation, such as the IDs of associated product variants. + * + * @example + * import { + * initialize as initializePricingModule, + * } from "@medusajs/pricing" + * + * async function softDeleteMoneyAmounts (moneyAmountIds: string[]) { + * const pricingService = await initializePricingModule() + * + * await pricingService.softDeleteMoneyAmounts( + * moneyAmountIds + * ) + * } + */ + restoreDeletedMoneyAmounts( + ids: string[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> + /** * This method retrieves a currency by its code and and optionally based on the provided configurations. * diff --git a/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts b/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts index 0bcc495237a80..2f8fe52d329bb 100644 --- a/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts +++ b/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts @@ -6,7 +6,7 @@ import { } from "@medusajs/types" import { isString } from "../../common" import { MedusaContext } from "../../decorators" -import { InjectTransactionManager, buildQuery } from "../../modules-sdk" +import { buildQuery, InjectTransactionManager } from "../../modules-sdk" import { getSoftDeletedCascadedEntitiesIdsMappedBy, transactionWrapper, diff --git a/packages/utils/src/dal/mikro-orm/utils.ts b/packages/utils/src/dal/mikro-orm/utils.ts index 63d599eb2c1fd..fc5d1f55d78a3 100644 --- a/packages/utils/src/dal/mikro-orm/utils.ts +++ b/packages/utils/src/dal/mikro-orm/utils.ts @@ -1,5 +1,3 @@ -import { SoftDeletableFilterKey } from "./mikro-orm-soft-deletable-filter" - export const mikroOrmUpdateDeletedAtRecursively = async < T extends object = any >( @@ -22,19 +20,25 @@ export const mikroOrmUpdateDeletedAtRecursively = async < ) for (const relation of relationsToCascade) { - let collectionRelation = entity[relation.name] + let entityRelation = entity[relation.name] - if (!collectionRelation.isInitialized()) { - await collectionRelation.init() + // Handle optional relationships + if (relation.nullable && !entityRelation) { + continue } - const relationEntities = await collectionRelation.getItems({ - filters: { - [SoftDeletableFilterKey]: { - withDeleted: true, - }, - }, - }) + const isCollection = "toArray" in entityRelation + let relationEntities: any[] = [] + + if (isCollection) { + if (!entityRelation.isInitialized()) { + entityRelation = await entityRelation.init({ populate: true }) + } + relationEntities = entityRelation.getItems() + } else { + const initializedEntityRelation = await entityRelation.__helper?.init() + relationEntities = [initializedEntityRelation] + } await mikroOrmUpdateDeletedAtRecursively(manager, relationEntities, value) } From 7f62ab1b583f8ea39cc2aad169d98a5c514f40b1 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Thu, 4 Jan 2024 09:04:43 +0100 Subject: [PATCH 8/8] fix(medusa-payment-stripe): Fix error handling (#5991) --- .changeset/smart-zoos-nail.md | 6 ++++++ .../medusa-payment-stripe/src/core/stripe-base.ts | 14 +++++++------- .../medusa/src/interfaces/payment-processor.ts | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 .changeset/smart-zoos-nail.md diff --git a/.changeset/smart-zoos-nail.md b/.changeset/smart-zoos-nail.md new file mode 100644 index 0000000000000..c8438cb4e8f0a --- /dev/null +++ b/.changeset/smart-zoos-nail.md @@ -0,0 +1,6 @@ +--- +"@medusajs/medusa": patch +"medusa-payment-stripe": patch +--- + +fix(medusa, medusa-payment-stripe): fix stripe error handling diff --git a/packages/medusa-payment-stripe/src/core/stripe-base.ts b/packages/medusa-payment-stripe/src/core/stripe-base.ts index 454bce3133d98..0cdd4b2beedd4 100644 --- a/packages/medusa-payment-stripe/src/core/stripe-base.ts +++ b/packages/medusa-payment-stripe/src/core/stripe-base.ts @@ -322,16 +322,16 @@ abstract class StripeBase extends AbstractPaymentProcessor { protected buildError( message: string, - e: Stripe.StripeRawError | PaymentProcessorError | Error + error: Stripe.StripeRawError | PaymentProcessorError | Error ): PaymentProcessorError { return { error: message, - code: "code" in e ? e.code : "", - detail: isPaymentProcessorError(e) - ? `${e.error}${EOL}${e.detail ?? ""}` - : "detail" in e - ? e.detail - : e.message ?? "", + code: "code" in error ? error.code : "unknown", + detail: isPaymentProcessorError(error) + ? `${error.error}${EOL}${error.detail ?? ""}` + : "detail" in error + ? error.detail + : error.message ?? "", } } } diff --git a/packages/medusa/src/interfaces/payment-processor.ts b/packages/medusa/src/interfaces/payment-processor.ts index 97ac9771e4a13..fd6cb9ecc7886 100644 --- a/packages/medusa/src/interfaces/payment-processor.ts +++ b/packages/medusa/src/interfaces/payment-processor.ts @@ -777,5 +777,5 @@ export abstract class AbstractPaymentProcessor implements PaymentProcessor { export function isPaymentProcessorError( obj: any ): obj is PaymentProcessorError { - return obj && typeof obj === "object" && (obj.error || obj.code || obj.detail) + return obj && typeof obj === "object" && obj.error && obj.code && obj.detail }