Feat: use price selection strategy (#1165)
* init * add query params * added buld id validation to repo * admin done * updated price reqs * initial price selection strategy * update customer seeder * format models * price selection strategy * price selection testing * update price selection tests * update price selection strategy * remove console.warn * update price seleciton strategy * use price selection strategy in "get-product" * price selection strategy integration testing * update integration tests * update price selection strat * remove console.log * fix unit tests * update product snapshot integration tests * fix failing unit tests * update variant test snapshots * update variant test snapshots * fix failing unit tests * update product snapshot integration tests * intial implementation of PriceList * add price selection strategy test to list-products * add price selection to list products * add price selection strategy to getRegionPrice * add price selection strategy to get variant * update product snapshot test * store testing of price selection * variant services * update cart service * update product tests * update test * unit testing with price selection strategy * integration tests for price lists * update sort prices for consistent results * update snapshot * update product snapshot with product ids * this time pipelines work * swap tests * redo ordering * updated admin/product integration tests * update updateVariantPrices method * remove comment from error handler * add integration test for batch deleting prices associated with price list * named ids * run with verbose * add console.log * sort products in integration test * remove verbose flag * make update to prices through variant service limited to default prices * update store/products.js snapshot * update comment * add todo * lift existing price calculations to variant level * remove unused import * add api unit tests and update product integration tests to validate that prices from Price List are ignored * fix product test * update integration tests * pre merge commit * requested changes * cascade * ensure delete variant cascades to MoneyAmount * fetch variants correctly * use find options * add pricelist to relevant seeders * update integration tests * update price selection with "includeDiscountPrices" * use transaction with price selection strategy * add await to prevent store test errors * remove verbose * addresses PR feedback * removed unused endpoint * remove unused repository from constructor * remove from constructor argument * update mock * update unit tests * fix failing store integration tests * remove medusajs ressource * re add env.template * price selection strategy methods * fix integration tests * update unit tests * remove commented out code * update jsdoc * update price selection strategy parameter * update snapshots * integration tests cleanup * pr feedback * update integration tests * pr feedback * price selection unit tests * add calculated price type to results * cleanup include discount prices and price selection configs * refactor price selection params to a separate file * update tests and refactor price selection seeder * remove prices from variant update * rename result * pr feedback * remove unused import * create getRegionPriceContext * remove from params in search * remove unused import Co-authored-by: Kasper <kasper@medusajs.com> Co-authored-by: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com>
This commit is contained in:
36 files changed
+3794
-533
No files matched your search
@@ -74,9 +74,9 @@ export const ProductVariantServiceMock = {
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
getRegionPrice: jest.fn().mockImplementation((variantId, regionId) => {
|
||||
getRegionPrice: jest.fn().mockImplementation((variantId, context) => {
|
||||
if (variantId === IdMap.getId("test-variant-1")) {
|
||||
if (regionId === IdMap.getId("world")) {
|
||||
if (context.regionId === IdMap.getId("world")) {
|
||||
return Promise.resolve(10)
|
||||
} else {
|
||||
return Promise.resolve(20)
|
||||
|
||||
@@ -58,7 +58,7 @@ class AddOnLineItemService extends BaseService {
|
||||
|
||||
let unitPrice = await this.productVariantService_.getRegionPrice(
|
||||
variant._id,
|
||||
region._id
|
||||
{regionId: region._id}
|
||||
)
|
||||
|
||||
const addOnPrices = await Promise.all(
|
||||
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
|
||||
import { ProductService, ProductVariantService } from "../../../../services"
|
||||
import { PriceSelectionParams } from "../../../../types/price-selection"
|
||||
import { ProductVariantPricesUpdateReq } from "../../../../types/product-variant"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
@@ -130,6 +132,8 @@ export default async (req, res) => {
|
||||
req.body
|
||||
)
|
||||
|
||||
const validatedQueryParams = await validator(PriceSelectionParams, req.query)
|
||||
|
||||
const productService: ProductService = req.scope.resolve("productService")
|
||||
const productVariantService: ProductVariantService = req.scope.resolve(
|
||||
"productVariantService"
|
||||
@@ -140,9 +144,12 @@ export default async (req, res) => {
|
||||
...validated,
|
||||
})
|
||||
|
||||
await productVariantService.retrieve(variant_id)
|
||||
|
||||
const product = await productService.retrieve(id, {
|
||||
select: defaultAdminProductFields,
|
||||
relations: defaultAdminProductRelations,
|
||||
...validatedQueryParams,
|
||||
})
|
||||
|
||||
res.json({ product })
|
||||
|
||||
@@ -21,7 +21,13 @@ describe("Get product by id", () => {
|
||||
expect(ProductServiceMock.retrieve).toHaveBeenCalledTimes(1)
|
||||
expect(ProductServiceMock.retrieve).toHaveBeenCalledWith(
|
||||
IdMap.getId("product1"),
|
||||
{ relations: defaultStoreProductsRelations }
|
||||
{
|
||||
relations: defaultStoreProductsRelations,
|
||||
cart_id: undefined,
|
||||
currency_code: undefined,
|
||||
region_id: undefined,
|
||||
include_discount_prices: true,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -51,7 +57,13 @@ describe("Get product by id", () => {
|
||||
it("endpoint called with defaultRelations", () => {
|
||||
expect(ProductServiceMock.retrieve).toHaveBeenCalledWith(
|
||||
IdMap.getId("variantsWithPrices"),
|
||||
{ relations: defaultStoreProductsRelations }
|
||||
{
|
||||
relations: defaultStoreProductsRelations,
|
||||
cart_id: undefined,
|
||||
currency_code: undefined,
|
||||
region_id: undefined,
|
||||
include_discount_prices: true,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -19,7 +19,15 @@ describe("GET /store/products", () => {
|
||||
expect(ProductServiceMock.listAndCount).toHaveBeenCalledTimes(1)
|
||||
expect(ProductServiceMock.listAndCount).toHaveBeenCalledWith(
|
||||
{ status: ["published"] },
|
||||
{ relations: defaultStoreProductsRelations, skip: 0, take: 100 }
|
||||
{
|
||||
relations: defaultStoreProductsRelations,
|
||||
skip: 0,
|
||||
take: 100,
|
||||
cart_id: undefined,
|
||||
currency_code: undefined,
|
||||
region_id: undefined,
|
||||
include_discount_prices: true,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -42,7 +50,15 @@ describe("GET /store/products", () => {
|
||||
expect(ProductServiceMock.listAndCount).toHaveBeenCalledTimes(1)
|
||||
expect(ProductServiceMock.listAndCount).toHaveBeenCalledWith(
|
||||
{ is_giftcard: true, status: ["published"] },
|
||||
{ relations: defaultStoreProductsRelations, skip: 0, take: 100 }
|
||||
{
|
||||
relations: defaultStoreProductsRelations,
|
||||
skip: 0,
|
||||
take: 100,
|
||||
cart_id: undefined,
|
||||
currency_code: undefined,
|
||||
region_id: undefined,
|
||||
include_discount_prices: true,
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { defaultStoreProductsRelations } from "."
|
||||
import { ProductService } from "../../../../services"
|
||||
import { PriceSelectionParams } from "../../../../types/price-selection"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [get] /products/{id}
|
||||
@@ -23,9 +25,18 @@ import { ProductService } from "../../../../services"
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const validated = await validator(PriceSelectionParams, req.query)
|
||||
|
||||
const customer_id = req.user?.customer_id
|
||||
|
||||
const productService: ProductService = req.scope.resolve("productService")
|
||||
const product = await productService.retrieve(id, {
|
||||
relations: defaultStoreProductsRelations,
|
||||
cart_id: validated.cart_id,
|
||||
customer_id: customer_id,
|
||||
region_id: validated.region_id,
|
||||
currency_code: validated.currency_code,
|
||||
include_discount_prices: true,
|
||||
})
|
||||
|
||||
res.json({ product })
|
||||
|
||||
@@ -11,6 +11,7 @@ import { omit, pickBy } from "lodash"
|
||||
import { defaultStoreProductsRelations } from "."
|
||||
import { ProductService } from "../../../../services"
|
||||
import { DateComparisonOperator } from "../../../../types/common"
|
||||
import { PriceSelectionParams } from "../../../../types/price-selection"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { IsType } from "../../../../utils/validators/is-type"
|
||||
import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
|
||||
@@ -68,6 +69,9 @@ export default async (req, res) => {
|
||||
"expand",
|
||||
"limit",
|
||||
"offset",
|
||||
"cart_id",
|
||||
"region_id",
|
||||
"currency_code",
|
||||
])
|
||||
|
||||
// get only published products for store endpoint
|
||||
@@ -92,6 +96,11 @@ export default async (req, res) => {
|
||||
: defaultStoreProductsRelations,
|
||||
skip: validated.offset,
|
||||
take: validated.limit,
|
||||
cart_id: validated.cart_id,
|
||||
region_id: validated.region_id,
|
||||
currency_code: validated.currency_code,
|
||||
customer_id: req.user?.customer_id,
|
||||
include_discount_prices: true,
|
||||
}
|
||||
|
||||
const [products, count] = await productService.listAndCount(
|
||||
@@ -107,7 +116,7 @@ export default async (req, res) => {
|
||||
})
|
||||
}
|
||||
|
||||
export class StoreGetProductsPaginationParams {
|
||||
export class StoreGetProductsPaginationParams extends PriceSelectionParams {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
fields?: string
|
||||
|
||||
@@ -16,6 +16,10 @@ describe("Get variant by id", () => {
|
||||
expect(ProductVariantServiceMock.retrieve).toHaveBeenCalledTimes(1)
|
||||
expect(ProductVariantServiceMock.retrieve).toHaveBeenCalledWith("1", {
|
||||
relations: ["prices", "options"],
|
||||
cart_id: undefined,
|
||||
currency_code: undefined,
|
||||
region_id: undefined,
|
||||
include_discount_prices: true,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { defaultStoreVariantRelations } from "."
|
||||
import ProductVariantService from "../../../../services/product-variant"
|
||||
import { PriceSelectionParams } from "../../../../types/price-selection"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [get] /variants/{variant_id}
|
||||
@@ -23,12 +25,21 @@ import ProductVariantService from "../../../../services/product-variant"
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const validated = await validator(PriceSelectionParams, req.query)
|
||||
|
||||
const variantService: ProductVariantService = req.scope.resolve(
|
||||
"productVariantService"
|
||||
)
|
||||
|
||||
const customer_id = req.user?.customer_id
|
||||
|
||||
const variant = await variantService.retrieve(id, {
|
||||
relations: defaultStoreVariantRelations,
|
||||
cart_id: validated.cart_id,
|
||||
customer_id: customer_id,
|
||||
region_id: validated.region_id,
|
||||
currency_code: validated.currency_code,
|
||||
include_discount_prices: true,
|
||||
})
|
||||
|
||||
res.json({ variant })
|
||||
|
||||
@@ -7,6 +7,7 @@ import ProductVariantService from "../../../../services/product-variant"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { IsType } from "../../../../utils/validators/is-type"
|
||||
import { NumericalComparisonOperator } from "../../../../types/common"
|
||||
import { PriceSelectionParams } from "../../../../types/price-selection"
|
||||
|
||||
/**
|
||||
* @oas [get] /variants
|
||||
@@ -41,12 +42,19 @@ export default async (req, res) => {
|
||||
expandFields = expand.split(",")
|
||||
}
|
||||
|
||||
const customer_id = req.user?.customer_id
|
||||
|
||||
const listConfig = {
|
||||
relations: expandFields.length
|
||||
? expandFields
|
||||
: defaultStoreVariantRelations,
|
||||
skip: offset,
|
||||
take: limit,
|
||||
cart_id: validated.cart_id,
|
||||
region_id: validated.region_id,
|
||||
currency_code: validated.currency_code,
|
||||
customer_id: customer_id,
|
||||
include_discount_prices: true,
|
||||
}
|
||||
|
||||
const filterableFields: FilterableProductVariantProps = omit(validated, [
|
||||
@@ -54,6 +62,9 @@ export default async (req, res) => {
|
||||
"limit",
|
||||
"offset",
|
||||
"expand",
|
||||
"cart_id",
|
||||
"region_id",
|
||||
"currency_code",
|
||||
])
|
||||
|
||||
if (validated.ids) {
|
||||
@@ -68,7 +79,7 @@ export default async (req, res) => {
|
||||
res.json({ variants })
|
||||
}
|
||||
|
||||
export class StoreGetVariantsParams {
|
||||
export class StoreGetVariantsParams extends PriceSelectionParams {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { EntityManager } from "typeorm"
|
||||
import { MoneyAmount } from ".."
|
||||
import { MoneyAmountRepository } from "../repositories/money-amount"
|
||||
import { PriceListType } from "../types/price-list"
|
||||
|
||||
export interface IPriceSelectionStrategy {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn
|
||||
UpdateDateColumn,
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
import { resolveDbType } from "../utils/db-aware-column"
|
||||
@@ -41,11 +41,10 @@ export class MoneyAmount {
|
||||
@Column({ nullable: true })
|
||||
price_list_id: string | null
|
||||
|
||||
@ManyToOne(
|
||||
() => PriceList,
|
||||
(priceList) => priceList.prices,
|
||||
{ cascade: true, onDelete: "CASCADE" }
|
||||
)
|
||||
@ManyToOne(() => PriceList, (priceList) => priceList.prices, {
|
||||
cascade: true,
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
@JoinColumn({ name: "price_list_id" })
|
||||
price_list: PriceList | null
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ManyToMany,
|
||||
OneToMany,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn
|
||||
UpdateDateColumn,
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
import { PriceListStatus, PriceListType } from "../types/price-list"
|
||||
@@ -53,18 +53,14 @@ export class PriceList {
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
})
|
||||
@ManyToMany(
|
||||
() => CustomerGroup,
|
||||
(cg) => cg.price_lists,
|
||||
{ onDelete: "CASCADE" }
|
||||
)
|
||||
@ManyToMany(() => CustomerGroup, (cg) => cg.price_lists, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
customer_groups: CustomerGroup[]
|
||||
|
||||
@OneToMany(
|
||||
() => MoneyAmount,
|
||||
(moneyAmount) => moneyAmount.price_list,
|
||||
{ onDelete: "CASCADE" }
|
||||
)
|
||||
@OneToMany(() => MoneyAmount, (moneyAmount) => moneyAmount.price_list, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
prices: MoneyAmount[]
|
||||
|
||||
@CreateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import partition from "lodash/partition"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import {
|
||||
Brackets,
|
||||
EntityRepository,
|
||||
@@ -130,7 +131,7 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
||||
"price_list",
|
||||
"ma.price_list_id = price_list.id "
|
||||
)
|
||||
.where({ variant_id: variant_id }) // "ma.variant_id = :variant_id",
|
||||
.where({ variant_id: variant_id })
|
||||
.andWhere("(ma.price_list_id is null or price_list.status = 'active')")
|
||||
.andWhere(
|
||||
"(price_list is null or price_list.ends_at is null OR price_list.ends_at > :date) ",
|
||||
|
||||
@@ -1040,6 +1040,19 @@ describe("CartService", () => {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const priceSelectionStrat = {
|
||||
calculateVariantPrice: async (variantId, context) => {
|
||||
if (variantId === IdMap.getId("fail")) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Money amount for variant with id ${variantId} in region ${context.region_id} does not exist`
|
||||
)
|
||||
} else {
|
||||
return { calculatedPrice: 100 }
|
||||
}
|
||||
},
|
||||
}
|
||||
const cartService = new CartService({
|
||||
manager: MockManager,
|
||||
paymentProviderService,
|
||||
@@ -1050,6 +1063,7 @@ describe("CartService", () => {
|
||||
lineItemService,
|
||||
productVariantService,
|
||||
eventBusService,
|
||||
priceSelectionStrategy: priceSelectionStrat,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -1316,7 +1330,7 @@ describe("CartService", () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
let cartService = new CartService({})
|
||||
const cartService = new CartService({})
|
||||
|
||||
it("given a cart with custom shipping options and a shipping option id corresponding to a custom shipping option, then it should return a custom shipping option", async () => {
|
||||
const cartCSO = [
|
||||
|
||||
@@ -3,7 +3,7 @@ import ProductVariantService from "../product-variant"
|
||||
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -18,9 +18,33 @@ describe("ProductVariantService", () => {
|
||||
return Promise.resolve({ id: IdMap.getId("ironman") })
|
||||
},
|
||||
})
|
||||
|
||||
const cartRepository = MockRepository({
|
||||
findOne: (data) => {
|
||||
return Promise.resolve({})
|
||||
},
|
||||
})
|
||||
|
||||
const priceSelectionStrat = {
|
||||
calculateVariantPrice: (variantId, context) => {
|
||||
return {
|
||||
originalPrice: null,
|
||||
calculatedPrice: null,
|
||||
prices: [],
|
||||
}
|
||||
},
|
||||
}
|
||||
const priceSelectionStrategy = {
|
||||
withTransaction: (manager) => {
|
||||
return priceSelectionStrat
|
||||
},
|
||||
}
|
||||
|
||||
const productVariantService = new ProductVariantService({
|
||||
manager: MockManager,
|
||||
productVariantRepository,
|
||||
cartRepository,
|
||||
priceSelectionStrategy,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -227,6 +251,29 @@ describe("ProductVariantService", () => {
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
const cartRepository = MockRepository({
|
||||
findOne: (data) => {
|
||||
return Promise.resolve({})
|
||||
},
|
||||
})
|
||||
|
||||
const priceSelectionStrat = {
|
||||
calculateVariantPrice: (variantId, context) => {
|
||||
return {
|
||||
originalPrice: null,
|
||||
calculatedPrice: null,
|
||||
calculatedPriceType: undefined,
|
||||
prices: [],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const priceSelectionStrategy = {
|
||||
withTransaction: (manager) => {
|
||||
return priceSelectionStrat
|
||||
},
|
||||
}
|
||||
|
||||
const productVariantRepository = MockRepository({
|
||||
findOne: (query) => Promise.resolve({ id: IdMap.getId("ironman") }),
|
||||
})
|
||||
@@ -246,6 +293,8 @@ describe("ProductVariantService", () => {
|
||||
moneyAmountRepository,
|
||||
productVariantRepository,
|
||||
productOptionValueRepository,
|
||||
cartRepository,
|
||||
priceSelectionStrategy,
|
||||
})
|
||||
|
||||
productVariantService.updateOptionValue = jest
|
||||
@@ -464,13 +513,13 @@ describe("ProductVariantService", () => {
|
||||
|
||||
describe("getRegionPrice", () => {
|
||||
const regionService = {
|
||||
retrieve: function() {
|
||||
retrieve: function () {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("california"),
|
||||
name: "California",
|
||||
})
|
||||
},
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -498,11 +547,35 @@ describe("ProductVariantService", () => {
|
||||
},
|
||||
})
|
||||
|
||||
const cartRepository = MockRepository({
|
||||
findOne: (data) => {
|
||||
return Promise.resolve({})
|
||||
},
|
||||
})
|
||||
|
||||
const priceSelectionStrat = {
|
||||
calculateVariantPrice: (variantId, context) => {
|
||||
return Promise.resolve({
|
||||
originalPrice: null,
|
||||
calculatedPrice: 1000,
|
||||
prices: [],
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const priceSelectionStrategy = {
|
||||
withTransaction: (manager) => {
|
||||
return priceSelectionStrat
|
||||
},
|
||||
}
|
||||
|
||||
const productVariantService = new ProductVariantService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
regionService,
|
||||
moneyAmountRepository,
|
||||
cartRepository,
|
||||
priceSelectionStrategy,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import ProductService from "../product"
|
||||
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -54,7 +54,7 @@ describe("ProductService", () => {
|
||||
|
||||
describe("create", () => {
|
||||
const productRepository = MockRepository({
|
||||
create: product => ({
|
||||
create: (product) => ({
|
||||
id: IdMap.getId("ironman"),
|
||||
title: "Suit",
|
||||
options: [],
|
||||
@@ -71,7 +71,7 @@ describe("ProductService", () => {
|
||||
|
||||
const productTagRepository = MockRepository({
|
||||
findOne: () => Promise.resolve(undefined),
|
||||
create: data => {
|
||||
create: (data) => {
|
||||
if (data.value === "title") {
|
||||
return { id: "tag-1", value: "title" }
|
||||
}
|
||||
@@ -83,16 +83,16 @@ describe("ProductService", () => {
|
||||
})
|
||||
const productTypeRepository = MockRepository({
|
||||
findOne: () => Promise.resolve(undefined),
|
||||
create: data => {
|
||||
create: (data) => {
|
||||
return { id: "type", value: "type1" }
|
||||
},
|
||||
})
|
||||
|
||||
const productCollectionService = {
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
retrieve: id =>
|
||||
retrieve: (id) =>
|
||||
Promise.resolve({ id: IdMap.getId("cat"), title: "Suits" }),
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ describe("ProductService", () => {
|
||||
|
||||
const productTypeRepository = MockRepository({
|
||||
findOne: () => Promise.resolve(undefined),
|
||||
create: data => {
|
||||
create: (data) => {
|
||||
return { id: "type", value: "type1" }
|
||||
},
|
||||
})
|
||||
@@ -231,7 +231,7 @@ describe("ProductService", () => {
|
||||
const productVariantRepository = MockRepository()
|
||||
|
||||
const productVariantService = {
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
update: (variant, update) => {
|
||||
@@ -243,7 +243,7 @@ describe("ProductService", () => {
|
||||
}
|
||||
|
||||
const productTagRepository = MockRepository({
|
||||
findOne: data => {
|
||||
findOne: (data) => {
|
||||
if (data.where.value === "test") {
|
||||
return Promise.resolve({ id: IdMap.getId("test"), value: "test" })
|
||||
}
|
||||
@@ -253,6 +253,25 @@ describe("ProductService", () => {
|
||||
},
|
||||
})
|
||||
|
||||
const cartRepository = MockRepository({
|
||||
findOne: (data) => {
|
||||
return Promise.resolve({})
|
||||
},
|
||||
})
|
||||
|
||||
const priceSelectionStrategy = {
|
||||
withTransaction: (manager) => {
|
||||
return this
|
||||
},
|
||||
calculateVariantPrice: (variantId, context) => {
|
||||
return {
|
||||
originalPrice: null,
|
||||
calculatedPrice: null,
|
||||
prices: [],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const productService = new ProductService({
|
||||
manager: MockManager,
|
||||
productRepository,
|
||||
@@ -261,6 +280,8 @@ describe("ProductService", () => {
|
||||
productTagRepository,
|
||||
productTypeRepository,
|
||||
eventBusService,
|
||||
cartRepository,
|
||||
priceSelectionStrategy,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -440,7 +461,7 @@ describe("ProductService", () => {
|
||||
|
||||
describe("addOption", () => {
|
||||
const productRepository = MockRepository({
|
||||
findOneWithRelations: query =>
|
||||
findOneWithRelations: (query) =>
|
||||
Promise.resolve({
|
||||
id: IdMap.getId("ironman"),
|
||||
options: [{ title: "Color" }],
|
||||
@@ -449,7 +470,7 @@ describe("ProductService", () => {
|
||||
})
|
||||
|
||||
const productVariantService = {
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
addOptionValue: jest.fn(),
|
||||
@@ -513,7 +534,7 @@ describe("ProductService", () => {
|
||||
|
||||
describe("reorderVariants", () => {
|
||||
const productRepository = MockRepository({
|
||||
findOneWithRelations: query =>
|
||||
findOneWithRelations: (query) =>
|
||||
Promise.resolve({
|
||||
id: IdMap.getId("ironman"),
|
||||
variants: [{ id: IdMap.getId("green") }, { id: IdMap.getId("blue") }],
|
||||
@@ -571,7 +592,7 @@ describe("ProductService", () => {
|
||||
|
||||
describe("reorderOptions", () => {
|
||||
const productRepository = MockRepository({
|
||||
findOneWithRelations: query =>
|
||||
findOneWithRelations: (query) =>
|
||||
Promise.resolve({
|
||||
id: IdMap.getId("ironman"),
|
||||
options: [
|
||||
@@ -637,7 +658,7 @@ describe("ProductService", () => {
|
||||
|
||||
describe("updateOption", () => {
|
||||
const productRepository = MockRepository({
|
||||
findOneWithRelations: query =>
|
||||
findOneWithRelations: (query) =>
|
||||
Promise.resolve({
|
||||
id: IdMap.getId("ironman"),
|
||||
options: [
|
||||
@@ -712,7 +733,7 @@ describe("ProductService", () => {
|
||||
|
||||
describe("deleteOption", () => {
|
||||
const productRepository = MockRepository({
|
||||
findOneWithRelations: query =>
|
||||
findOneWithRelations: (query) =>
|
||||
Promise.resolve({
|
||||
id: IdMap.getId("ironman"),
|
||||
variants: [
|
||||
@@ -751,7 +772,7 @@ describe("ProductService", () => {
|
||||
})
|
||||
|
||||
const productOptionRepository = MockRepository({
|
||||
findOne: query => {
|
||||
findOne: (query) => {
|
||||
if (query.where.id === IdMap.getId("material")) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import _ from "lodash"
|
||||
import { EntityManager, DeepPartial } from "typeorm"
|
||||
import {
|
||||
EntityManager,
|
||||
DeepPartial,
|
||||
AlreadyHasActiveConnectionError,
|
||||
} from "typeorm"
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
|
||||
@@ -38,6 +42,7 @@ import GiftCardService from "./gift-card"
|
||||
import TotalsService from "./totals"
|
||||
import InventoryService from "./inventory"
|
||||
import CustomShippingOptionService from "./custom-shipping-option"
|
||||
import { IPriceSelectionStrategy } from "../interfaces/price-selection-strategy"
|
||||
|
||||
type CartConstructorProps = {
|
||||
manager: EntityManager
|
||||
@@ -59,6 +64,7 @@ type CartConstructorProps = {
|
||||
totalsService: TotalsService
|
||||
inventoryService: InventoryService
|
||||
customShippingOptionService: CustomShippingOptionService
|
||||
priceSelectionStrategy: IPriceSelectionStrategy
|
||||
}
|
||||
|
||||
type TotalsConfig = {
|
||||
@@ -94,6 +100,7 @@ class CartService extends BaseService {
|
||||
private paymentSessionRepository_: typeof PaymentSessionRepository
|
||||
private inventoryService_: InventoryService
|
||||
private customShippingOptionService_: CustomShippingOptionService
|
||||
private priceSelectionStrategy_: IPriceSelectionStrategy
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
@@ -115,6 +122,7 @@ class CartService extends BaseService {
|
||||
paymentSessionRepository,
|
||||
inventoryService,
|
||||
customShippingOptionService,
|
||||
priceSelectionStrategy,
|
||||
}: CartConstructorProps) {
|
||||
super()
|
||||
|
||||
@@ -137,6 +145,7 @@ class CartService extends BaseService {
|
||||
this.inventoryService_ = inventoryService
|
||||
this.customShippingOptionService_ = customShippingOptionService
|
||||
this.taxProviderService_ = taxProviderService
|
||||
this.priceSelectionStrategy_ = priceSelectionStrategy
|
||||
}
|
||||
|
||||
withTransaction(transactionManager: EntityManager): CartService {
|
||||
@@ -164,6 +173,7 @@ class CartService extends BaseService {
|
||||
giftCardService: this.giftCardService_,
|
||||
inventoryService: this.inventoryService_,
|
||||
customShippingOptionService: this.customShippingOptionService_,
|
||||
priceSelectionStrategy: this.priceSelectionStrategy_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
@@ -701,12 +711,6 @@ class CartService extends BaseService {
|
||||
],
|
||||
})
|
||||
|
||||
if (typeof update.region_id !== "undefined") {
|
||||
const countryCode =
|
||||
(update.country_code || update.shipping_address?.country_code) ?? null
|
||||
await this.setRegion_(cart, update.region_id, countryCode)
|
||||
}
|
||||
|
||||
if (typeof update.customer_id !== "undefined") {
|
||||
await this.updateCustomerId_(cart, update.customer_id)
|
||||
} else {
|
||||
@@ -718,6 +722,19 @@ class CartService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof update.region_id !== "undefined") {
|
||||
const countryCode =
|
||||
(update.country_code || update.shipping_address?.country_code) ?? null
|
||||
await this.setRegion_(cart, update.region_id, countryCode)
|
||||
}
|
||||
|
||||
if (
|
||||
typeof update.customer_id !== "undefined" ||
|
||||
typeof update.region_id !== "undefined"
|
||||
) {
|
||||
await this.updateUnitPrices(cart, update.region_id, update.customer_id)
|
||||
}
|
||||
|
||||
const addrRepo = manager.getCustomRepository(this.addressRepository_)
|
||||
if ("shipping_address_id" in update || "shipping_address" in update) {
|
||||
let address: string | Partial<Address> | undefined
|
||||
@@ -1169,7 +1186,10 @@ class CartService extends BaseService {
|
||||
* this could be IP address or similar for fraud handling.
|
||||
* @return the resulting cart
|
||||
*/
|
||||
async authorizePayment(cartId: string, context: Record<string, any> = {}): Promise<Cart> {
|
||||
async authorizePayment(
|
||||
cartId: string,
|
||||
context: Record<string, any> = {}
|
||||
): Promise<Cart> {
|
||||
return this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const cartRepository = manager.getCustomRepository(this.cartRepository_)
|
||||
|
||||
@@ -1592,6 +1612,57 @@ class CartService extends BaseService {
|
||||
return customOption
|
||||
}
|
||||
|
||||
async updateUnitPrices(
|
||||
cart: Cart,
|
||||
regionId?: string,
|
||||
customer_id?: string
|
||||
): Promise<void> {
|
||||
// If the cart contains items, we update the price of the items
|
||||
// to match the updated region or customer id (keeping the old
|
||||
// value if it exists)
|
||||
if (cart.items.length) {
|
||||
const region = await this.regionService_.retrieve(
|
||||
regionId || cart.region_id,
|
||||
{
|
||||
relations: ["countries"],
|
||||
}
|
||||
)
|
||||
|
||||
cart.items = await Promise.all(
|
||||
cart.items
|
||||
.map(async (item) => {
|
||||
const availablePrice = await this.priceSelectionStrategy_
|
||||
.calculateVariantPrice(item.variant_id, {
|
||||
region_id: region.id,
|
||||
currency_code: region.currency_code,
|
||||
quantity: item.quantity,
|
||||
customer_id: customer_id || cart.customer_id,
|
||||
include_discount_prices: true,
|
||||
})
|
||||
.catch(() => undefined)
|
||||
|
||||
if (
|
||||
availablePrice !== undefined &&
|
||||
availablePrice.calculatedPrice !== null
|
||||
) {
|
||||
return this.lineItemService_
|
||||
.withTransaction(this.transactionManager_)
|
||||
.update(item.id, {
|
||||
has_shipping: false,
|
||||
unit_price: availablePrice.calculatedPrice,
|
||||
})
|
||||
} else {
|
||||
await this.lineItemService_
|
||||
.withTransaction(this.transactionManager_)
|
||||
.delete(item.id)
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set's the region of a cart.
|
||||
* @param cart - the cart to set region on
|
||||
@@ -1619,34 +1690,6 @@ class CartService extends BaseService {
|
||||
cart.region = region
|
||||
cart.region_id = region.id
|
||||
|
||||
// If the cart contains items we want to change the unit_price field of each
|
||||
// item to correspond to the price given in the region
|
||||
if (cart.items.length) {
|
||||
cart.items = await Promise.all(
|
||||
cart.items
|
||||
.map(async (item) => {
|
||||
const availablePrice = await this.productVariantService_
|
||||
.getRegionPrice(item.variant_id, regionId)
|
||||
.catch(() => undefined)
|
||||
|
||||
if (availablePrice !== undefined) {
|
||||
return this.lineItemService_
|
||||
.withTransaction(this.transactionManager_)
|
||||
.update(item.id, {
|
||||
has_shipping: false,
|
||||
unit_price: availablePrice,
|
||||
})
|
||||
} else {
|
||||
await this.lineItemService_
|
||||
.withTransaction(this.transactionManager_)
|
||||
.delete(item.id)
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* When changing the region you are changing the set of countries that your
|
||||
* cart can be shipped to so we need to make sure that the current shipping
|
||||
|
||||
@@ -143,6 +143,7 @@ class LineItemService extends BaseService {
|
||||
.withTransaction(manager)
|
||||
.retrieve(variantId, {
|
||||
relations: ["product"],
|
||||
include_discount_prices: true,
|
||||
})
|
||||
|
||||
const region = await this.regionService_
|
||||
@@ -164,7 +165,12 @@ class LineItemService extends BaseService {
|
||||
} else {
|
||||
price = await this.productVariantService_
|
||||
.withTransaction(manager)
|
||||
.getRegionPrice(variant.id, region.id)
|
||||
.getRegionPrice(variant.id, {
|
||||
regionId: region.id,
|
||||
quantity: quantity,
|
||||
customer_id: undefined,
|
||||
include_discount_prices: true,
|
||||
})
|
||||
}
|
||||
|
||||
const toCreate = {
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { JsonWebTokenError } from "jsonwebtoken"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { Brackets, EntityManager, ILike, SelectQueryBuilder } from "typeorm"
|
||||
import { MoneyAmount } from ".."
|
||||
import {
|
||||
IPriceSelectionStrategy,
|
||||
PriceSelectionContext,
|
||||
} from "../interfaces/price-selection-strategy"
|
||||
import { MoneyAmount } from "../models/money-amount"
|
||||
import { Product } from "../models/product"
|
||||
import { ProductOptionValue } from "../models/product-option-value"
|
||||
import { ProductVariant } from "../models/product-variant"
|
||||
import { CartRepository } from "../repositories/cart"
|
||||
import { MoneyAmountRepository } from "../repositories/money-amount"
|
||||
import { ProductRepository } from "../repositories/product"
|
||||
import { ProductOptionValueRepository } from "../repositories/product-option-value"
|
||||
@@ -18,6 +24,7 @@ import { FindConfig } from "../types/common"
|
||||
import {
|
||||
CreateProductVariantInput,
|
||||
FilterableProductVariantProps,
|
||||
GetRegionPriceContext,
|
||||
ProductVariantPrice,
|
||||
UpdateProductVariantInput,
|
||||
} from "../types/product-variant"
|
||||
@@ -38,8 +45,10 @@ class ProductVariantService extends BaseService {
|
||||
private productRepository_: typeof ProductRepository
|
||||
private eventBus_: EventBusService
|
||||
private regionService_: RegionService
|
||||
private priceSelectionStrategy_: IPriceSelectionStrategy
|
||||
private moneyAmountRepository_: typeof MoneyAmountRepository
|
||||
private productOptionValueRepository_: typeof ProductOptionValueRepository
|
||||
private cartRepository_: typeof CartRepository
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
@@ -49,6 +58,8 @@ class ProductVariantService extends BaseService {
|
||||
regionService,
|
||||
moneyAmountRepository,
|
||||
productOptionValueRepository,
|
||||
cartRepository,
|
||||
priceSelectionStrategy,
|
||||
}) {
|
||||
super()
|
||||
|
||||
@@ -70,6 +81,10 @@ class ProductVariantService extends BaseService {
|
||||
this.moneyAmountRepository_ = moneyAmountRepository
|
||||
|
||||
this.productOptionValueRepository_ = productOptionValueRepository
|
||||
|
||||
this.cartRepository_ = cartRepository
|
||||
|
||||
this.priceSelectionStrategy_ = priceSelectionStrategy
|
||||
}
|
||||
|
||||
withTransaction(transactionManager: EntityManager): ProductVariantService {
|
||||
@@ -85,6 +100,8 @@ class ProductVariantService extends BaseService {
|
||||
regionService: this.regionService_,
|
||||
moneyAmountRepository: this.moneyAmountRepository_,
|
||||
productOptionValueRepository: this.productOptionValueRepository_,
|
||||
cartRepository: this.cartRepository_,
|
||||
priceSelectionStrategy: this.priceSelectionStrategy_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
@@ -100,12 +117,21 @@ class ProductVariantService extends BaseService {
|
||||
*/
|
||||
async retrieve(
|
||||
variantId: string,
|
||||
config: FindConfig<ProductVariant> = {}
|
||||
config: FindConfig<ProductVariant> & PriceSelectionContext = {
|
||||
include_discount_prices: false,
|
||||
}
|
||||
): Promise<ProductVariant> {
|
||||
const variantRepo = this.manager_.getCustomRepository(
|
||||
this.productVariantRepository_
|
||||
)
|
||||
const validatedId = this.validateId_(variantId)
|
||||
|
||||
const priceIndex = config.relations?.indexOf("prices") ?? -1
|
||||
if (priceIndex >= 0 && config.relations) {
|
||||
config.relations = [...config.relations]
|
||||
config.relations.splice(priceIndex, 1)
|
||||
}
|
||||
|
||||
const query = this.buildQuery_({ id: validatedId }, config)
|
||||
const variant = await variantRepo.findOne(query)
|
||||
|
||||
@@ -116,7 +142,16 @@ class ProductVariantService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
return variant
|
||||
return priceIndex >= 0
|
||||
? ((await this.setAdditionalPrices(
|
||||
variant,
|
||||
config.currency_code,
|
||||
config.region_id,
|
||||
config.cart_id,
|
||||
config.customer_id,
|
||||
config.include_discount_prices
|
||||
)) as ProductVariant)
|
||||
: variant
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,11 +162,20 @@ class ProductVariantService extends BaseService {
|
||||
*/
|
||||
async retrieveBySKU(
|
||||
sku: string,
|
||||
config: FindConfig<ProductVariant> = {}
|
||||
config: FindConfig<ProductVariant> & PriceSelectionContext = {
|
||||
include_discount_prices: false,
|
||||
}
|
||||
): Promise<ProductVariant> {
|
||||
const variantRepo = this.manager_.getCustomRepository(
|
||||
this.productVariantRepository_
|
||||
)
|
||||
|
||||
const priceIndex = config.relations?.indexOf("prices") ?? -1
|
||||
if (priceIndex >= 0 && config.relations) {
|
||||
config.relations = [...config.relations]
|
||||
config.relations.splice(priceIndex, 1)
|
||||
}
|
||||
|
||||
const query = this.buildQuery_({ sku }, config)
|
||||
const variant = await variantRepo.findOne(query)
|
||||
|
||||
@@ -142,7 +186,16 @@ class ProductVariantService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
return variant
|
||||
return priceIndex >= 0
|
||||
? ((await this.setAdditionalPrices(
|
||||
variant,
|
||||
config.currency_code,
|
||||
config.region_id,
|
||||
config.cart_id,
|
||||
config.customer_id,
|
||||
config.include_discount_prices
|
||||
)) as ProductVariant)
|
||||
: variant
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,6 +307,7 @@ class ProductVariantService extends BaseService {
|
||||
* The function will throw, if price updates are attempted.
|
||||
* @param {string | ProductVariant} variantOrVariantId - variant or id of a variant.
|
||||
* @param {object} update - an object with the update values.
|
||||
* @param {object} config - an object with the config values for returning the variant.
|
||||
* @return {Promise} resolves to the update result.
|
||||
*/
|
||||
async update(
|
||||
@@ -267,7 +321,17 @@ class ProductVariantService extends BaseService {
|
||||
|
||||
let variant = variantOrVariantId as ProductVariant
|
||||
if (typeof variant === `string`) {
|
||||
variant = await this.retrieve(variantOrVariantId as string)
|
||||
const variantRes = await variantRepo.findOne({
|
||||
where: { id: variantOrVariantId as string },
|
||||
})
|
||||
if (typeof variant === "undefined") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Variant with id ${variantOrVariantId} was not found`
|
||||
)
|
||||
} else {
|
||||
variant = variantRes as ProductVariant
|
||||
}
|
||||
} else if (!variant.id) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
@@ -291,12 +355,12 @@ class ProductVariantService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata) {
|
||||
variant.metadata = this.setMetadata_(variant, metadata)
|
||||
if (typeof metadata === "object") {
|
||||
variant.metadata = this.setMetadata_(variant, metadata as object)
|
||||
}
|
||||
|
||||
if (typeof inventory_quantity === "number") {
|
||||
variant.inventory_quantity = inventory_quantity
|
||||
variant.inventory_quantity = inventory_quantity as number
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(rest)) {
|
||||
@@ -312,6 +376,7 @@ class ProductVariantService extends BaseService {
|
||||
product_id: result.product_id,
|
||||
fields: Object.keys(update),
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
@@ -358,41 +423,29 @@ class ProductVariantService extends BaseService {
|
||||
* exists the function will try to use a currency price. If no default
|
||||
* currency price exists the function will throw an error.
|
||||
* @param {string} variantId - the id of the variant to get price from
|
||||
* @param {string} regionId - the id of the region to get price for
|
||||
* @param {GetRegionPriceContext} context - context for getting region price
|
||||
* @return {number} the price specific to the region
|
||||
*/
|
||||
async getRegionPrice(variantId: string, regionId: string): Promise<number> {
|
||||
async getRegionPrice(
|
||||
variantId: string,
|
||||
context: GetRegionPriceContext
|
||||
): Promise<number> {
|
||||
return this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const moneyAmountRepo = manager.getCustomRepository(
|
||||
this.moneyAmountRepository_
|
||||
)
|
||||
|
||||
const region = await this.regionService_
|
||||
.withTransaction(manager)
|
||||
.retrieve(regionId)
|
||||
.retrieve(context.regionId)
|
||||
|
||||
// Find region price based on region id
|
||||
let moneyAmount = await moneyAmountRepo.findOne({
|
||||
where: { region_id: regionId, variant_id: variantId },
|
||||
})
|
||||
|
||||
// If no price could be find based on region id, we try to fetch
|
||||
// based on the region currency code
|
||||
if (!moneyAmount) {
|
||||
moneyAmount = await moneyAmountRepo.findOne({
|
||||
where: { variant_id: variantId, currency_code: region.currency_code },
|
||||
const prices = await this.priceSelectionStrategy_
|
||||
.withTransaction(manager)
|
||||
.calculateVariantPrice(variantId, {
|
||||
region_id: context.regionId,
|
||||
currency_code: region.currency_code,
|
||||
quantity: context.quantity,
|
||||
customer_id: context.customer_id,
|
||||
include_discount_prices: !!context.include_discount_prices,
|
||||
})
|
||||
}
|
||||
|
||||
// Still, if no price is found, we throw
|
||||
if (!moneyAmount) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`A price for region: ${region.name} could not be found`
|
||||
)
|
||||
}
|
||||
|
||||
return moneyAmount.amount
|
||||
return prices.calculatedPrice
|
||||
})
|
||||
}
|
||||
|
||||
@@ -554,12 +607,23 @@ class ProductVariantService extends BaseService {
|
||||
*/
|
||||
async listAndCount(
|
||||
selector: FilterableProductVariantProps,
|
||||
config: FindConfig<ProductVariant> = { relations: [], skip: 0, take: 20 }
|
||||
config: FindConfig<ProductVariant> & PriceSelectionContext = {
|
||||
relations: [],
|
||||
skip: 0,
|
||||
take: 20,
|
||||
include_discount_prices: false,
|
||||
}
|
||||
): Promise<[ProductVariant[], number]> {
|
||||
const variantRepo = this.manager_.getCustomRepository(
|
||||
this.productVariantRepository_
|
||||
)
|
||||
|
||||
const priceIndex = config.relations?.indexOf("prices") ?? -1
|
||||
if (priceIndex >= 0 && config.relations) {
|
||||
config.relations = [...config.relations]
|
||||
config.relations.splice(priceIndex, 1)
|
||||
}
|
||||
|
||||
const { q, query, relations } = this.prepareListQuery_(selector, config)
|
||||
|
||||
if (q) {
|
||||
@@ -571,10 +635,39 @@ class ProductVariantService extends BaseService {
|
||||
raw.map((i) => i.id),
|
||||
query.withDeleted ?? false
|
||||
)
|
||||
if (priceIndex >= 0) {
|
||||
const res = await this.setAdditionalPrices(
|
||||
variants,
|
||||
config.currency_code,
|
||||
config.region_id,
|
||||
config.cart_id,
|
||||
config.customer_id,
|
||||
config.include_discount_prices
|
||||
)
|
||||
return [res as ProductVariant[], count]
|
||||
}
|
||||
|
||||
return [variants, count]
|
||||
}
|
||||
|
||||
return await variantRepo.findWithRelationsAndCount(relations, query)
|
||||
const [variants, count] = await variantRepo.findWithRelationsAndCount(
|
||||
relations,
|
||||
query
|
||||
)
|
||||
|
||||
if (priceIndex >= 0) {
|
||||
const res = await this.setAdditionalPrices(
|
||||
variants,
|
||||
config.currency_code,
|
||||
config.region_id,
|
||||
config.cart_id,
|
||||
config.customer_id,
|
||||
config.include_discount_prices
|
||||
)
|
||||
return [res as ProductVariant[], count]
|
||||
}
|
||||
|
||||
return [variants, count]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -584,12 +677,22 @@ class ProductVariantService extends BaseService {
|
||||
*/
|
||||
async list(
|
||||
selector: FilterableProductVariantProps,
|
||||
config: FindConfig<ProductVariant> = { relations: [], skip: 0, take: 20 }
|
||||
config: FindConfig<ProductVariant> & PriceSelectionContext = {
|
||||
relations: [],
|
||||
skip: 0,
|
||||
take: 20,
|
||||
}
|
||||
): Promise<ProductVariant[]> {
|
||||
const productVariantRepo = this.manager_.getCustomRepository(
|
||||
this.productVariantRepository_
|
||||
)
|
||||
|
||||
const priceIndex = config.relations?.indexOf("prices") ?? -1
|
||||
if (priceIndex >= 0 && config.relations) {
|
||||
config.relations = [...config.relations]
|
||||
config.relations.splice(priceIndex, 1)
|
||||
}
|
||||
|
||||
let q: string | undefined
|
||||
if ("q" in selector) {
|
||||
q = selector.q
|
||||
@@ -620,7 +723,18 @@ class ProductVariantService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
return await productVariantRepo.find(query)
|
||||
const variants = await productVariantRepo.find(query)
|
||||
|
||||
return priceIndex >= 0
|
||||
? ((await this.setAdditionalPrices(
|
||||
variants,
|
||||
config.currency_code,
|
||||
config.region_id,
|
||||
config.cart_id,
|
||||
config.customer_id,
|
||||
config.include_discount_prices
|
||||
)) as ProductVariant[])
|
||||
: variants
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -770,6 +884,72 @@ class ProductVariantService extends BaseService {
|
||||
|
||||
return qb
|
||||
}
|
||||
|
||||
/**
|
||||
* Set additional prices on a list of variants.
|
||||
* @param {ProductVariant | ProductVariant[] } variant variant on which to set additional prices
|
||||
* @param {string} currency_code currency code to fetch prices for
|
||||
* @param {string} region_id region to fetch prices for
|
||||
* @param {string} cart_id string of cart to use as a basis for getting currency and region
|
||||
* @param {string} customer_id id of potentially logged in customer, used to get prices valid for their customer groups
|
||||
* @param {boolean} include_discount_prices should result include discount pricing
|
||||
* @return {Promise<ProductVariant | ProductVariant[]>} A list of variants with variants decorated with "additional_prices"
|
||||
*/
|
||||
async setAdditionalPrices(
|
||||
variant,
|
||||
currency_code,
|
||||
region_id,
|
||||
cart_id,
|
||||
customer_id,
|
||||
include_discount_prices = false
|
||||
): Promise<ProductVariant | ProductVariant[]> {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const cartRepo = manager.getCustomRepository(this.cartRepository_)
|
||||
|
||||
let regionId = region_id
|
||||
let currencyCode = currency_code
|
||||
|
||||
if (cart_id) {
|
||||
const cart = await cartRepo.findOne({
|
||||
where: { id: cart_id },
|
||||
relations: ["region"],
|
||||
})
|
||||
|
||||
regionId = cart.region.id
|
||||
currencyCode = cart.region.currency_code
|
||||
}
|
||||
|
||||
const variantArray = Array.isArray(variant) ? variant : [variant]
|
||||
|
||||
const priceSelectionStrategy =
|
||||
this.priceSelectionStrategy_.withTransaction(manager)
|
||||
|
||||
const variantsWithPrices = await Promise.all(
|
||||
variantArray.map(async (v) => {
|
||||
const prices = await priceSelectionStrategy.calculateVariantPrice(
|
||||
v.id,
|
||||
{
|
||||
region_id: regionId,
|
||||
currency_code: currencyCode,
|
||||
cart_id: cart_id,
|
||||
customer_id: customer_id,
|
||||
include_discount_prices: include_discount_prices,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
...v,
|
||||
prices: prices.prices,
|
||||
original_price: prices.originalPrice,
|
||||
calculated_price: prices.calculatedPrice,
|
||||
calculated_price_type: prices.calculatedPriceType,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return Array.isArray(variant) ? variantsWithPrices : variantsWithPrices[0]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductVariantService
|
||||
@@ -27,6 +27,8 @@ class ProductService extends BaseService {
|
||||
productTagRepository,
|
||||
imageRepository,
|
||||
searchService,
|
||||
cartRepository,
|
||||
priceSelectionStrategy,
|
||||
}) {
|
||||
super()
|
||||
|
||||
@@ -62,6 +64,12 @@ class ProductService extends BaseService {
|
||||
|
||||
/** @private @const {SearchService} */
|
||||
this.searchService_ = searchService
|
||||
|
||||
/** @private @const {CartRepository} */
|
||||
this.cartRepository_ = cartRepository
|
||||
|
||||
/** @private @const {IPriceSelectionStrategy} */
|
||||
this.priceSelectionStrategy_ = priceSelectionStrategy
|
||||
}
|
||||
|
||||
withTransaction(transactionManager) {
|
||||
@@ -80,6 +88,8 @@ class ProductService extends BaseService {
|
||||
productTagRepository: this.productTagRepository_,
|
||||
productTypeRepository: this.productTypeRepository_,
|
||||
imageRepository: this.imageRepository_,
|
||||
cartRepository: this.cartRepository_,
|
||||
priceSelectionStrategy: this.priceSelectionStrategy_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
@@ -95,10 +105,23 @@ class ProductService extends BaseService {
|
||||
* returned
|
||||
* @return {Promise<Product[]>} the result of the find operation
|
||||
*/
|
||||
async list(selector = {}, config = { relations: [], skip: 0, take: 20 }) {
|
||||
async list(
|
||||
selector = {},
|
||||
config = {
|
||||
relations: [],
|
||||
skip: 0,
|
||||
take: 20,
|
||||
include_discount_prices: false,
|
||||
}
|
||||
) {
|
||||
const productRepo = this.manager_.getCustomRepository(
|
||||
this.productRepository_
|
||||
)
|
||||
const priceIndex = config.relations?.indexOf("variants.prices") ?? -1
|
||||
if (priceIndex >= 0 && config.relations) {
|
||||
config.relations = [...config.relations]
|
||||
config.relations.splice(priceIndex, 1)
|
||||
}
|
||||
|
||||
const { q, query, relations } = this.prepareListQuery_(selector, config)
|
||||
|
||||
@@ -112,7 +135,18 @@ class ProductService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
return productRepo.findWithRelations(relations, query)
|
||||
const products = productRepo.findWithRelations(relations, query)
|
||||
|
||||
return priceIndex > -1
|
||||
? await this.setAdditionalPrices(
|
||||
products,
|
||||
config.currency_code,
|
||||
config.region_id,
|
||||
config.cart_id,
|
||||
config.customer_id,
|
||||
config.include_discount_prices
|
||||
)
|
||||
: products
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,12 +162,23 @@ class ProductService extends BaseService {
|
||||
*/
|
||||
async listAndCount(
|
||||
selector = {},
|
||||
config = { relations: [], skip: 0, take: 20 }
|
||||
config = {
|
||||
relations: [],
|
||||
skip: 0,
|
||||
take: 20,
|
||||
include_discount_prices: false,
|
||||
}
|
||||
) {
|
||||
const productRepo = this.manager_.getCustomRepository(
|
||||
this.productRepository_
|
||||
)
|
||||
|
||||
const priceIndex = config.relations?.indexOf("variants.prices") ?? -1
|
||||
if (priceIndex >= 0 && config.relations) {
|
||||
config.relations = [...config.relations]
|
||||
config.relations.splice(priceIndex, 1)
|
||||
}
|
||||
|
||||
const { q, query, relations } = this.prepareListQuery_(selector, config)
|
||||
|
||||
if (q) {
|
||||
@@ -148,7 +193,25 @@ class ProductService extends BaseService {
|
||||
return [products, count]
|
||||
}
|
||||
|
||||
return await productRepo.findWithRelationsAndCount(relations, query)
|
||||
const [products, count] = await productRepo.findWithRelationsAndCount(
|
||||
relations,
|
||||
query
|
||||
)
|
||||
|
||||
if (priceIndex > -1) {
|
||||
const productsWithAdditionalPrices = await this.setAdditionalPrices(
|
||||
products,
|
||||
config.currency_code,
|
||||
config.region_id,
|
||||
config.cart_id,
|
||||
config.customer_id,
|
||||
config.include_discount_prices
|
||||
)
|
||||
|
||||
return [productsWithAdditionalPrices, count]
|
||||
} else {
|
||||
return [products, count]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,12 +235,18 @@ class ProductService extends BaseService {
|
||||
* query response
|
||||
* @return {Promise<Product>} the result of the find one operation.
|
||||
*/
|
||||
async retrieve(productId, config = {}) {
|
||||
async retrieve(productId, config = { include_discount_prices: false }) {
|
||||
const productRepo = this.manager_.getCustomRepository(
|
||||
this.productRepository_
|
||||
)
|
||||
const validatedId = this.validateId_(productId)
|
||||
|
||||
const priceIndex = config.relations?.indexOf("variants.prices") ?? -1
|
||||
if (priceIndex >= 0 && config.relations) {
|
||||
config.relations = [...config.relations]
|
||||
config.relations.splice(priceIndex, 1)
|
||||
}
|
||||
|
||||
const query = { where: { id: validatedId } }
|
||||
|
||||
if (config.relations && config.relations.length > 0) {
|
||||
@@ -199,7 +268,16 @@ class ProductService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
return product
|
||||
return priceIndex > -1
|
||||
? await this.setAdditionalPrices(
|
||||
product,
|
||||
config.currency_code,
|
||||
config.region_id,
|
||||
config.cart_id,
|
||||
config.customer_id,
|
||||
config.include_discount_prices
|
||||
)
|
||||
: product
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,6 +292,12 @@ class ProductService extends BaseService {
|
||||
this.productRepository_
|
||||
)
|
||||
|
||||
const priceIndex = config.relations?.indexOf("variants.prices") ?? -1
|
||||
if (priceIndex >= 0 && config.relations) {
|
||||
config.relations = [...config.relations]
|
||||
config.relations.splice(priceIndex, 1)
|
||||
}
|
||||
|
||||
const query = { where: { handle: productHandle } }
|
||||
|
||||
if (config.relations && config.relations.length > 0) {
|
||||
@@ -235,7 +319,16 @@ class ProductService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
return product
|
||||
return priceIndex > -1
|
||||
? await this.setAdditionalPrices(
|
||||
product,
|
||||
config.currency_code,
|
||||
config.region_id,
|
||||
config.cart_id,
|
||||
config.customer_id,
|
||||
config.include_discount_prices
|
||||
)
|
||||
: product
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,6 +343,12 @@ class ProductService extends BaseService {
|
||||
this.productRepository_
|
||||
)
|
||||
|
||||
const priceIndex = config.relations?.indexOf("variants.prices") ?? -1
|
||||
if (priceIndex >= 0 && config.relations) {
|
||||
config.relations = [...config.relations]
|
||||
config.relations.splice(priceIndex, 1)
|
||||
}
|
||||
|
||||
const query = { where: { external_id: externalId } }
|
||||
|
||||
if (config.relations && config.relations.length > 0) {
|
||||
@@ -271,7 +370,16 @@ class ProductService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
return product
|
||||
return priceIndex > -1
|
||||
? await this.setAdditionalPrices(
|
||||
product,
|
||||
config.currency_code,
|
||||
config.region_id,
|
||||
config.cart_id,
|
||||
config.customer_id,
|
||||
config.include_discount_prices
|
||||
)
|
||||
: product
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -535,6 +643,7 @@ class ProductService extends BaseService {
|
||||
}
|
||||
|
||||
const result = await productRepo.save(product)
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(ProductService.Events.UPDATED, {
|
||||
@@ -821,11 +930,18 @@ class ProductService extends BaseService {
|
||||
* @param {string} productId - the productId to decorate.
|
||||
* @param {string[]} fields - the fields to include.
|
||||
* @param {string[]} expandFields - fields to expand.
|
||||
* @param {object} config - retrieve config for price calculation.
|
||||
* @return {Product} return the decorated product.
|
||||
*/
|
||||
async decorate(productId, fields = [], expandFields = []) {
|
||||
async decorate(productId, fields = [], expandFields = [], config = {}) {
|
||||
const requiredFields = ["id", "metadata"]
|
||||
|
||||
const priceIndex = expandFields.indexOf("variants.prices") ?? -1
|
||||
if (priceIndex >= 0 && expandFields.length) {
|
||||
expandFields = [...expandFields]
|
||||
expandFields.splice(priceIndex, 1)
|
||||
}
|
||||
|
||||
fields = fields.concat(requiredFields)
|
||||
|
||||
const product = await this.retrieve(productId, {
|
||||
@@ -833,8 +949,16 @@ class ProductService extends BaseService {
|
||||
relations: expandFields,
|
||||
})
|
||||
|
||||
// const final = await this.runDecorators_(decorated)
|
||||
return product
|
||||
return priceIndex > -1
|
||||
? await this.setAdditionalPrices(
|
||||
product,
|
||||
config.currency_code,
|
||||
config.region_id,
|
||||
config.cart_id,
|
||||
config.customer_id,
|
||||
config.include_discount_prices
|
||||
)
|
||||
: product
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -908,6 +1032,80 @@ class ProductService extends BaseService {
|
||||
|
||||
return qb
|
||||
}
|
||||
|
||||
/**
|
||||
* Set additional prices on a list of products.
|
||||
* @param {Product[] | Product} products list of products on which to set additional prices
|
||||
* @param {string} currency_code currency code to fetch prices for
|
||||
* @param {string} region_id region to fetch prices for
|
||||
* @param {string} cart_id string of cart to use as a basis for getting currency and region
|
||||
* @param {string} customer_id id of potentially logged in customer, used to get prices valid for their customer groups
|
||||
* @param {boolean} include_discount_prices indication wether or not to include sales prices in result
|
||||
* @return {Promise<Product[]>} A list of products with variants decorated with "additional_prices"
|
||||
*/
|
||||
async setAdditionalPrices(
|
||||
products,
|
||||
currency_code,
|
||||
region_id,
|
||||
cart_id,
|
||||
customer_id,
|
||||
include_discount_prices = false
|
||||
) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const cartRepo = this.manager_.getCustomRepository(this.cartRepository_)
|
||||
|
||||
let regionId = region_id
|
||||
let currencyCode = currency_code
|
||||
|
||||
if (cart_id) {
|
||||
const cart = await cartRepo.findOne({
|
||||
where: { id: cart_id },
|
||||
relations: ["region"],
|
||||
})
|
||||
|
||||
regionId = cart.region.id
|
||||
currencyCode = cart.region.currency_code
|
||||
}
|
||||
|
||||
const productArray = Array.isArray(products) ? products : [products]
|
||||
|
||||
const priceSelectionStrategy =
|
||||
this.priceSelectionStrategy_.withTransaction(manager)
|
||||
|
||||
const productsWithPrices = await Promise.all(
|
||||
productArray.map(async (p) => {
|
||||
if (p.variants?.length) {
|
||||
p.variants = await Promise.all(
|
||||
p.variants.map(async (v) => {
|
||||
const prices =
|
||||
await priceSelectionStrategy.calculateVariantPrice(v.id, {
|
||||
region_id: regionId,
|
||||
currency_code: currencyCode,
|
||||
cart_id: cart_id,
|
||||
customer_id: customer_id,
|
||||
include_discount_prices,
|
||||
})
|
||||
|
||||
return {
|
||||
...v,
|
||||
prices: prices.prices,
|
||||
original_price: prices.originalPrice,
|
||||
calculated_price: prices.calculatedPrice,
|
||||
calculated_price_type: prices.calculatedPriceType,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return p
|
||||
})
|
||||
)
|
||||
|
||||
return Array.isArray(products)
|
||||
? productsWithPrices
|
||||
: productsWithPrices[0]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductService
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "../interfaces/price-selection-strategy"
|
||||
import { MoneyAmountRepository } from "../repositories/money-amount"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
class PriceSelectionStrategy extends AbstractPriceSelectionStrategy {
|
||||
private moneyAmountRepository_: typeof MoneyAmountRepository
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { IsOptional, IsString } from "class-validator"
|
||||
|
||||
export class PriceSelectionParams {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
cart_id?: string
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
region_id?: string
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
currency_code?: string
|
||||
}
|
||||
@@ -25,6 +25,13 @@ export type ProductVariantPrice = {
|
||||
max_quantity?: number
|
||||
}
|
||||
|
||||
export type GetRegionPriceContext = {
|
||||
regionId: string
|
||||
quantity?: number
|
||||
customer_id?: string
|
||||
include_discount_prices?: boolean
|
||||
}
|
||||
|
||||
export type ProductVariantOption = {
|
||||
option_id: string
|
||||
value: string
|
||||
|
||||
Reference in new issue
Block a user