feat(medusa): Support deleting prices from a price list by product or variant (#1555)

This commit is contained in:
Adrien de Peretti
2022-05-30 09:41:57 +02:00
committed by GitHub
parent ad9cfedf04
commit fa031fd28b
16 changed files with 602 additions and 78 deletions
@@ -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)
})
})
}) })
@@ -112,5 +112,5 @@ export const simpleProductFactory = async (
await simpleProductVariantFactory(connection, factoryData) await simpleProductVariantFactory(connection, factoryData)
} }
return manager.findOne(Product, { id: prodId }, { relations: ["tags"] }) return manager.findOne(Product, { id: prodId }, { relations: ["tags", "variants", "variants.prices"] })
} }
@@ -94,6 +94,24 @@ class AdminPriceListResource extends BaseResource {
const path = `/admin/price-lists/${id}/prices/batch` const path = `/admin/price-lists/${id}/prices/batch`
return this.client.request("DELETE", path, payload, {}, customHeaders) return this.client.request("DELETE", path, payload, {}, customHeaders)
} }
deleteProductPrices(
priceListId: string,
productId: string,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminPriceListDeleteBatchRes> {
const path = `/admin/price-lists/${priceListId}/products/${productId}/prices`
return this.client.request("DELETE", path, {}, {}, customHeaders)
}
deleteVariantPrices(
priceListId: string,
variantId: string,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminPriceListDeleteBatchRes> {
const path = `/admin/price-lists/${priceListId}/variants/${variantId}/prices`
return this.client.request("DELETE", path, {}, {}, customHeaders)
}
} }
export default AdminPriceListResource export default AdminPriceListResource
@@ -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) => { rest.post("/admin/return-reasons/", (req, res, ctx) => {
const body = req.body as Record<string, any> const body = req.body as Record<string, any>
return res( return res(
@@ -6,12 +6,16 @@ import {
AdminDeletePriceListPricesPricesReq, AdminDeletePriceListPricesPricesReq,
AdminPriceListDeleteRes, AdminPriceListDeleteRes,
AdminPriceListDeleteBatchRes, AdminPriceListDeleteBatchRes,
AdminPriceListDeleteProductPricesRes,
AdminPriceListDeleteVariantPricesRes,
} from "@medusajs/medusa" } from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js" import { Response } from "@medusajs/medusa-js"
import { useMutation, UseMutationOptions, useQueryClient } from "react-query" import { useMutation, UseMutationOptions, useQueryClient } from "react-query"
import { useMedusa } from "../../../contexts/medusa" import { useMedusa } from "../../../contexts/medusa"
import { buildOptions } from "../../utils/buildOptions" import { buildOptions } from "../../utils/buildOptions"
import { adminPriceListKeys } from "./queries" import { adminPriceListKeys } from "./queries"
import { adminProductKeys } from "../products"
import { adminVariantKeys } from "../variants"
export const useAdminCreatePriceList = ( export const useAdminCreatePriceList = (
options?: UseMutationOptions< options?: UseMutationOptions<
@@ -118,3 +122,53 @@ export const useAdminDeletePriceListPrices = (
) )
) )
} }
export const useAdminDeletePriceListProductPrices = (
id: string,
productId: string,
options?: UseMutationOptions<
Response<AdminPriceListDeleteProductPricesRes>,
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<AdminPriceListDeleteVariantPricesRes>,
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
)
)
}
@@ -21,7 +21,7 @@ export const adminPriceListKeys = {
"products" as const, "products" as const,
{ ...(query || {}) }, { ...(query || {}) },
] as const ] as const
}, }
} }
type PriceListQueryKeys = typeof adminPriceListKeys type PriceListQueryKeys = typeof adminPriceListKeys
@@ -3,6 +3,8 @@ import {
useAdminCreatePriceListPrices, useAdminCreatePriceListPrices,
useAdminDeletePriceList, useAdminDeletePriceList,
useAdminDeletePriceListPrices, useAdminDeletePriceListPrices,
useAdminDeletePriceListProductPrices,
useAdminDeletePriceListVariantPrices,
useAdminUpdatePriceList, useAdminUpdatePriceList,
} from "../../../../src" } from "../../../../src"
import { renderHook } from "@testing-library/react-hooks" 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
}))
})
})
@@ -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,
})
}
@@ -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,
})
}
@@ -22,6 +22,15 @@ export default (app) => {
middlewares.wrap(require("./list-price-list-products").default) 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("/", middlewares.wrap(require("./create-price-list").default))
route.post("/:id", middlewares.wrap(require("./update-price-list").default)) route.post("/:id", middlewares.wrap(require("./update-price-list").default))
@@ -68,6 +77,9 @@ export type AdminPriceListDeleteBatchRes = {
object: string object: string
} }
export type AdminPriceListDeleteProductPricesRes = AdminPriceListDeleteBatchRes
export type AdminPriceListDeleteVariantPricesRes = AdminPriceListDeleteBatchRes
export type AdminPriceListDeleteRes = DeleteResponse export type AdminPriceListDeleteRes = DeleteResponse
export type AdminPriceListsListRes = PaginatedResponse & { export type AdminPriceListsListRes = PaginatedResponse & {
@@ -8,7 +8,7 @@ import {
IsString, IsString,
ValidateNested, ValidateNested,
} from "class-validator" } from "class-validator"
import { Product } from "../../../../models/product" import { Product } from "../../../../models"
import { DateComparisonOperator } from "../../../../types/common" import { DateComparisonOperator } from "../../../../types/common"
import { validator } from "../../../../utils/validator" import { validator } from "../../../../utils/validator"
import { FilterableProductProps } from "../../../../types/product" import { FilterableProductProps } from "../../../../types/product"
@@ -18,7 +18,6 @@ import {
defaultAdminProductFields, defaultAdminProductFields,
defaultAdminProductRelations, defaultAdminProductRelations,
} from "../products" } from "../products"
import listAndCount from "../../../../controllers/products/admin-list-products"
import { MedusaError } from "medusa-core-utils" import { MedusaError } from "medusa-core-utils"
import { getListConfig } from "../../../../utils/get-query-config" import { getListConfig } from "../../../../utils/get-query-config"
import PriceListService from "../../../../services/price-list" import PriceListService from "../../../../services/price-list"
@@ -49,7 +49,7 @@ export default async (req, res) => {
expandFields = validated.expand.split(",") expandFields = validated.expand.split(",")
} }
const listConfig: FindConfig<PriceList> = { const listConfig: FindConfig<FilterablePriceListProps> = {
relations: expandFields, relations: expandFields,
skip: validated.offset, skip: validated.offset,
take: validated.limit, take: validated.limit,
@@ -65,7 +65,7 @@ export default async (req, res) => {
} }
} }
const filterableFields = omit(validated, [ const filterableFields: FilterablePriceListProps = omit(validated, [
"limit", "limit",
"offset", "offset",
"expand", "expand",
@@ -119,18 +119,24 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
public async findManyForVariantInPriceList( public async findManyForVariantInPriceList(
variant_id: string, variant_id: string,
price_list_id: string price_list_id: string,
requiresPriceList = false
): Promise<[MoneyAmount[], number]> { ): Promise<[MoneyAmount[], number]> {
const qb = this.createQueryBuilder("ma") const qb = this.createQueryBuilder("ma")
.leftJoinAndSelect("ma.price_list", "price_list") .leftJoinAndSelect("ma.price_list", "price_list")
.where("ma.variant_id = :variant_id", { variant_id }) .where("ma.variant_id = :variant_id", { variant_id })
.andWhere(
new Brackets((qb) => { const getAndWhere = (subQb) => {
qb.where("ma.price_list_id = :price_list_id", { const andWhere = subQb.where("ma.price_list_id = :price_list_id", {
price_list_id, price_list_id,
}).orWhere("ma.price_list_id IS NULL") })
}) if (!requiresPriceList) {
) andWhere.orWhere("ma.price_list_id IS NULL")
}
return andWhere
}
qb.andWhere(new Brackets(getAndWhere))
return await qb.getManyAndCount() return await qb.getManyAndCount()
} }
@@ -8,16 +8,18 @@ import {
} from "typeorm" } from "typeorm"
import { PriceList } from "../models/price-list" import { PriceList } from "../models/price-list"
import { CustomFindOptions, ExtendedFindConfig } from "../types/common" import { CustomFindOptions, ExtendedFindConfig } from "../types/common"
import { CustomerGroup } from "../models"
import { FilterablePriceListProps } from "../types/price-list"
type PriceListFindOptions = CustomFindOptions<PriceList, "status" | "type"> export type PriceListFindOptions = CustomFindOptions<PriceList, "status" | "type">
@EntityRepository(PriceList) @EntityRepository(PriceList)
export class PriceListRepository extends Repository<PriceList> { export class PriceListRepository extends Repository<PriceList> {
public async getFreeTextSearchResultsAndCount( public async getFreeTextSearchResultsAndCount(
q: string, q: string,
options: PriceListFindOptions = { where: {} }, options: PriceListFindOptions = { where: {} },
groups?: FindOperator<PriceList>, groups: FindOperator<string[]>,
relations: (keyof PriceList)[] = [] relations: string[] = []
): Promise<[PriceList[], number]> { ): Promise<[PriceList[], number]> {
options.where = options.where ?? {} options.where = options.where ?? {}
@@ -50,7 +52,7 @@ export class PriceListRepository extends Repository<PriceList> {
} }
public async findWithRelations( public async findWithRelations(
relations: (keyof PriceList)[] = [], relations: string[] = [],
idsOrOptionsWithoutRelations: idsOrOptionsWithoutRelations:
| Omit<FindManyOptions<PriceList>, "relations"> | Omit<FindManyOptions<PriceList>, "relations">
| string[] = {} | string[] = {}
@@ -97,8 +99,8 @@ export class PriceListRepository extends Repository<PriceList> {
} }
async listAndCount( async listAndCount(
query: ExtendedFindConfig<PriceList>, query: ExtendedFindConfig<FilterablePriceListProps>,
groups?: FindOperator<PriceList> groups: FindOperator<string[]>
): Promise<[PriceList[], number]> { ): Promise<[PriceList[], number]> {
const qb = this.createQueryBuilder("price_list") const qb = this.createQueryBuilder("price_list")
.where(query.where) .where(query.where)
+158 -56
View File
@@ -1,13 +1,13 @@
import { MedusaError } from "medusa-core-utils" import { MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces" import { EntityManager, FindOperator } from "typeorm"
import { EntityManager } from "typeorm"
import { CustomerGroupService } from "." import { CustomerGroupService } from "."
import { Product } from "../models" import { CustomerGroup, PriceList, Product, ProductVariant } from "../models"
import { CustomerGroup } from "../models/customer-group"
import { PriceList } from "../models/price-list"
import { MoneyAmountRepository } from "../repositories/money-amount" import { MoneyAmountRepository } from "../repositories/money-amount"
import { PriceListRepository } from "../repositories/price-list" import {
import { FindConfig } from "../types/common" PriceListFindOptions,
PriceListRepository,
} from "../repositories/price-list"
import { FindConfig, Selector } from "../types/common"
import { import {
CreatePriceListInput, CreatePriceListInput,
FilterablePriceListProps, FilterablePriceListProps,
@@ -18,12 +18,18 @@ import {
import { formatException } from "../utils/exception-formatter" import { formatException } from "../utils/exception-formatter"
import ProductService from "./product" import ProductService from "./product"
import RegionService from "./region" 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 = { type PriceListConstructorProps = {
manager: EntityManager manager: EntityManager
customerGroupService: CustomerGroupService customerGroupService: CustomerGroupService
regionService: RegionService regionService: RegionService
productService: ProductService productService: ProductService
productVariantService: ProductVariantService
priceListRepository: typeof PriceListRepository priceListRepository: typeof PriceListRepository
moneyAmountRepository: typeof MoneyAmountRepository moneyAmountRepository: typeof MoneyAmountRepository
} }
@@ -32,50 +38,38 @@ type PriceListConstructorProps = {
* Provides layer to manipulate product tags. * Provides layer to manipulate product tags.
* @extends BaseService * @extends BaseService
*/ */
class PriceListService extends BaseService { class PriceListService extends TransactionBaseService<PriceListService> {
private manager_: EntityManager protected manager_: EntityManager
private customerGroupService_: CustomerGroupService protected transactionManager_: EntityManager | undefined
private regionService_: RegionService
private productService_: ProductService protected readonly customerGroupService_: CustomerGroupService
private priceListRepo_: typeof PriceListRepository protected readonly regionService_: RegionService
private moneyAmountRepo_: typeof MoneyAmountRepository protected readonly productService_: ProductService
protected readonly variantService_: ProductVariantService
protected readonly priceListRepo_: typeof PriceListRepository
protected readonly moneyAmountRepo_: typeof MoneyAmountRepository
constructor({ constructor({
manager, manager,
customerGroupService, customerGroupService,
regionService, regionService,
productService, productService,
productVariantService,
priceListRepository, priceListRepository,
moneyAmountRepository, moneyAmountRepository,
}: PriceListConstructorProps) { }: PriceListConstructorProps) {
super() // eslint-disable-next-line prefer-rest-params
super(arguments[0])
this.manager_ = manager this.manager_ = manager
this.customerGroupService_ = customerGroupService this.customerGroupService_ = customerGroupService
this.productService_ = productService this.productService_ = productService
this.variantService_ = productVariantService
this.regionService_ = regionService this.regionService_ = regionService
this.priceListRepo_ = priceListRepository this.priceListRepo_ = priceListRepository
this.moneyAmountRepo_ = moneyAmountRepository 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. * Retrieves a product tag by id.
* @param {string} priceListId - the id of the product tag to retrieve * @param {string} priceListId - the id of the product tag to retrieve
@@ -88,7 +82,7 @@ class PriceListService extends BaseService {
): Promise<PriceList> { ): Promise<PriceList> {
const priceListRepo = this.manager_.getCustomRepository(this.priceListRepo_) 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) const priceList = await priceListRepo.findOne(query)
if (!priceList) { if (!priceList) {
@@ -167,11 +161,9 @@ class PriceListService extends BaseService {
await this.upsertCustomerGroups_(id, customer_groups) await this.upsertCustomerGroups_(id, customer_groups)
} }
const result = await this.retrieve(id, { return await this.retrieve(id, {
relations: ["prices", "customer_groups"], relations: ["prices", "customer_groups"],
}) })
return result
}) })
} }
@@ -195,11 +187,9 @@ class PriceListService extends BaseService {
const prices_ = await this.addCurrencyFromRegion(prices) const prices_ = await this.addCurrencyFromRegion(prices)
await moneyAmountRepo.addPriceListPrices(priceList.id, prices_, replace) await moneyAmountRepo.addPriceListPrices(priceList.id, prices_, replace)
const result = await this.retrieve(priceList.id, { return await this.retrieve(priceList.id, {
relations: ["prices"], relations: ["prices"],
}) })
return result
}) })
} }
@@ -216,8 +206,6 @@ class PriceListService extends BaseService {
const priceList = await this.retrieve(id, { select: ["id"] }) const priceList = await this.retrieve(id, { select: ["id"] })
await moneyAmountRepo.deletePriceListPrices(priceList.id, priceIds) await moneyAmountRepo.deletePriceListPrices(priceList.id, priceIds)
return Promise.resolve()
}) })
} }
@@ -249,14 +237,18 @@ class PriceListService extends BaseService {
*/ */
async list( async list(
selector: FilterablePriceListProps = {}, selector: FilterablePriceListProps = {},
config: FindConfig<PriceList> = { skip: 0, take: 20 } config: FindConfig<FilterablePriceListProps> = { skip: 0, take: 20 }
): Promise<PriceList[]> { ): Promise<PriceList[]> {
return await this.atomicPhase_(async (manager: EntityManager) => { return await this.atomicPhase_(async (manager: EntityManager) => {
const priceListRepo = manager.getCustomRepository(this.priceListRepo_) const priceListRepo = manager.getCustomRepository(this.priceListRepo_)
const query = this.buildQuery_(selector, config) const { q, ...priceListSelector } = selector
const query = buildQuery<FilterablePriceListProps>(
priceListSelector,
config
)
const groups = query.where.customer_groups const groups = query.where.customer_groups as FindOperator<string[]>
query.where.customer_groups = undefined query.where.customer_groups = undefined
const [priceLists] = await priceListRepo.listAndCount(query, groups) const [priceLists] = await priceListRepo.listAndCount(query, groups)
@@ -273,21 +265,26 @@ class PriceListService extends BaseService {
*/ */
async listAndCount( async listAndCount(
selector: FilterablePriceListProps = {}, selector: FilterablePriceListProps = {},
config: FindConfig<PriceList> = { skip: 0, take: 20 } config: FindConfig<FilterablePriceListProps> = {
skip: 0,
take: 20,
}
): Promise<[PriceList[], number]> { ): Promise<[PriceList[], number]> {
return await this.atomicPhase_(async (manager: EntityManager) => { return await this.atomicPhase_(async (manager: EntityManager) => {
const priceListRepo = manager.getCustomRepository(this.priceListRepo_) const priceListRepo = manager.getCustomRepository(this.priceListRepo_)
const q = selector.q const { q, ...priceListSelector } = selector
const { relations, ...query } = this.buildQuery_(selector, config) const { relations, ...query } = buildQuery<FilterablePriceListProps>(
priceListSelector,
config
)
const groups = query.where.customer_groups const groups = query.where.customer_groups as FindOperator<string[]>
delete query.where.customer_groups delete query.where.customer_groups
if (q) { if (q) {
delete query.where.q
return await priceListRepo.getFreeTextSearchResultsAndCount( return await priceListRepo.getFreeTextSearchResultsAndCount(
q, q,
query, query as PriceListFindOptions,
groups, groups,
relations relations
) )
@@ -296,7 +293,7 @@ class PriceListService extends BaseService {
}) })
} }
async upsertCustomerGroups_( protected async upsertCustomerGroups_(
priceListId: string, priceListId: string,
customerGroups: { id: string }[] customerGroups: { id: string }[]
): Promise<void> { ): Promise<void> {
@@ -317,12 +314,13 @@ class PriceListService extends BaseService {
async listProducts( async listProducts(
priceListId: string, priceListId: string,
selector = {}, selector: FilterableProductProps | Selector<Product> = {},
config: FindConfig<Product> = { config: FindConfig<Product> = {
relations: [], relations: [],
skip: 0, skip: 0,
take: 20, take: 20,
} },
requiresPriceList = false
): Promise<[Product[], number]> { ): Promise<[Product[], number]> {
return await this.atomicPhase_(async (manager: EntityManager) => { return await this.atomicPhase_(async (manager: EntityManager) => {
const [products, count] = await this.productService_.listAndCount( const [products, count] = await this.productService_.listAndCount(
@@ -340,7 +338,8 @@ class PriceListService extends BaseService {
const [prices] = const [prices] =
await moneyAmountRepo.findManyForVariantInPriceList( await moneyAmountRepo.findManyForVariantInPriceList(
v.id, v.id,
priceListId priceListId,
requiresPriceList
) )
return { return {
@@ -359,6 +358,109 @@ class PriceListService extends BaseService {
}) })
} }
async listVariants(
priceListId: string,
selector: FilterableProductVariantProps = {},
config: FindConfig<ProductVariant> = {
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. * Add `currency_code` to an MA record if `region_id`is passed.
* @param prices - a list of PriceListPrice(Create/Update)Input records * @param prices - a list of PriceListPrice(Create/Update)Input records
+9 -2
View File
@@ -7,18 +7,24 @@ import {
IsString, IsString,
} from "class-validator" } from "class-validator"
import "reflect-metadata" import "reflect-metadata"
import { FindManyOptions, OrderByCondition } from "typeorm" import { FindManyOptions, FindOperator, OrderByCondition } from "typeorm"
import { transformDate } from "../utils/validators/date-transform" import { transformDate } from "../utils/validators/date-transform"
export type PartialPick<T, K extends keyof T> = { export type PartialPick<T, K extends keyof T> = {
[P in K]?: T[P] [P in K]?: T[P]
} }
export type Writable<T> = { -readonly [key in keyof T]: T[key] } export type Writable<T> = {
-readonly [key in keyof T]:
| T[key]
| FindOperator<T[key][]>
| FindOperator<string[]>
}
export type ExtendedFindConfig<TEntity> = FindConfig<TEntity> & { export type ExtendedFindConfig<TEntity> = FindConfig<TEntity> & {
where: Partial<Writable<TEntity>> where: Partial<Writable<TEntity>>
withDeleted?: boolean withDeleted?: boolean
relations?: string[]
} }
export type Selector<TEntity> = { export type Selector<TEntity> = {
@@ -28,6 +34,7 @@ export type Selector<TEntity> = {
| DateComparisonOperator | DateComparisonOperator
| StringComparisonOperator | StringComparisonOperator
| NumericalComparisonOperator | NumericalComparisonOperator
| FindOperator<TEntity[key][] | string[]>
} }
export type TotalField = export type TotalField =