From fa031fd28be8b12ff38eaec6e56c373324e0beed Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Mon, 30 May 2022 09:41:57 +0200 Subject: [PATCH] feat(medusa): Support deleting prices from a price list by product or variant (#1555) --- .../api/__tests__/admin/price-list.js | 154 +++++++++++++ .../api/factories/simple-product-factory.ts | 2 +- .../src/resources/admin/price-lists.ts | 18 ++ packages/medusa-react/mocks/handlers/admin.ts | 22 ++ .../src/hooks/admin/price-lists/mutations.ts | 54 +++++ .../src/hooks/admin/price-lists/queries.ts | 2 +- .../hooks/admin/price-lists/mutations.test.ts | 52 +++++ .../price-lists/delete-product-prices.ts | 48 ++++ .../price-lists/delete-variant-prices.ts | 48 ++++ .../src/api/routes/admin/price-lists/index.ts | 12 + .../price-lists/list-price-list-products.ts | 3 +- .../admin/price-lists/list-price-lists.ts | 4 +- .../medusa/src/repositories/money-amount.ts | 22 +- .../medusa/src/repositories/price-list.ts | 14 +- packages/medusa/src/services/price-list.ts | 214 +++++++++++++----- packages/medusa/src/types/common.ts | 11 +- 16 files changed, 602 insertions(+), 78 deletions(-) create mode 100644 packages/medusa/src/api/routes/admin/price-lists/delete-product-prices.ts create mode 100644 packages/medusa/src/api/routes/admin/price-lists/delete-variant-prices.ts diff --git a/integration-tests/api/__tests__/admin/price-list.js b/integration-tests/api/__tests__/admin/price-list.js index 6dd4a8face..32f2f71fc4 100644 --- a/integration-tests/api/__tests__/admin/price-list.js +++ b/integration-tests/api/__tests__/admin/price-list.js @@ -1207,4 +1207,158 @@ describe("/admin/price-lists", () => { ]) }) }) + + describe("delete prices from price list related to the specified product or variant", () => { + let product1, product2 + + function getCustomPriceIdFromVariant(variantId, index) { + return "ma_" + index + "_" + variantId + } + + beforeEach(async () => { + try { + await adminSeeder(dbConnection) + + product1 = await simpleProductFactory( + dbConnection, + { + id: "test-prod-1", + title: "some product", + variants: [ + { + id: `simple-test-variant-${Math.random() * 1000}`, + title: "Test", + prices: [{ currency: "usd", amount: 100 }], + }, + { + id: `simple-test-variant-${Math.random() * 1000}`, + title: "Test 2", + prices: [{ currency: "usd", amount: 200 }], + } + ] + }, + 1 + ) + + product2 = await simpleProductFactory( + dbConnection, + { + id: "test-prod-2", + title: "some product 2" + }, + 2 + ) + + await simplePriceListFactory(dbConnection, { + id: "test-list", + customer_groups: ["test-group"], + prices: [ + ...product1.variants.map((variant, i) => ( + { + id: getCustomPriceIdFromVariant(variant.id, i), + variant_id: variant.id, + currency_code: "usd", + amount: (i + 1) * 150 + } + )), + ...product2.variants.map((variant, i) => ( + { + id: getCustomPriceIdFromVariant(variant.id, i), + variant_id: variant.id, + currency_code: "usd", + amount: (i + 1) * 150 + } + )), + ] + }) + } catch (err) { + console.log(err) + throw err + } + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it('should delete all the prices that are part of the price list for the specified product', async () => { + const api = useApi() + + response = await api + .get("/admin/price-lists/test-list", { + headers: { + Authorization: "Bearer test_token", + } + }) + + expect(response.status).toBe(200) + expect(response.data.price_list.prices.length).toBe(3) + + let response = await api + .delete(`/admin/price-lists/test-list/products/${product1.id}/prices`, { + headers: { + Authorization: "Bearer test_token", + } + }) + + expect(response.status).toBe(200) + expect(response.data).toEqual({ + ids: product1.variants.map((variant, i) => { + return getCustomPriceIdFromVariant(variant.id, i) + }), + object: "money-amount", + deleted: true, + }) + + response = await api + .get("/admin/price-lists/test-list", { + headers: { + Authorization: "Bearer test_token", + } + }) + + expect(response.status).toBe(200) + expect(response.data.price_list.prices.length).toBe(1) + }) + + it('should delete all the prices that are part of the price list for the specified variant', async () => { + const api = useApi() + + response = await api + .get("/admin/price-lists/test-list", { + headers: { + Authorization: "Bearer test_token", + } + }) + + expect(response.status).toBe(200) + expect(response.data.price_list.prices.length).toBe(3) + + const variant = product2.variants[0] + let response = await api + .delete(`/admin/price-lists/test-list/variants/${variant.id}/prices`, { + headers: { + Authorization: "Bearer test_token", + } + }) + + expect(response.status).toBe(200) + expect(response.data).toEqual({ + ids: [getCustomPriceIdFromVariant(variant.id, 0)], + object: "money-amount", + deleted: true, + }) + + response = await api + .get("/admin/price-lists/test-list", { + headers: { + Authorization: "Bearer test_token", + } + }) + + expect(response.status).toBe(200) + expect(response.data.price_list.prices.length).toBe(2) + }) + }) }) diff --git a/integration-tests/api/factories/simple-product-factory.ts b/integration-tests/api/factories/simple-product-factory.ts index d01ba8195f..a4670d4ecf 100644 --- a/integration-tests/api/factories/simple-product-factory.ts +++ b/integration-tests/api/factories/simple-product-factory.ts @@ -112,5 +112,5 @@ export const simpleProductFactory = async ( await simpleProductVariantFactory(connection, factoryData) } - return manager.findOne(Product, { id: prodId }, { relations: ["tags"] }) + return manager.findOne(Product, { id: prodId }, { relations: ["tags", "variants", "variants.prices"] }) } diff --git a/packages/medusa-js/src/resources/admin/price-lists.ts b/packages/medusa-js/src/resources/admin/price-lists.ts index fce3dadd0d..633e92492b 100644 --- a/packages/medusa-js/src/resources/admin/price-lists.ts +++ b/packages/medusa-js/src/resources/admin/price-lists.ts @@ -94,6 +94,24 @@ class AdminPriceListResource extends BaseResource { const path = `/admin/price-lists/${id}/prices/batch` return this.client.request("DELETE", path, payload, {}, customHeaders) } + + deleteProductPrices( + priceListId: string, + productId: string, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/price-lists/${priceListId}/products/${productId}/prices` + return this.client.request("DELETE", path, {}, {}, customHeaders) + } + + deleteVariantPrices( + priceListId: string, + variantId: string, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/price-lists/${priceListId}/variants/${variantId}/prices` + return this.client.request("DELETE", path, {}, {}, customHeaders) + } } export default AdminPriceListResource diff --git a/packages/medusa-react/mocks/handlers/admin.ts b/packages/medusa-react/mocks/handlers/admin.ts index 3e87c2dd69..fbaf3fb107 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -253,6 +253,28 @@ export const adminHandlers = [ ) }), + rest.delete("/admin/price-lists/:id/products/:product_id/prices", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + ids: [], + object: "money-amount", + deleted: true, + }) + ) + }), + + rest.delete("/admin/price-lists/:id/variants/:variant_id/prices", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + ids: [], + object: "money-amount", + deleted: true, + }) + ) + }), + rest.post("/admin/return-reasons/", (req, res, ctx) => { const body = req.body as Record return res( diff --git a/packages/medusa-react/src/hooks/admin/price-lists/mutations.ts b/packages/medusa-react/src/hooks/admin/price-lists/mutations.ts index 0a7556987b..faaffdc4b1 100644 --- a/packages/medusa-react/src/hooks/admin/price-lists/mutations.ts +++ b/packages/medusa-react/src/hooks/admin/price-lists/mutations.ts @@ -6,12 +6,16 @@ import { AdminDeletePriceListPricesPricesReq, AdminPriceListDeleteRes, AdminPriceListDeleteBatchRes, + AdminPriceListDeleteProductPricesRes, + AdminPriceListDeleteVariantPricesRes, } from "@medusajs/medusa" import { Response } from "@medusajs/medusa-js" import { useMutation, UseMutationOptions, useQueryClient } from "react-query" import { useMedusa } from "../../../contexts/medusa" import { buildOptions } from "../../utils/buildOptions" import { adminPriceListKeys } from "./queries" +import { adminProductKeys } from "../products" +import { adminVariantKeys } from "../variants" export const useAdminCreatePriceList = ( options?: UseMutationOptions< @@ -118,3 +122,53 @@ export const useAdminDeletePriceListPrices = ( ) ) } + +export const useAdminDeletePriceListProductPrices = ( + id: string, + productId: string, + options?: UseMutationOptions< + Response, + Error + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + () => client.admin.priceLists.deleteProductPrices(id, productId), + buildOptions( + queryClient, + [ + adminPriceListKeys.detail(id), + adminPriceListKeys.lists(), + adminProductKeys.detail(productId) + ], + options + ) + ) +} + +export const useAdminDeletePriceListVariantPrices = ( + id: string, + variantId: string, + options?: UseMutationOptions< + Response, + Error + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + () => client.admin.priceLists.deleteVariantPrices(id, variantId), + buildOptions( + queryClient, + [ + adminPriceListKeys.detail(id), + adminPriceListKeys.lists(), + adminVariantKeys.detail(variantId) + ], + options + ) + ) +} diff --git a/packages/medusa-react/src/hooks/admin/price-lists/queries.ts b/packages/medusa-react/src/hooks/admin/price-lists/queries.ts index c724825e0d..51f0417a5a 100644 --- a/packages/medusa-react/src/hooks/admin/price-lists/queries.ts +++ b/packages/medusa-react/src/hooks/admin/price-lists/queries.ts @@ -21,7 +21,7 @@ export const adminPriceListKeys = { "products" as const, { ...(query || {}) }, ] as const - }, + } } type PriceListQueryKeys = typeof adminPriceListKeys diff --git a/packages/medusa-react/test/hooks/admin/price-lists/mutations.test.ts b/packages/medusa-react/test/hooks/admin/price-lists/mutations.test.ts index f2bdea588b..42fc8d2a1b 100644 --- a/packages/medusa-react/test/hooks/admin/price-lists/mutations.test.ts +++ b/packages/medusa-react/test/hooks/admin/price-lists/mutations.test.ts @@ -3,6 +3,8 @@ import { useAdminCreatePriceListPrices, useAdminDeletePriceList, useAdminDeletePriceListPrices, + useAdminDeletePriceListProductPrices, + useAdminDeletePriceListVariantPrices, useAdminUpdatePriceList, } from "../../../../src" import { renderHook } from "@testing-library/react-hooks" @@ -137,3 +139,53 @@ describe("useAdminDeletePriceList hook", () => { ) }) }) + +describe("useAdminDeletePriceListProductPrices hook", () => { + test("should delete prices from a price list for all the variants related to the specified product", async () => { + const { result, waitFor } = renderHook( + () => useAdminDeletePriceListProductPrices( + fixtures.get("price_list").id, + fixtures.get("product").id + ), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate() + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data).toEqual(expect.objectContaining({ + ids: [], + object: "money-amount", + deleted: true + })) + }) +}) + +describe("useAdminDeletePriceListVariantPrices hook", () => { + test("should delete prices from a price list for the specified variant", async () => { + const { result, waitFor } = renderHook( + () => useAdminDeletePriceListVariantPrices( + fixtures.get("price_list").id, + fixtures.get("product_variant").id + ), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate() + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data).toEqual(expect.objectContaining({ + ids: [], + object: "money-amount", + deleted: true + })) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/price-lists/delete-product-prices.ts b/packages/medusa/src/api/routes/admin/price-lists/delete-product-prices.ts new file mode 100644 index 0000000000..78b628ca42 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/price-lists/delete-product-prices.ts @@ -0,0 +1,48 @@ +import PriceListService from "../../../../services/price-list" + +/** + * @oas [delete] /price-lists/{id}/products/{product_id}/prices + * operationId: "DeletePriceListsPriceListProductsProductPrices" + * summary: "Delete all the prices related to a specific product in a price list" + * description: "Delete all the prices related to a specific product in a price list" + * x-authenticated: true + * parameters: + * - (path) id=* {string} The id of the Price List that the Money Amounts that will be deleted belongs to. + * - (path) product_id=* {string} The id of the product from which the money amount will be deleted. + * tags: + * - Price List + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * ids: + * type: number + * description: The price ids that have been deleted. + * count: + * type: number + * description: The number of prices that have been deleted. + * object: + * type: string + * description: The type of the object that was deleted. + * deleted: + * type: boolean + */ +export default async (req, res) => { + const { id, product_id } = req.params + + const priceListService: PriceListService = + req.scope.resolve("priceListService") + + const [deletedPriceIds] = await priceListService.deleteProductPrices(id, [ + product_id, + ]) + + return res.json({ + ids: deletedPriceIds, + object: "money-amount", + deleted: true, + }) +} diff --git a/packages/medusa/src/api/routes/admin/price-lists/delete-variant-prices.ts b/packages/medusa/src/api/routes/admin/price-lists/delete-variant-prices.ts new file mode 100644 index 0000000000..b34a9858cf --- /dev/null +++ b/packages/medusa/src/api/routes/admin/price-lists/delete-variant-prices.ts @@ -0,0 +1,48 @@ +import PriceListService from "../../../../services/price-list" + +/** + * @oas [delete] /price-lists/{id}/variants/{variant_id}/prices + * operationId: "DeletePriceListsPriceListVariantsVariantPrices" + * summary: "Delete all the prices related to a specific variant in a price list" + * description: "Delete all the prices related to a specific variant in a price list" + * x-authenticated: true + * parameters: + * - (path) id=* {string} The id of the Price List that the Money Amounts that will be deleted belongs to. + * - (path) variant_id=* {string} The id of the variant from which the money amount will be deleted. + * tags: + * - Price List + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * ids: + * type: number + * description: The price ids that have been deleted. + * count: + * type: number + * description: The number of prices that have been deleted. + * object: + * type: string + * description: The type of the object that was deleted. + * deleted: + * type: boolean + */ +export default async (req, res) => { + const { id, variant_id } = req.params + + const priceListService: PriceListService = + req.scope.resolve("priceListService") + + const [deletedPriceIds] = await priceListService.deleteVariantPrices(id, [ + variant_id, + ]) + + return res.json({ + ids: deletedPriceIds, + object: "money-amount", + deleted: true, + }) +} diff --git a/packages/medusa/src/api/routes/admin/price-lists/index.ts b/packages/medusa/src/api/routes/admin/price-lists/index.ts index 4a5fbbddb0..ef10783591 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/index.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/index.ts @@ -22,6 +22,15 @@ export default (app) => { middlewares.wrap(require("./list-price-list-products").default) ) + route.delete( + "/:id/products/:product_id/prices", + middlewares.wrap(require("./delete-product-prices").default) + ) + route.delete( + "/:id/variants/:variant_id/prices", + middlewares.wrap(require("./delete-variant-prices").default) + ) + route.post("/", middlewares.wrap(require("./create-price-list").default)) route.post("/:id", middlewares.wrap(require("./update-price-list").default)) @@ -68,6 +77,9 @@ export type AdminPriceListDeleteBatchRes = { object: string } +export type AdminPriceListDeleteProductPricesRes = AdminPriceListDeleteBatchRes +export type AdminPriceListDeleteVariantPricesRes = AdminPriceListDeleteBatchRes + export type AdminPriceListDeleteRes = DeleteResponse export type AdminPriceListsListRes = PaginatedResponse & { diff --git a/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts b/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts index 21a3f73b66..6b4c6de109 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts @@ -8,7 +8,7 @@ import { IsString, ValidateNested, } from "class-validator" -import { Product } from "../../../../models/product" +import { Product } from "../../../../models" import { DateComparisonOperator } from "../../../../types/common" import { validator } from "../../../../utils/validator" import { FilterableProductProps } from "../../../../types/product" @@ -18,7 +18,6 @@ import { defaultAdminProductFields, defaultAdminProductRelations, } from "../products" -import listAndCount from "../../../../controllers/products/admin-list-products" import { MedusaError } from "medusa-core-utils" import { getListConfig } from "../../../../utils/get-query-config" import PriceListService from "../../../../services/price-list" diff --git a/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts b/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts index 3a83b10fb0..61a9ee2aec 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts @@ -49,7 +49,7 @@ export default async (req, res) => { expandFields = validated.expand.split(",") } - const listConfig: FindConfig = { + const listConfig: FindConfig = { relations: expandFields, skip: validated.offset, take: validated.limit, @@ -65,7 +65,7 @@ export default async (req, res) => { } } - const filterableFields = omit(validated, [ + const filterableFields: FilterablePriceListProps = omit(validated, [ "limit", "offset", "expand", diff --git a/packages/medusa/src/repositories/money-amount.ts b/packages/medusa/src/repositories/money-amount.ts index 118f143968..0a841e10e5 100644 --- a/packages/medusa/src/repositories/money-amount.ts +++ b/packages/medusa/src/repositories/money-amount.ts @@ -119,18 +119,24 @@ export class MoneyAmountRepository extends Repository { public async findManyForVariantInPriceList( variant_id: string, - price_list_id: string + price_list_id: string, + requiresPriceList = false ): Promise<[MoneyAmount[], number]> { const qb = this.createQueryBuilder("ma") .leftJoinAndSelect("ma.price_list", "price_list") .where("ma.variant_id = :variant_id", { variant_id }) - .andWhere( - new Brackets((qb) => { - qb.where("ma.price_list_id = :price_list_id", { - price_list_id, - }).orWhere("ma.price_list_id IS NULL") - }) - ) + + const getAndWhere = (subQb) => { + const andWhere = subQb.where("ma.price_list_id = :price_list_id", { + price_list_id, + }) + if (!requiresPriceList) { + andWhere.orWhere("ma.price_list_id IS NULL") + } + return andWhere + } + + qb.andWhere(new Brackets(getAndWhere)) return await qb.getManyAndCount() } diff --git a/packages/medusa/src/repositories/price-list.ts b/packages/medusa/src/repositories/price-list.ts index 37b3b9e468..17badaef2d 100644 --- a/packages/medusa/src/repositories/price-list.ts +++ b/packages/medusa/src/repositories/price-list.ts @@ -8,16 +8,18 @@ import { } from "typeorm" import { PriceList } from "../models/price-list" import { CustomFindOptions, ExtendedFindConfig } from "../types/common" +import { CustomerGroup } from "../models" +import { FilterablePriceListProps } from "../types/price-list" -type PriceListFindOptions = CustomFindOptions +export type PriceListFindOptions = CustomFindOptions @EntityRepository(PriceList) export class PriceListRepository extends Repository { public async getFreeTextSearchResultsAndCount( q: string, options: PriceListFindOptions = { where: {} }, - groups?: FindOperator, - relations: (keyof PriceList)[] = [] + groups: FindOperator, + relations: string[] = [] ): Promise<[PriceList[], number]> { options.where = options.where ?? {} @@ -50,7 +52,7 @@ export class PriceListRepository extends Repository { } public async findWithRelations( - relations: (keyof PriceList)[] = [], + relations: string[] = [], idsOrOptionsWithoutRelations: | Omit, "relations"> | string[] = {} @@ -97,8 +99,8 @@ export class PriceListRepository extends Repository { } async listAndCount( - query: ExtendedFindConfig, - groups?: FindOperator + query: ExtendedFindConfig, + groups: FindOperator ): Promise<[PriceList[], number]> { const qb = this.createQueryBuilder("price_list") .where(query.where) diff --git a/packages/medusa/src/services/price-list.ts b/packages/medusa/src/services/price-list.ts index 325ae55d38..bf9cd87985 100644 --- a/packages/medusa/src/services/price-list.ts +++ b/packages/medusa/src/services/price-list.ts @@ -1,13 +1,13 @@ import { MedusaError } from "medusa-core-utils" -import { BaseService } from "medusa-interfaces" -import { EntityManager } from "typeorm" +import { EntityManager, FindOperator } from "typeorm" import { CustomerGroupService } from "." -import { Product } from "../models" -import { CustomerGroup } from "../models/customer-group" -import { PriceList } from "../models/price-list" +import { CustomerGroup, PriceList, Product, ProductVariant } from "../models" import { MoneyAmountRepository } from "../repositories/money-amount" -import { PriceListRepository } from "../repositories/price-list" -import { FindConfig } from "../types/common" +import { + PriceListFindOptions, + PriceListRepository, +} from "../repositories/price-list" +import { FindConfig, Selector } from "../types/common" import { CreatePriceListInput, FilterablePriceListProps, @@ -18,12 +18,18 @@ import { import { formatException } from "../utils/exception-formatter" import ProductService from "./product" import RegionService from "./region" +import { TransactionBaseService } from "../interfaces" +import { buildQuery } from "../utils" +import { FilterableProductProps } from "../types/product" +import ProductVariantService from "./product-variant" +import { FilterableProductVariantProps } from "../types/product-variant" type PriceListConstructorProps = { manager: EntityManager customerGroupService: CustomerGroupService regionService: RegionService productService: ProductService + productVariantService: ProductVariantService priceListRepository: typeof PriceListRepository moneyAmountRepository: typeof MoneyAmountRepository } @@ -32,50 +38,38 @@ type PriceListConstructorProps = { * Provides layer to manipulate product tags. * @extends BaseService */ -class PriceListService extends BaseService { - private manager_: EntityManager - private customerGroupService_: CustomerGroupService - private regionService_: RegionService - private productService_: ProductService - private priceListRepo_: typeof PriceListRepository - private moneyAmountRepo_: typeof MoneyAmountRepository +class PriceListService extends TransactionBaseService { + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + + protected readonly customerGroupService_: CustomerGroupService + protected readonly regionService_: RegionService + protected readonly productService_: ProductService + protected readonly variantService_: ProductVariantService + protected readonly priceListRepo_: typeof PriceListRepository + protected readonly moneyAmountRepo_: typeof MoneyAmountRepository constructor({ manager, customerGroupService, regionService, productService, + productVariantService, priceListRepository, moneyAmountRepository, }: PriceListConstructorProps) { - super() + // eslint-disable-next-line prefer-rest-params + super(arguments[0]) + this.manager_ = manager this.customerGroupService_ = customerGroupService this.productService_ = productService + this.variantService_ = productVariantService this.regionService_ = regionService this.priceListRepo_ = priceListRepository this.moneyAmountRepo_ = moneyAmountRepository } - withTransaction(transactionManager: EntityManager): PriceListService { - if (!transactionManager) { - return this - } - - const cloned = new PriceListService({ - manager: transactionManager, - customerGroupService: this.customerGroupService_, - productService: this.productService_, - regionService: this.regionService_, - priceListRepository: this.priceListRepo_, - moneyAmountRepository: this.moneyAmountRepo_, - }) - - cloned.transactionManager_ = transactionManager - - return cloned - } - /** * Retrieves a product tag by id. * @param {string} priceListId - the id of the product tag to retrieve @@ -88,7 +82,7 @@ class PriceListService extends BaseService { ): Promise { const priceListRepo = this.manager_.getCustomRepository(this.priceListRepo_) - const query = this.buildQuery_({ id: priceListId }, config) + const query = buildQuery({ id: priceListId }, config) const priceList = await priceListRepo.findOne(query) if (!priceList) { @@ -167,11 +161,9 @@ class PriceListService extends BaseService { await this.upsertCustomerGroups_(id, customer_groups) } - const result = await this.retrieve(id, { + return await this.retrieve(id, { relations: ["prices", "customer_groups"], }) - - return result }) } @@ -195,11 +187,9 @@ class PriceListService extends BaseService { const prices_ = await this.addCurrencyFromRegion(prices) await moneyAmountRepo.addPriceListPrices(priceList.id, prices_, replace) - const result = await this.retrieve(priceList.id, { + return await this.retrieve(priceList.id, { relations: ["prices"], }) - - return result }) } @@ -216,8 +206,6 @@ class PriceListService extends BaseService { const priceList = await this.retrieve(id, { select: ["id"] }) await moneyAmountRepo.deletePriceListPrices(priceList.id, priceIds) - - return Promise.resolve() }) } @@ -249,14 +237,18 @@ class PriceListService extends BaseService { */ async list( selector: FilterablePriceListProps = {}, - config: FindConfig = { skip: 0, take: 20 } + config: FindConfig = { skip: 0, take: 20 } ): Promise { return await this.atomicPhase_(async (manager: EntityManager) => { const priceListRepo = manager.getCustomRepository(this.priceListRepo_) - const query = this.buildQuery_(selector, config) + const { q, ...priceListSelector } = selector + const query = buildQuery( + priceListSelector, + config + ) - const groups = query.where.customer_groups + const groups = query.where.customer_groups as FindOperator query.where.customer_groups = undefined const [priceLists] = await priceListRepo.listAndCount(query, groups) @@ -273,21 +265,26 @@ class PriceListService extends BaseService { */ async listAndCount( selector: FilterablePriceListProps = {}, - config: FindConfig = { skip: 0, take: 20 } + config: FindConfig = { + skip: 0, + take: 20, + } ): Promise<[PriceList[], number]> { return await this.atomicPhase_(async (manager: EntityManager) => { const priceListRepo = manager.getCustomRepository(this.priceListRepo_) - const q = selector.q - const { relations, ...query } = this.buildQuery_(selector, config) + const { q, ...priceListSelector } = selector + const { relations, ...query } = buildQuery( + priceListSelector, + config + ) - const groups = query.where.customer_groups + const groups = query.where.customer_groups as FindOperator delete query.where.customer_groups if (q) { - delete query.where.q return await priceListRepo.getFreeTextSearchResultsAndCount( q, - query, + query as PriceListFindOptions, groups, relations ) @@ -296,7 +293,7 @@ class PriceListService extends BaseService { }) } - async upsertCustomerGroups_( + protected async upsertCustomerGroups_( priceListId: string, customerGroups: { id: string }[] ): Promise { @@ -317,12 +314,13 @@ class PriceListService extends BaseService { async listProducts( priceListId: string, - selector = {}, + selector: FilterableProductProps | Selector = {}, config: FindConfig = { relations: [], skip: 0, take: 20, - } + }, + requiresPriceList = false ): Promise<[Product[], number]> { return await this.atomicPhase_(async (manager: EntityManager) => { const [products, count] = await this.productService_.listAndCount( @@ -340,7 +338,8 @@ class PriceListService extends BaseService { const [prices] = await moneyAmountRepo.findManyForVariantInPriceList( v.id, - priceListId + priceListId, + requiresPriceList ) return { @@ -359,6 +358,109 @@ class PriceListService extends BaseService { }) } + async listVariants( + priceListId: string, + selector: FilterableProductVariantProps = {}, + config: FindConfig = { + relations: [], + skip: 0, + take: 20, + }, + requiresPriceList = false + ): Promise<[ProductVariant[], number]> { + return await this.atomicPhase_(async (manager: EntityManager) => { + const [variants, count] = await this.variantService_.listAndCount( + selector, + config + ) + + const moneyAmountRepo = manager.getCustomRepository(this.moneyAmountRepo_) + + const variantsWithPrices = await Promise.all( + variants.map(async (variant) => { + const [prices] = await moneyAmountRepo.findManyForVariantInPriceList( + variant.id, + priceListId, + requiresPriceList + ) + + variant.prices = prices + return variant + }) + ) + + return [variantsWithPrices, count] + }) + } + + public async deleteProductPrices( + priceListId: string, + productIds: string[] + ): Promise<[string[], number]> { + return await this.atomicPhase_(async () => { + const [products, count] = await this.listProducts( + priceListId, + { + id: productIds, + }, + { + relations: ["variants"], + }, + true + ) + + if (count === 0) { + return [[], count] + } + + const priceIds = products + .map(({ variants }) => + variants + .map((variant) => variant.prices.map((price) => price.id)) + .flat() + ) + .flat() + + if (!priceIds.length) { + return [[], 0] + } + + await this.deletePrices(priceListId, priceIds) + return [priceIds, priceIds.length] + }) + } + + public async deleteVariantPrices( + priceListId: string, + variantIds: string[] + ): Promise<[string[], number]> { + return await this.atomicPhase_(async () => { + const [variants, count] = await this.listVariants( + priceListId, + { + id: variantIds, + }, + {}, + true + ) + + if (count === 0) { + return [[], count] + } + + const priceIds = variants + .map((variant) => variant.prices.map((price) => price.id)) + .flat() + + if (!priceIds.length) { + return [[], 0] + } + + await this.deletePrices(priceListId, priceIds) + return [priceIds, priceIds.length] + }) + } + /** * Add `currency_code` to an MA record if `region_id`is passed. * @param prices - a list of PriceListPrice(Create/Update)Input records diff --git a/packages/medusa/src/types/common.ts b/packages/medusa/src/types/common.ts index 4c903e4d84..c75d4de338 100644 --- a/packages/medusa/src/types/common.ts +++ b/packages/medusa/src/types/common.ts @@ -7,18 +7,24 @@ import { IsString, } from "class-validator" import "reflect-metadata" -import { FindManyOptions, OrderByCondition } from "typeorm" +import { FindManyOptions, FindOperator, OrderByCondition } from "typeorm" import { transformDate } from "../utils/validators/date-transform" export type PartialPick = { [P in K]?: T[P] } -export type Writable = { -readonly [key in keyof T]: T[key] } +export type Writable = { + -readonly [key in keyof T]: + | T[key] + | FindOperator + | FindOperator +} export type ExtendedFindConfig = FindConfig & { where: Partial> withDeleted?: boolean + relations?: string[] } export type Selector = { @@ -28,6 +34,7 @@ export type Selector = { | DateComparisonOperator | StringComparisonOperator | NumericalComparisonOperator + | FindOperator } export type TotalField =