fix: variant price update (#1093)

* fix: variant prices update + integration tests

* add: unit tests

* fix: rename variable

* add: integration tests

* fix: integration tests

* fix: test name

* fix: move db logic to repo layer + create upsert method

* fix: linting
This commit is contained in:
Zakaria El Asri
2022-02-22 20:23:11 +01:00
committed by GitHub
parent 1909d20e48
commit cb7b211c9b
8 changed files with 542 additions and 128 deletions
@@ -1,5 +1,58 @@
import { EntityRepository, Repository } from "typeorm"
import {
Brackets,
EntityRepository,
In,
IsNull,
Not,
Repository,
} from "typeorm"
import { MoneyAmount } from "../models/money-amount"
type Price = Partial<
Pick<
MoneyAmount,
"currency_code" | "region_id" | "sale_amount" | "currency_code"
>
> & { amount: number }
@EntityRepository(MoneyAmount)
export class MoneyAmountRepository extends Repository<MoneyAmount> { }
export class MoneyAmountRepository extends Repository<MoneyAmount> {
public async findVariantPricesNotIn(variantId: string, prices: Price[]) {
const pricesNotInPricesPayload = await this.createQueryBuilder()
.where({
variant_id: variantId,
})
.andWhere(
new Brackets((qb) => {
qb.where({
currency_code: Not(In(prices.map((p) => p.currency_code))),
}).orWhere({ region_id: Not(In(prices.map((p) => p.region_id))) })
})
)
.getMany()
return pricesNotInPricesPayload
}
public async upsertCurrencyPrice(variantId: string, price: Price) {
let moneyAmount = await this.findOne({
where: {
currency_code: price.currency_code,
variant_id: variantId,
region_id: IsNull(),
},
})
if (!moneyAmount) {
moneyAmount = this.create({
...price,
currency_code: price.currency_code?.toLowerCase(),
variant_id: variantId,
})
} else {
moneyAmount.amount = price.amount
moneyAmount.sale_amount = price.sale_amount
}
return await this.save(moneyAmount)
}
}
@@ -4,7 +4,7 @@ import ProductVariantService from "../product-variant"
const eventBusService = {
emit: jest.fn(),
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -253,7 +253,7 @@ describe("ProductVariantService", () => {
.fn()
.mockReturnValue(() => Promise.resolve())
productVariantService.setCurrencyPrice = jest
productVariantService.updateVariantPrices = jest
.fn()
.mockReturnValue(() => Promise.resolve())
@@ -381,14 +381,16 @@ describe("ProductVariantService", () => {
],
})
expect(productVariantService.setCurrencyPrice).toHaveBeenCalledTimes(1)
expect(productVariantService.setCurrencyPrice).toHaveBeenCalledWith(
expect(productVariantService.updateVariantPrices).toHaveBeenCalledTimes(1)
expect(productVariantService.updateVariantPrices).toHaveBeenCalledWith(
IdMap.getId("ironman"),
{
currency_code: "dkk",
amount: 1000,
sale_amount: 750,
}
[
{
currency_code: "dkk",
amount: 1000,
sale_amount: 750,
},
]
)
expect(productVariantRepository.save).toHaveBeenCalledTimes(1)
@@ -416,23 +418,125 @@ describe("ProductVariantService", () => {
})
})
describe("updateVariantPrices", () => {
const moneyAmountRepository = MockRepository({
remove: () => Promise.resolve(),
})
const oldPrices = [
{
currency_code: "dkk",
amount: 1000,
variant_id: "ironman",
region_id: null,
},
]
moneyAmountRepository.findVariantPricesNotIn = jest
.fn()
.mockImplementation(() => Promise.resolve(oldPrices))
const productVariantRepository = MockRepository({
findOne: (query) => Promise.resolve({ id: IdMap.getId("ironman") }),
})
const productOptionValueRepository = MockRepository({
findOne: () =>
Promise.resolve({ id: IdMap.getId("some-value"), value: "blue" }),
})
const productVariantService = new ProductVariantService({
manager: MockManager,
eventBusService,
moneyAmountRepository,
productVariantRepository,
productOptionValueRepository,
})
productVariantService.updateOptionValue = jest
.fn()
.mockReturnValue(() => Promise.resolve())
productVariantService.setCurrencyPrice = jest
.fn()
.mockReturnValue(() => Promise.resolve())
productVariantService.setRegionPrice = jest
.fn()
.mockReturnValue(() => Promise.resolve())
beforeEach(async () => {
jest.clearAllMocks()
})
it("successfully removes obsolete prices and calls setCurrencyPrice on new/existing prices", async () => {
await productVariantService.updateVariantPrices("ironman", [
{
currency_code: "usd",
amount: 4000,
},
])
expect(
moneyAmountRepository.findVariantPricesNotIn
).toHaveBeenCalledTimes(1)
expect(productVariantService.setCurrencyPrice).toHaveBeenCalledTimes(1)
expect(productVariantService.setCurrencyPrice).toHaveBeenCalledWith(
"ironman",
{
currency_code: "usd",
amount: 4000,
}
)
expect(moneyAmountRepository.remove).toHaveBeenCalledTimes(1)
expect(moneyAmountRepository.remove).toHaveBeenCalledWith(oldPrices)
})
it("successfully removes obsolete prices and calls setRegionPrice on new/existing prices", async () => {
await productVariantService.updateVariantPrices("ironman", [
{
region_id: "test-region",
amount: 4000,
sale_amount: 2000,
},
])
expect(
moneyAmountRepository.findVariantPricesNotIn
).toHaveBeenCalledTimes(1)
expect(productVariantService.setRegionPrice).toHaveBeenCalledTimes(1)
expect(productVariantService.setRegionPrice).toHaveBeenCalledWith(
"ironman",
{
region_id: "test-region",
amount: 4000,
sale_amount: 2000,
}
)
expect(moneyAmountRepository.remove).toHaveBeenCalledTimes(1)
expect(moneyAmountRepository.remove).toHaveBeenCalledWith(oldPrices)
})
})
describe("setCurrencyPrice", () => {
const productVariantRepository = MockRepository({
findOne: (query) => Promise.resolve({ id: IdMap.getId("ironman") }),
})
const moneyAmountRepository = MockRepository({
findOne: (query) => {
if (query.where.currency_code === "usd") {
return Promise.resolve(undefined)
}
const moneyAmountRepository = MockRepository()
moneyAmountRepository.upsertCurrencyPrice = jest
.fn()
.mockImplementation((variantId, price) => {
return Promise.resolve({
id: IdMap.getId("dkk"),
variant_id: IdMap.getId("ironman"),
currency_code: "dkk",
id: IdMap.getId("test-amount"),
variant_id: IdMap.getId(variantId),
...price,
})
},
})
})
const productVariantService = new ProductVariantService({
manager: MockManager,
@@ -445,50 +549,32 @@ describe("ProductVariantService", () => {
jest.clearAllMocks()
})
it("successfully creates a price if none exist with given currency", async () => {
it("calls upsert price with given currency", async () => {
await productVariantService.setCurrencyPrice(IdMap.getId("ironman"), {
currency_code: "usd",
amount: 100,
})
expect(moneyAmountRepository.create).toHaveBeenCalledTimes(1)
expect(moneyAmountRepository.create).toHaveBeenCalledWith({
variant_id: IdMap.getId("ironman"),
currency_code: "usd",
amount: 100,
})
expect(moneyAmountRepository.save).toHaveBeenCalledTimes(1)
})
it("successfully updates a non-regional price if currency exists", async () => {
await productVariantService.setCurrencyPrice(IdMap.getId("ironman"), {
currency_code: "dkk",
amount: 1000,
})
expect(moneyAmountRepository.create).toHaveBeenCalledTimes(0)
expect(moneyAmountRepository.save).toHaveBeenCalledTimes(1)
expect(moneyAmountRepository.save).toHaveBeenCalledWith({
variant_id: IdMap.getId("ironman"),
id: IdMap.getId("dkk"),
currency_code: "dkk",
amount: 1000,
sale_amount: undefined,
})
expect(moneyAmountRepository.upsertCurrencyPrice).toHaveBeenCalledTimes(1)
expect(moneyAmountRepository.upsertCurrencyPrice).toHaveBeenCalledWith(
IdMap.getId("ironman"),
{
currency_code: "usd",
amount: 100,
}
)
})
})
describe("getRegionPrice", () => {
const regionService = {
retrieve: function () {
retrieve: function() {
return Promise.resolve({
id: IdMap.getId("california"),
name: "California",
})
},
withTransaction: function () {
withTransaction: function() {
return this
},
}
+34 -38
View File
@@ -1,12 +1,6 @@
import { MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
import {
Brackets,
EntityManager,
ILike,
IsNull,
SelectQueryBuilder,
} from "typeorm"
import { Brackets, EntityManager, ILike, In, SelectQueryBuilder } from "typeorm"
import { MoneyAmount } from "../models/money-amount"
import { Product } from "../models/product"
import { ProductOptionValue } from "../models/product-option-value"
@@ -285,17 +279,7 @@ class ProductVariantService extends BaseService {
const { prices, options, metadata, inventory_quantity, ...rest } = update
if (prices) {
for (const price of prices) {
if (price.region_id) {
await this.setRegionPrice(variant.id, {
region_id: price.region_id,
amount: price.amount,
sale_amount: price.sale_amount || undefined,
})
} else {
await this.setCurrencyPrice(variant.id, price)
}
}
await this.updateVariantPrices(variant.id, prices)
}
if (options) {
@@ -333,6 +317,37 @@ class ProductVariantService extends BaseService {
})
}
async updateVariantPrices(
variantId: string,
prices: ProductVariantPrice[]
): Promise<void> {
return this.atomicPhase_(async (manager: EntityManager) => {
const moneyAmountRepo = manager.getCustomRepository(
this.moneyAmountRepository_
)
// get prices to be deleted
const obsoletePrices = await moneyAmountRepo.findVariantPricesNotIn(
variantId,
prices
)
for (const price of prices) {
if (price.region_id) {
await this.setRegionPrice(variantId, {
region_id: price.region_id,
amount: price.amount,
sale_amount: price.sale_amount || undefined,
})
} else {
await this.setCurrencyPrice(variantId, price)
}
}
await moneyAmountRepo.remove(obsoletePrices)
})
}
/**
* Sets the default price for the given currency.
* @param {string} variantId - the id of the variant to set prices for
@@ -348,26 +363,7 @@ class ProductVariantService extends BaseService {
this.moneyAmountRepository_
)
let moneyAmount = await moneyAmountRepo.findOne({
where: {
currency_code: price.currency_code?.toLowerCase(),
variant_id: variantId,
region_id: IsNull(),
},
})
if (!moneyAmount) {
moneyAmount = moneyAmountRepo.create({
...price,
currency_code: price.currency_code?.toLowerCase(),
variant_id: variantId,
})
} else {
moneyAmount.amount = price.amount
moneyAmount.sale_amount = price.sale_amount
}
return await moneyAmountRepo.save(moneyAmount)
return await moneyAmountRepo.upsertCurrencyPrice(variantId, price)
})
}