feat: Implement PriceList and extend MoneyAmount (#1152)
* init * added buld id validation to repo * admin done * updated price reqs * intial implementation of PriceList * integration tests for price lists * updated admin/product integration tests * update updateVariantPrices method * remove comment from error handler * add integration test for batch deleting prices associated with price list * make update to prices through variant service limited to default prices * update store/products.js snapshot * add api unit tests and update product integration tests to validate that prices from Price List are ignored * fix product test * requested changes * cascade * ensure delete variant cascades to MoneyAmount * addresses PR feedback * removed unused endpoint * update mock * fix failing store integration tests * remove medusajs ressource * re add env.template * Update integration-tests/api/__tests__/admin/price-list.js Co-authored-by: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> * Update integration-tests/api/__tests__/admin/price-list.js Co-authored-by: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> * fix: update snapshots Co-authored-by: Sebastian Rindom <skrindom@gmail.com> Co-authored-by: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com>
This commit is contained in:
co-authored by
Philip Korsholm
Sebastian Rindom
parent
23f8399c16
commit
5300926db8
@@ -1,33 +1,33 @@
|
||||
import { Router } from "express"
|
||||
import cors from "cors"
|
||||
|
||||
import { Router } from "express"
|
||||
import middlewares from "../../middlewares"
|
||||
import appRoutes from "./apps"
|
||||
import authRoutes from "./auth"
|
||||
import productRoutes from "./products"
|
||||
import userRoutes, { unauthenticatedUserRoutes } from "./users"
|
||||
import collectionRoutes from "./collections"
|
||||
import customerGroupRoutes from "./customer-groups"
|
||||
import customerRoutes from "./customers"
|
||||
import discountRoutes from "./discounts"
|
||||
import draftOrderRoutes from "./draft-orders"
|
||||
import giftCardRoutes from "./gift-cards"
|
||||
import inviteRoutes, { unauthenticatedInviteRoutes } from "./invites"
|
||||
import noteRoutes from "./notes"
|
||||
import notificationRoutes from "./notifications"
|
||||
import orderRoutes from "./orders"
|
||||
import priceListRoutes from "./price-lists"
|
||||
import productTagRoutes from "./product-tags"
|
||||
import productTypesRoutes from "./product-types"
|
||||
import productRoutes from "./products"
|
||||
import regionRoutes from "./regions"
|
||||
import returnReasonRoutes from "./return-reasons"
|
||||
import returnRoutes from "./returns"
|
||||
import shippingOptionRoutes from "./shipping-options"
|
||||
import shippingProfileRoutes from "./shipping-profiles"
|
||||
import discountRoutes from "./discounts"
|
||||
import giftCardRoutes from "./gift-cards"
|
||||
import orderRoutes from "./orders"
|
||||
import returnReasonRoutes from "./return-reasons"
|
||||
import storeRoutes from "./store"
|
||||
import uploadRoutes from "./uploads"
|
||||
import customerRoutes from "./customers"
|
||||
import appRoutes from "./apps"
|
||||
import swapRoutes from "./swaps"
|
||||
import returnRoutes from "./returns"
|
||||
import variantRoutes from "./variants"
|
||||
import draftOrderRoutes from "./draft-orders"
|
||||
import collectionRoutes from "./collections"
|
||||
import productTagRoutes from "./product-tags"
|
||||
import notificationRoutes from "./notifications"
|
||||
import noteRoutes from "./notes"
|
||||
import taxRateRoutes from "./tax-rates"
|
||||
import productTypesRoutes from "./product-types"
|
||||
import customerGroupRoutes from "./customer-groups"
|
||||
import uploadRoutes from "./uploads"
|
||||
import userRoutes, { unauthenticatedUserRoutes } from "./users"
|
||||
import variantRoutes from "./variants"
|
||||
|
||||
const route = Router()
|
||||
|
||||
@@ -86,6 +86,7 @@ export default (app, container, config) => {
|
||||
inviteRoutes(route)
|
||||
taxRateRoutes(route)
|
||||
customerGroupRoutes(route)
|
||||
priceListRoutes(route)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { PriceListServiceMock } from "../../../../../services/__mocks__/price-list"
|
||||
|
||||
describe("POST /price-lists/:id/prices/batch", () => {
|
||||
describe("successfully adds several new prices to a price list", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/admin/price-lists/pl_1234/prices/batch`,
|
||||
{
|
||||
payload: {
|
||||
prices: [
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 500,
|
||||
min_quantity: 10,
|
||||
max_quantity: 20,
|
||||
variant_id: "variant_12",
|
||||
},
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 430,
|
||||
min_quantity: 21,
|
||||
max_quantity: 40,
|
||||
variant_id: "variant_12",
|
||||
},
|
||||
],
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("calls PriceListService addPrices", () => {
|
||||
expect(PriceListServiceMock.addPrices).toHaveBeenCalledTimes(1)
|
||||
expect(PriceListServiceMock.addPrices).toHaveBeenCalledWith(
|
||||
"pl_1234",
|
||||
[
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 500,
|
||||
min_quantity: 10,
|
||||
max_quantity: 20,
|
||||
variant_id: "variant_12",
|
||||
},
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 430,
|
||||
min_quantity: 21,
|
||||
max_quantity: 40,
|
||||
variant_id: "variant_12",
|
||||
},
|
||||
],
|
||||
undefined
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("fails if no prices were provided", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/admin/price-lists/pl_1234/prices/batch`,
|
||||
{
|
||||
payload: {},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("returns 400", () => {
|
||||
expect(subject.status).toEqual(400)
|
||||
})
|
||||
|
||||
it("returns descriptive error that name is missing", () => {
|
||||
expect(subject.body.type).toEqual("invalid_data")
|
||||
expect(subject.body.message).toEqual("prices must be an array")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,119 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { PriceListServiceMock } from "../../../../../services/__mocks__/price-list"
|
||||
|
||||
describe("POST /price-lists", () => {
|
||||
describe("successfully creates a price list", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request("POST", `/admin/price-lists`, {
|
||||
payload: {
|
||||
name: "My Price List",
|
||||
description: "testing",
|
||||
ends_at: "2022-03-14T08:28:38.551Z",
|
||||
starts_at: "2022-03-14T08:28:38.551Z",
|
||||
customer_groups: [
|
||||
{
|
||||
id: "gc_123",
|
||||
},
|
||||
],
|
||||
type: "sale",
|
||||
prices: [
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 500,
|
||||
min_quantity: 10,
|
||||
max_quantity: 20,
|
||||
variant_id: "variant_12",
|
||||
},
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 430,
|
||||
min_quantity: 21,
|
||||
max_quantity: 40,
|
||||
variant_id: "variant_12",
|
||||
},
|
||||
],
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("calls PriceListService addPrices", () => {
|
||||
expect(PriceListServiceMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(PriceListServiceMock.create).toHaveBeenCalledWith({
|
||||
name: "My Price List",
|
||||
description: "testing",
|
||||
ends_at: "2022-03-14T08:28:38.551Z",
|
||||
starts_at: "2022-03-14T08:28:38.551Z",
|
||||
customer_groups: [
|
||||
{
|
||||
id: "gc_123",
|
||||
},
|
||||
],
|
||||
type: "sale",
|
||||
prices: [
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 500,
|
||||
min_quantity: 10,
|
||||
max_quantity: 20,
|
||||
variant_id: "variant_12",
|
||||
},
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 430,
|
||||
min_quantity: 21,
|
||||
max_quantity: 40,
|
||||
variant_id: "variant_12",
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("fails if required fields are missing", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request("POST", `/admin/price-lists`, {
|
||||
payload: {
|
||||
description: "bad payload",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("returns 400", () => {
|
||||
expect(subject.status).toEqual(400)
|
||||
})
|
||||
|
||||
it("returns descriptive error that several fields are missing", () => {
|
||||
expect(subject.body.type).toEqual("invalid_data")
|
||||
expect(subject.body.message).toEqual(
|
||||
"name must be a string, type must be a valid enum value, prices must be an array"
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { PriceListServiceMock } from "../../../../../services/__mocks__/price-list"
|
||||
|
||||
describe("POST /price-lists/:id/prices/batch", () => {
|
||||
describe("successfully adds several new prices to a price list", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request("DELETE", `/admin/price-lists/pl_1234`, {
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("calls PriceListService addPrices", () => {
|
||||
expect(PriceListServiceMock.delete).toHaveBeenCalledTimes(1)
|
||||
expect(PriceListServiceMock.delete).toHaveBeenCalledWith("pl_1234")
|
||||
|
||||
expect(subject.body).toEqual({
|
||||
id: "pl_1234",
|
||||
object: "price-list",
|
||||
deleted: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,76 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { PriceListServiceMock } from "../../../../../services/__mocks__/price-list"
|
||||
|
||||
describe("DELETE /price-lists/:id/prices/batch", () => {
|
||||
describe("successfully adds several new prices to a price list", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"DELETE",
|
||||
`/admin/price-lists/pl_1234/prices/batch`,
|
||||
{
|
||||
payload: {
|
||||
price_ids: ["price_1234", "price_1235", "price_1236", "price_1237"],
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("calls PriceListService addPrices", () => {
|
||||
expect(PriceListServiceMock.deletePrices).toHaveBeenCalledTimes(1)
|
||||
expect(PriceListServiceMock.deletePrices).toHaveBeenCalledWith(
|
||||
"pl_1234",
|
||||
["price_1234", "price_1235", "price_1236", "price_1237"]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("fails if no prices were provided", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"DELETE",
|
||||
`/admin/price-lists/pl_1234/prices/batch`,
|
||||
{
|
||||
payload: {},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("returns 400", () => {
|
||||
expect(subject.status).toEqual(400)
|
||||
})
|
||||
|
||||
it("returns descriptive error that price_ids is missing", () => {
|
||||
expect(subject.body.type).toEqual("invalid_data")
|
||||
expect(subject.body.message).toEqual(
|
||||
"each value in price_ids must be a string, price_ids should not be empty"
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from ".."
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { PriceListServiceMock } from "../../../../../services/__mocks__/price-list"
|
||||
|
||||
describe("GET /price-lists/:id", () => {
|
||||
describe("", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request("GET", `/admin/price-lists/pl_1234`, {
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("calls PriceListService retrieve", () => {
|
||||
expect(PriceListServiceMock.retrieve).toHaveBeenCalledTimes(1)
|
||||
expect(PriceListServiceMock.retrieve).toHaveBeenCalledWith("pl_1234", {
|
||||
relations: defaultAdminPriceListRelations,
|
||||
select: defaultAdminPriceListFields,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { PriceListServiceMock } from "../../../../../services/__mocks__/price-list"
|
||||
|
||||
describe("GET /price-lists", () => {
|
||||
describe("successfully lists price lists", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request("GET", `/admin/price-lists`, {
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("calls PriceListService listAndCount", () => {
|
||||
expect(PriceListServiceMock.listAndCount).toHaveBeenCalledTimes(1)
|
||||
expect(PriceListServiceMock.listAndCount).toHaveBeenCalledWith(
|
||||
{
|
||||
created_at: undefined,
|
||||
deleted_at: undefined,
|
||||
description: undefined,
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
q: undefined,
|
||||
status: undefined,
|
||||
type: undefined,
|
||||
updated_at: undefined,
|
||||
},
|
||||
{ order: { created_at: "DESC" }, relations: [], skip: 0, take: 10 }
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { PriceListServiceMock } from "../../../../../services/__mocks__/price-list"
|
||||
|
||||
describe("POST /price-lists/:id", () => {
|
||||
describe("successfully updates a price list", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request("POST", `/admin/price-lists/pl_1234`, {
|
||||
payload: {
|
||||
description: "new description",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("calls PriceListService update", () => {
|
||||
expect(PriceListServiceMock.update).toHaveBeenCalledTimes(1)
|
||||
expect(PriceListServiceMock.update).toHaveBeenCalledWith("pl_1234", {
|
||||
description: "new description",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Type } from "class-transformer"
|
||||
import { IsArray, IsBoolean, IsOptional, ValidateNested } from "class-validator"
|
||||
import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from "."
|
||||
import { PriceList } from "../../../.."
|
||||
import PriceListService from "../../../../services/price-list"
|
||||
import { AdminPriceListPricesUpdateReq } from "../../../../types/price-list"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [post] /price-lists/{id}/prices/batch
|
||||
* operationId: "PostPriceListsPriceListPricesBatch"
|
||||
* summary: "Batch update prices for a Price List"
|
||||
* description: "Batch update prices for a Price List"
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (path) id=* {string} The id of the Price List to update prices for.
|
||||
* requestBody:
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* prices:
|
||||
* description: The prices to update or add.
|
||||
* type: array
|
||||
* items:
|
||||
* properties:
|
||||
* id:
|
||||
* description: The id of the price.
|
||||
* type: string
|
||||
* status:
|
||||
* description: The status of the Price List.
|
||||
* type: string
|
||||
* enum:
|
||||
* - active
|
||||
* - draft
|
||||
* region_id:
|
||||
* description: The id of the Region for which the price is used.
|
||||
* type: string
|
||||
* currency_code:
|
||||
* description: The 3 character ISO currency code for which the price will be used.
|
||||
* type: string
|
||||
* amount:
|
||||
* description: The amount of the price.
|
||||
* type: number
|
||||
* min_quantity:
|
||||
* description: The minimum quantity for which the price will be used.
|
||||
* type: number
|
||||
* max_quantity:
|
||||
* description: The maximum quantity for which the price will be used.
|
||||
* type: number
|
||||
* override:
|
||||
* description: "If true the prices will replace all existing prices associated with the Price List."
|
||||
* type: boolean
|
||||
* tags:
|
||||
* - Price List
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* id:
|
||||
* type: string
|
||||
* description: The id of the deleted Price List.
|
||||
* object:
|
||||
* type: string
|
||||
* description: The type of the object that was deleted.
|
||||
* deleted:
|
||||
* type: boolean
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const validated = await validator(AdminPostPriceListPricesPricesReq, req.body)
|
||||
|
||||
const priceListService: PriceListService =
|
||||
req.scope.resolve("priceListService")
|
||||
|
||||
await priceListService.addPrices(id, validated.prices, validated.override)
|
||||
|
||||
const priceList = await priceListService.retrieve(id, {
|
||||
select: defaultAdminPriceListFields as (keyof PriceList)[],
|
||||
relations: defaultAdminPriceListRelations,
|
||||
})
|
||||
|
||||
res.json({ price_list: priceList })
|
||||
}
|
||||
|
||||
export class AdminPostPriceListPricesPricesReq {
|
||||
@IsArray()
|
||||
@Type(() => AdminPriceListPricesUpdateReq)
|
||||
@ValidateNested({ each: true })
|
||||
prices: AdminPriceListPricesUpdateReq[]
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
override?: boolean
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Type } from "class-transformer"
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import PriceListService from "../../../../services/price-list"
|
||||
import {
|
||||
AdminPriceListPricesCreateReq,
|
||||
PriceListStatus,
|
||||
PriceListType,
|
||||
} from "../../../../types/price-list"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [post] /price_lists
|
||||
* operationId: "PostPriceListsPriceList"
|
||||
* summary: "Creates a Price List"
|
||||
* description: "Creates a Price List"
|
||||
* x-authenticated: true
|
||||
* requestBody:
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* name:
|
||||
* description: "The name of the Price List"
|
||||
* type: string
|
||||
* description:
|
||||
* description: "A description of the Price List."
|
||||
* type: string
|
||||
* type:
|
||||
* description: The type of the Price List.
|
||||
* type: string
|
||||
* enum:
|
||||
* - sale
|
||||
* - override
|
||||
* status:
|
||||
* description: The status of the Price List.
|
||||
* type: string
|
||||
* enum:
|
||||
* - active
|
||||
* - draft
|
||||
* prices:
|
||||
* description: The prices of the Price List.
|
||||
* type: array
|
||||
* items:
|
||||
* properties:
|
||||
* region_id:
|
||||
* description: The id of the Region for which the price is used.
|
||||
* type: string
|
||||
* currency_code:
|
||||
* description: The 3 character ISO currency code for which the price will be used.
|
||||
* type: string
|
||||
* amount:
|
||||
* description: The amount to charge for the Product Variant.
|
||||
* type: integer
|
||||
* min_quantity:
|
||||
* description: The minimum quantity for which the price will be used.
|
||||
* type: integer
|
||||
* max_quantity:
|
||||
* description: The maximum quantity for which the price will be used.
|
||||
* type: integer
|
||||
* customer_groups:
|
||||
* type: array
|
||||
* description: A list of customer groups that the Price List applies to.
|
||||
* items:
|
||||
* required:
|
||||
* - id
|
||||
* properties:
|
||||
* id:
|
||||
* description: The id of a customer group
|
||||
* type: string
|
||||
* tags:
|
||||
* - Price List
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* product:
|
||||
* $ref: "#/components/schemas/price_list"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const validated = await validator(AdminPostPriceListsPriceListReq, req.body)
|
||||
|
||||
const priceListService: PriceListService =
|
||||
req.scope.resolve("priceListService")
|
||||
|
||||
const priceList = await priceListService.create(validated)
|
||||
|
||||
res.json({ price_list: priceList })
|
||||
}
|
||||
|
||||
class CustomerGroup {
|
||||
@IsString()
|
||||
id: string
|
||||
}
|
||||
|
||||
export class AdminPostPriceListsPriceListReq {
|
||||
@IsString()
|
||||
name: string
|
||||
|
||||
@IsString()
|
||||
description: string
|
||||
|
||||
@IsOptional()
|
||||
starts_at?: Date
|
||||
|
||||
@IsOptional()
|
||||
ends_at?: Date
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(PriceListStatus)
|
||||
status?: PriceListStatus
|
||||
|
||||
@IsEnum(PriceListType)
|
||||
type: PriceListType
|
||||
|
||||
@IsArray()
|
||||
@Type(() => AdminPriceListPricesCreateReq)
|
||||
@ValidateNested({ each: true })
|
||||
prices: AdminPriceListPricesCreateReq[]
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@Type(() => CustomerGroup)
|
||||
@ValidateNested({ each: true })
|
||||
customer_groups?: CustomerGroup[]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import PriceListService from "../../../../services/price-list"
|
||||
|
||||
/**
|
||||
* @oas [delete] /price-lists/{id}
|
||||
* operationId: "DeletePriceListsPriceList"
|
||||
* summary: "Delete a Price List"
|
||||
* description: "Deletes a Price List"
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (path) id=* {string} The id of the Price List to delete.
|
||||
* tags:
|
||||
* - Price List
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* id:
|
||||
* type: string
|
||||
* description: The id of the deleted Price List.
|
||||
* object:
|
||||
* type: string
|
||||
* description: The type of the object that was deleted.
|
||||
* deleted:
|
||||
* type: boolean
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const priceListService: PriceListService =
|
||||
req.scope.resolve("priceListService")
|
||||
await priceListService.delete(id)
|
||||
|
||||
res.json({
|
||||
id,
|
||||
object: "price-list",
|
||||
deleted: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { ArrayNotEmpty, IsString } from "class-validator"
|
||||
import PriceListService from "../../../../services/price-list"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [delete] /price-lists/{id}/prices/batch
|
||||
* operationId: "DeletePriceListsPriceListPricesBatch"
|
||||
* summary: "Batch delete prices that belongs to a Price List"
|
||||
* description: "Batch delete prices that belongs to 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.
|
||||
* requestBody:
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* price_ids:
|
||||
* description: The price id's of the Money Amounts to delete.
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* tags:
|
||||
* - Price List
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* ids:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: The id of the deleted Money Amount.
|
||||
* object:
|
||||
* type: string
|
||||
* description: The type of the object that was deleted.
|
||||
* deleted:
|
||||
* type: boolean
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const validated = await validator(
|
||||
AdminDeletePriceListPricesPricesReq,
|
||||
req.body
|
||||
)
|
||||
|
||||
const priceListService: PriceListService =
|
||||
req.scope.resolve("priceListService")
|
||||
|
||||
await priceListService.deletePrices(id, validated.price_ids)
|
||||
|
||||
res.json({ ids: validated.price_ids, object: "money-amount", deleted: true })
|
||||
}
|
||||
|
||||
export class AdminDeletePriceListPricesPricesReq {
|
||||
@ArrayNotEmpty()
|
||||
@IsString({ each: true })
|
||||
price_ids: string[]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from "."
|
||||
import { PriceList } from "../../../.."
|
||||
import PriceListService from "../../../../services/price-list"
|
||||
|
||||
/**
|
||||
* @oas [get] /price-lists/{id}
|
||||
* operationId: "GetPriceListsPriceList"
|
||||
* summary: "Retrieve a Price List"
|
||||
* description: "Retrieves a Price List."
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (path) id=* {string} The id of the Price List.
|
||||
* tags:
|
||||
* - Price List
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* price_list:
|
||||
* $ref: "#/components/schemas/price_list"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const priceListService: PriceListService =
|
||||
req.scope.resolve("priceListService")
|
||||
|
||||
const priceList = await priceListService.retrieve(id, {
|
||||
select: defaultAdminPriceListFields as (keyof PriceList)[],
|
||||
relations: defaultAdminPriceListRelations,
|
||||
})
|
||||
|
||||
res.status(200).json({ price_list: priceList })
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Router } from "express"
|
||||
import "reflect-metadata"
|
||||
import { PriceList } from "../../../.."
|
||||
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
||||
import middlewares from "../../../middlewares"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default (app) => {
|
||||
app.use("/price-lists", route)
|
||||
|
||||
route.get("/:id", middlewares.wrap(require("./get-price-list").default))
|
||||
|
||||
route.get("/", middlewares.wrap(require("./list-price-lists").default))
|
||||
|
||||
route.post("/", middlewares.wrap(require("./create-price-list").default))
|
||||
|
||||
route.post("/:id", middlewares.wrap(require("./update-price-list").default))
|
||||
|
||||
route.delete("/:id", middlewares.wrap(require("./delete-price-list").default))
|
||||
|
||||
route.delete(
|
||||
"/:id/prices/batch",
|
||||
middlewares.wrap(require("./delete-prices-batch").default)
|
||||
)
|
||||
|
||||
route.post(
|
||||
"/:id/prices/batch",
|
||||
middlewares.wrap(require("./add-prices-batch").default)
|
||||
)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
export const defaultAdminPriceListFields = [
|
||||
"id",
|
||||
"name",
|
||||
"description",
|
||||
"type",
|
||||
"status",
|
||||
"starts_at",
|
||||
"ends_at",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
]
|
||||
|
||||
export const defaultAdminPriceListRelations = ["prices", "customer_groups"]
|
||||
|
||||
export const allowedAdminPriceListFields = ["prices", "customer_groups"]
|
||||
|
||||
export type AdminPriceListRes = {
|
||||
price_list: PriceList
|
||||
}
|
||||
|
||||
export type AdminPriceListDeleteRes = DeleteResponse
|
||||
|
||||
export type AdminPriceListsListRes = PaginatedResponse & {
|
||||
price_lists: PriceList[]
|
||||
}
|
||||
|
||||
export * from "./add-prices-batch"
|
||||
export * from "./create-price-list"
|
||||
export * from "./delete-price-list"
|
||||
export * from "./get-price-list"
|
||||
export * from "./list-price-lists"
|
||||
export * from "./update-price-list"
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Type } from "class-transformer"
|
||||
import { IsNumber, IsOptional, IsString } from "class-validator"
|
||||
import omit from "lodash/omit"
|
||||
import { PriceList } from "../../../.."
|
||||
import PriceListService from "../../../../services/price-list"
|
||||
import { FindConfig } from "../../../../types/common"
|
||||
import { FilterablePriceListProps } from "../../../../types/price-list"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
/**
|
||||
* @oas [get] /price-lists
|
||||
* operationId: "GetPriceLists"
|
||||
* summary: "List Price Lists"
|
||||
* description: "Retrieves a list of Price Lists."
|
||||
* x-authenticated: true
|
||||
* tags:
|
||||
* - Price List
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* price_lists:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/price_list"
|
||||
* count:
|
||||
* description: The number of Price Lists.
|
||||
* type: integer
|
||||
* offset:
|
||||
* description: The offset of the Price List query.
|
||||
* type: integer
|
||||
* limit:
|
||||
* description: The limit of the Price List query.
|
||||
* type: integer
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const validated = await validator(
|
||||
AdminGetPriceListPaginationParams,
|
||||
req.query
|
||||
)
|
||||
|
||||
const priceListService: PriceListService =
|
||||
req.scope.resolve("priceListService")
|
||||
|
||||
let expandFields: string[] = []
|
||||
if (validated.expand) {
|
||||
expandFields = validated.expand.split(",")
|
||||
}
|
||||
|
||||
const listConfig: FindConfig<PriceList> = {
|
||||
relations: expandFields,
|
||||
skip: validated.offset,
|
||||
take: validated.limit,
|
||||
order: { created_at: "DESC" } as { [k: string]: "DESC" },
|
||||
}
|
||||
|
||||
if (typeof validated.order !== "undefined") {
|
||||
if (validated.order.startsWith("-")) {
|
||||
const [, field] = validated.order.split("-")
|
||||
listConfig.order = { [field]: "DESC" }
|
||||
} else {
|
||||
listConfig.order = { [validated.order]: "ASC" }
|
||||
}
|
||||
}
|
||||
|
||||
const filterableFields = omit(validated, [
|
||||
"limit",
|
||||
"offset",
|
||||
"expand",
|
||||
"order",
|
||||
])
|
||||
|
||||
const [price_lists, count] = await priceListService.listAndCount(
|
||||
filterableFields,
|
||||
listConfig
|
||||
)
|
||||
|
||||
res.json({
|
||||
price_lists,
|
||||
count,
|
||||
offset: validated.offset,
|
||||
limit: validated.limit,
|
||||
})
|
||||
}
|
||||
|
||||
export class AdminGetPriceListPaginationParams extends FilterablePriceListProps {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
offset?: number = 0
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
limit?: number = 10
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
expand?: string
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
order?: string
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { Type } from "class-transformer"
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from "."
|
||||
import { PriceList } from "../../../.."
|
||||
import PriceListService from "../../../../services/price-list"
|
||||
import {
|
||||
AdminPriceListPricesUpdateReq,
|
||||
PriceListStatus,
|
||||
PriceListType,
|
||||
} from "../../../../types/price-list"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [post] /price_lists/{id}
|
||||
* operationId: "PostPriceListsPriceListPriceList"
|
||||
* summary: "Update a Price List"
|
||||
* description: "Updates a Price List"
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (path) id=* {string} The id of the Price List.
|
||||
* requestBody:
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* name:
|
||||
* description: "The name of the Price List"
|
||||
* type: string
|
||||
* description:
|
||||
* description: "A description of the Price List."
|
||||
* type: string
|
||||
* type:
|
||||
* description: The type of the Price List.
|
||||
* type: string
|
||||
* enum:
|
||||
* - sale
|
||||
* - override
|
||||
* status:
|
||||
* description: The status of the Price List.
|
||||
* type: string
|
||||
* enum:
|
||||
* - active
|
||||
* - draft
|
||||
* prices:
|
||||
* description: The prices of the Price List.
|
||||
* type: array
|
||||
* items:
|
||||
* properties:
|
||||
* id:
|
||||
* description: The id of the price.
|
||||
* type: string
|
||||
* region_id:
|
||||
* description: The id of the Region for which the price is used.
|
||||
* type: string
|
||||
* currency_code:
|
||||
* description: The 3 character ISO currency code for which the price will be used.
|
||||
* type: string
|
||||
* amount:
|
||||
* description: The amount to charge for the Product Variant.
|
||||
* type: integer
|
||||
* min_quantity:
|
||||
* description: The minimum quantity for which the price will be used.
|
||||
* type: integer
|
||||
* max_quantity:
|
||||
* description: The maximum quantity for which the price will be used.
|
||||
* type: integer
|
||||
* customer_groups:
|
||||
* type: array
|
||||
* description: A list of customer groups that the Price List applies to.
|
||||
* items:
|
||||
* required:
|
||||
* - id
|
||||
* properties:
|
||||
* id:
|
||||
* description: The id of a customer group
|
||||
* type: string
|
||||
* tags:
|
||||
* - Price List
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* product:
|
||||
* $ref: "#/components/schemas/price_list"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const validated = await validator(
|
||||
AdminPostPriceListsPriceListPriceListReq,
|
||||
req.body
|
||||
)
|
||||
|
||||
const priceListService: PriceListService =
|
||||
req.scope.resolve("priceListService")
|
||||
|
||||
await priceListService.update(id, validated)
|
||||
|
||||
const priceList = await priceListService.retrieve(id, {
|
||||
select: defaultAdminPriceListFields as (keyof PriceList)[],
|
||||
relations: defaultAdminPriceListRelations,
|
||||
})
|
||||
|
||||
res.json({ price_list: priceList })
|
||||
}
|
||||
|
||||
class CustomerGroup {
|
||||
@IsString()
|
||||
id: string
|
||||
}
|
||||
|
||||
export class AdminPostPriceListsPriceListPriceListReq {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string
|
||||
|
||||
@IsOptional()
|
||||
starts_at?: Date
|
||||
|
||||
@IsOptional()
|
||||
ends_at?: Date
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(PriceListStatus)
|
||||
status?: PriceListStatus
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(PriceListType)
|
||||
type?: PriceListType
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@Type(() => AdminPriceListPricesUpdateReq)
|
||||
@ValidateNested({ each: true })
|
||||
prices: AdminPriceListPricesUpdateReq[]
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@Type(() => CustomerGroup)
|
||||
@ValidateNested({ each: true })
|
||||
customer_groups: CustomerGroup[]
|
||||
}
|
||||
@@ -3,12 +3,10 @@ import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Validate,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { EntityManager } from "typeorm"
|
||||
@@ -19,7 +17,7 @@ import {
|
||||
ShippingProfileService,
|
||||
} from "../../../../services"
|
||||
import { ProductStatus } from "../../../../types/product"
|
||||
import { XorConstraint } from "../../../../types/validators/xor"
|
||||
import { ProductVariantPricesCreateReq } from "../../../../types/product-variant"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
@@ -308,21 +306,6 @@ class ProductOptionReq {
|
||||
title: string
|
||||
}
|
||||
|
||||
class ProductVariantPricesReq {
|
||||
@Validate(XorConstraint, ["currency_code"])
|
||||
region_id?: string
|
||||
|
||||
@Validate(XorConstraint, ["region_id"])
|
||||
currency_code?: string
|
||||
|
||||
@IsInt()
|
||||
amount: number
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sale_amount?: number
|
||||
}
|
||||
|
||||
class ProductVariantReq {
|
||||
@IsString()
|
||||
title: string
|
||||
@@ -393,8 +376,8 @@ class ProductVariantReq {
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ProductVariantPricesReq)
|
||||
prices: ProductVariantPricesReq[]
|
||||
@Type(() => ProductVariantPricesCreateReq)
|
||||
prices: ProductVariantPricesCreateReq[]
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => ProductVariantOptionReq)
|
||||
|
||||
@@ -2,17 +2,15 @@ import { Type } from "class-transformer"
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Validate,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
|
||||
import { ProductService, ProductVariantService } from "../../../../services"
|
||||
import { XorConstraint } from "../../../../types/validators/xor"
|
||||
import { ProductVariantPricesCreateReq } from "../../../../types/product-variant"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
@@ -96,8 +94,11 @@ import { validator } from "../../../../utils/validator"
|
||||
* amount:
|
||||
* description: The amount to charge for the Product Variant.
|
||||
* type: integer
|
||||
* sale_amount:
|
||||
* description: The sale amount to charge for the Product Variant.
|
||||
* min_quantity:
|
||||
* description: The minimum quantity for which the price will be used.
|
||||
* type: integer
|
||||
* max_quantity:
|
||||
* description: The maximum quantity for which the price will be used.
|
||||
* type: integer
|
||||
* options:
|
||||
* type: array
|
||||
@@ -152,21 +153,6 @@ class ProductVariantOptionReq {
|
||||
option_id: string
|
||||
}
|
||||
|
||||
class ProductVariantPricesReq {
|
||||
@Validate(XorConstraint, ["currency_code"])
|
||||
region_id?: string
|
||||
|
||||
@Validate(XorConstraint, ["region_id"])
|
||||
currency_code?: string
|
||||
|
||||
@IsInt()
|
||||
amount: number
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sale_amount?: number
|
||||
}
|
||||
|
||||
export class AdminPostProductsProductVariantsReq {
|
||||
@IsString()
|
||||
title: string
|
||||
@@ -237,8 +223,8 @@ export class AdminPostProductsProductVariantsReq {
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ProductVariantPricesReq)
|
||||
prices: ProductVariantPricesReq[]
|
||||
@Type(() => ProductVariantPricesCreateReq)
|
||||
prices: ProductVariantPricesCreateReq[]
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => ProductVariantOptionReq)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Router } from "express"
|
||||
import { Product, ProductTag, ProductType } from "../../../.."
|
||||
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
||||
import middlewares from "../../../middlewares"
|
||||
import "reflect-metadata"
|
||||
import { Product, ProductTag, ProductType } from "../../../.."
|
||||
import { PaginatedResponse } from "../../../../types/common"
|
||||
import middlewares from "../../../middlewares"
|
||||
|
||||
const route = Router()
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
IsOptional,
|
||||
IsString,
|
||||
NotEquals,
|
||||
Validate,
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
@@ -19,7 +18,7 @@ import {
|
||||
ProductStatus,
|
||||
} from "."
|
||||
import { ProductService } from "../../../../services"
|
||||
import { XorConstraint } from "../../../../types/validators/xor"
|
||||
import { ProductVariantPricesUpdateReq } from "../../../../types/product-variant"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
@@ -248,21 +247,6 @@ class ProductVariantOptionReq {
|
||||
option_id: string
|
||||
}
|
||||
|
||||
class ProductVariantPricesReq {
|
||||
@Validate(XorConstraint, ["currency_code"])
|
||||
region_id?: string
|
||||
|
||||
@Validate(XorConstraint, ["region_id"])
|
||||
currency_code?: string
|
||||
|
||||
@IsInt()
|
||||
amount: number
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sale_amount?: number
|
||||
}
|
||||
|
||||
class ProductVariantReq {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@@ -339,8 +323,8 @@ class ProductVariantReq {
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ProductVariantPricesReq)
|
||||
prices: ProductVariantPricesReq[]
|
||||
@Type(() => ProductVariantPricesUpdateReq)
|
||||
prices: ProductVariantPricesUpdateReq[]
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => ProductVariantOptionReq)
|
||||
|
||||
@@ -2,17 +2,15 @@ import { Type } from "class-transformer"
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Validate,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
|
||||
import { ProductService, ProductVariantService } from "../../../../services"
|
||||
import { XorConstraint } from "../../../../types/validators/xor"
|
||||
import { ProductVariantPricesUpdateReq } from "../../../../types/product-variant"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
@@ -84,6 +82,9 @@ import { validator } from "../../../../utils/validator"
|
||||
* type: array
|
||||
* items:
|
||||
* properties:
|
||||
* id:
|
||||
* description: The id of the price.
|
||||
* type: string
|
||||
* region_id:
|
||||
* description: The id of the Region for which the price is used.
|
||||
* type: string
|
||||
@@ -93,8 +94,11 @@ import { validator } from "../../../../utils/validator"
|
||||
* amount:
|
||||
* description: The amount to charge for the Product Variant.
|
||||
* type: integer
|
||||
* sale_amount:
|
||||
* description: The sale amount to charge for the Product Variant.
|
||||
* min_quantity:
|
||||
* description: The minimum quantity for which the price will be used.
|
||||
* type: integer
|
||||
* max_quantity:
|
||||
* description: The maximum quantity for which the price will be used.
|
||||
* type: integer
|
||||
* options:
|
||||
* type: array
|
||||
@@ -152,21 +156,6 @@ class ProductVariantOptionReq {
|
||||
option_id: string
|
||||
}
|
||||
|
||||
class ProductVariantPricesReq {
|
||||
@Validate(XorConstraint, ["currency_code"])
|
||||
region_id?: string
|
||||
|
||||
@Validate(XorConstraint, ["region_id"])
|
||||
currency_code?: string
|
||||
|
||||
@IsInt()
|
||||
amount: number
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sale_amount?: number
|
||||
}
|
||||
|
||||
export class AdminPostProductsProductVariantsVariantReq {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@@ -238,8 +227,8 @@ export class AdminPostProductsProductVariantsVariantReq {
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ProductVariantPricesReq)
|
||||
prices: ProductVariantPricesReq[]
|
||||
@Type(() => ProductVariantPricesUpdateReq)
|
||||
prices: ProductVariantPricesUpdateReq[]
|
||||
|
||||
@Type(() => ProductVariantOptionReq)
|
||||
@ValidateNested({ each: true })
|
||||
|
||||
@@ -42,6 +42,7 @@ export * from "./models/order"
|
||||
export * from "./models/payment"
|
||||
export * from "./models/payment-provider"
|
||||
export * from "./models/payment-session"
|
||||
export * from "./models/price-list"
|
||||
export * from "./models/product"
|
||||
export * from "./models/product-collection"
|
||||
export * from "./models/product-option"
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class updateMoneyAmountAddPriceList1646915480108 implements MigrationInterface {
|
||||
name = 'updateMoneyAmountAddPriceList1646915480108'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TYPE "price_list_type_enum" AS ENUM('sale', 'override')`);
|
||||
await queryRunner.query(`CREATE TYPE "price_list_status_enum" AS ENUM('active', 'draft')`);
|
||||
await queryRunner.query(`CREATE TABLE "price_list" ("id" character varying NOT NULL, "name" character varying NOT NULL, "description" character varying NOT NULL, "type" "price_list_type_enum" NOT NULL DEFAULT 'sale', "status" "price_list_status_enum" NOT NULL DEFAULT 'draft', "starts_at" TIMESTAMP WITH TIME ZONE, "ends_at" TIMESTAMP WITH TIME ZONE, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_52ea7826468b1c889cb2c28df03" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "price_list_customer_groups" ("price_list_id" character varying NOT NULL, "customer_group_id" character varying NOT NULL, CONSTRAINT "PK_1afcbe15cc8782dc80c05707df9" PRIMARY KEY ("price_list_id", "customer_group_id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_52875734e9dd69064f0041f4d9" ON "price_list_customer_groups" ("price_list_id") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_c5516f550433c9b1c2630d787a" ON "price_list_customer_groups" ("customer_group_id") `);
|
||||
await queryRunner.query(`ALTER TABLE "money_amount" DROP COLUMN "sale_amount"`);
|
||||
await queryRunner.query(`ALTER TABLE "money_amount" ADD "min_quantity" integer`);
|
||||
await queryRunner.query(`ALTER TABLE "money_amount" ADD "max_quantity" integer`);
|
||||
await queryRunner.query(`ALTER TABLE "money_amount" ADD "price_list_id" character varying`);
|
||||
await queryRunner.query(`ALTER TABLE "money_amount" ADD CONSTRAINT "FK_f249976b079375499662eb80c40" FOREIGN KEY ("price_list_id") REFERENCES "price_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "price_list_customer_groups" ADD CONSTRAINT "FK_52875734e9dd69064f0041f4d92" FOREIGN KEY ("price_list_id") REFERENCES "price_list"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||
await queryRunner.query(`ALTER TABLE "price_list_customer_groups" ADD CONSTRAINT "FK_c5516f550433c9b1c2630d787a7" FOREIGN KEY ("customer_group_id") REFERENCES "customer_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "price_list_customer_groups" DROP CONSTRAINT "FK_c5516f550433c9b1c2630d787a7"`);
|
||||
await queryRunner.query(`ALTER TABLE "price_list_customer_groups" DROP CONSTRAINT "FK_52875734e9dd69064f0041f4d92"`);
|
||||
await queryRunner.query(`ALTER TABLE "money_amount" DROP CONSTRAINT "FK_f249976b079375499662eb80c40"`);
|
||||
await queryRunner.query(`ALTER TABLE "money_amount" DROP COLUMN "price_list_id"`);
|
||||
await queryRunner.query(`ALTER TABLE "money_amount" DROP COLUMN "max_quantity"`);
|
||||
await queryRunner.query(`ALTER TABLE "money_amount" DROP COLUMN "min_quantity"`);
|
||||
await queryRunner.query(`ALTER TABLE "money_amount" ADD "sale_amount" integer`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_c5516f550433c9b1c2630d787a"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_52875734e9dd69064f0041f4d9"`);
|
||||
await queryRunner.query(`DROP TABLE "price_list_customer_groups"`);
|
||||
await queryRunner.query(`DROP TABLE "price_list"`);
|
||||
await queryRunner.query(`DROP TYPE "price_list_status_enum"`);
|
||||
await queryRunner.query(`DROP TYPE "price_list_type_enum"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,14 +5,14 @@ import {
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
UpdateDateColumn
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
import { Customer } from ".."
|
||||
import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column"
|
||||
import { Customer } from "./customer"
|
||||
import { PriceList } from "./price-list"
|
||||
|
||||
@Entity()
|
||||
export class CustomerGroup {
|
||||
@@ -23,22 +23,22 @@ export class CustomerGroup {
|
||||
@Column()
|
||||
name: string
|
||||
|
||||
@ManyToMany(() => Customer, (customer) => customer.groups, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
@JoinTable({
|
||||
name: "customer_group_customers",
|
||||
joinColumn: {
|
||||
name: "customer_group_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "customer_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
})
|
||||
@ManyToMany(
|
||||
() => Customer,
|
||||
(customer) => customer.groups,
|
||||
{
|
||||
onDelete: "CASCADE",
|
||||
}
|
||||
)
|
||||
customers: Customer[]
|
||||
|
||||
@ManyToMany(
|
||||
() => PriceList,
|
||||
(priceList) => priceList.customer_groups,
|
||||
{ onDelete: "CASCADE" }
|
||||
)
|
||||
price_lists: PriceList[]
|
||||
|
||||
@CreateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
created_at: Date
|
||||
|
||||
|
||||
@@ -8,11 +8,12 @@ import {
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
UpdateDateColumn
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
import { resolveDbType } from "../utils/db-aware-column"
|
||||
import { Currency } from "./currency"
|
||||
import { PriceList } from "./price-list"
|
||||
import { ProductVariant } from "./product-variant"
|
||||
import { Region } from "./region"
|
||||
|
||||
@@ -31,16 +32,34 @@ export class MoneyAmount {
|
||||
@Column({ type: "int" })
|
||||
amount: number
|
||||
|
||||
@Column({ type: "int", nullable: true, default: null })
|
||||
sale_amount?: number
|
||||
@Column({ type: "int", nullable: true })
|
||||
min_quantity: number | null
|
||||
|
||||
@Column({ type: "int", nullable: true })
|
||||
max_quantity: number | null
|
||||
|
||||
@Column({ nullable: true })
|
||||
price_list_id: string | null
|
||||
|
||||
@ManyToOne(
|
||||
() => PriceList,
|
||||
(priceList) => priceList.prices,
|
||||
{ cascade: true, onDelete: "CASCADE" }
|
||||
)
|
||||
@JoinColumn({ name: "price_list_id" })
|
||||
price_list: PriceList | null
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
variant_id: string
|
||||
|
||||
@ManyToOne(() => ProductVariant, (variant) => variant.prices, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => ProductVariant,
|
||||
(variant) => variant.prices,
|
||||
{
|
||||
onDelete: "CASCADE",
|
||||
}
|
||||
)
|
||||
@JoinColumn({ name: "variant_id" })
|
||||
variant: ProductVariant
|
||||
|
||||
@@ -86,8 +105,11 @@ export class MoneyAmount {
|
||||
* amount:
|
||||
* description: "The amount in the smallest currecny unit (e.g. cents 100 cents to charge $1) that the Product Variant will cost."
|
||||
* type: integer
|
||||
* sale_amount:
|
||||
* description: "An optional sale amount that the Product Variant will be available for when defined."
|
||||
* min_quantity:
|
||||
* description: "The minimum quantity that the Money Amount applies to. If this value is not set, the Money Amount applies to all quantities."
|
||||
* type: integer
|
||||
* max_quantity:
|
||||
* description: "The maximum quantity that the Money Amount applies to. If this value is not set, the Money Amount applies to all quantities."
|
||||
* type: integer
|
||||
* variant_id:
|
||||
* description: "The id of the Product Variant that the Money Amount belongs to."
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
OneToMany,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
import { PriceListStatus, PriceListType } from "../types/price-list"
|
||||
import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column"
|
||||
import { CustomerGroup } from "./customer-group"
|
||||
import { MoneyAmount } from "./money-amount"
|
||||
|
||||
@Entity()
|
||||
export class PriceList {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
|
||||
@Column()
|
||||
description: string
|
||||
|
||||
@DbAwareColumn({ type: "enum", enum: PriceListType, default: "sale" })
|
||||
type: PriceListType
|
||||
|
||||
@DbAwareColumn({ type: "enum", enum: PriceListStatus, default: "draft" })
|
||||
status: PriceListStatus
|
||||
|
||||
@Column({
|
||||
type: resolveDbType("timestamptz"),
|
||||
nullable: true,
|
||||
})
|
||||
starts_at: Date
|
||||
|
||||
@Column({ type: resolveDbType("timestamptz"), nullable: true })
|
||||
ends_at: Date
|
||||
|
||||
@JoinTable({
|
||||
name: "price_list_customer_groups",
|
||||
joinColumn: {
|
||||
name: "price_list_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "customer_group_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
})
|
||||
@ManyToMany(
|
||||
() => CustomerGroup,
|
||||
(cg) => cg.price_lists,
|
||||
{ onDelete: "CASCADE" }
|
||||
)
|
||||
customer_groups: CustomerGroup[]
|
||||
|
||||
@OneToMany(
|
||||
() => MoneyAmount,
|
||||
(moneyAmount) => moneyAmount.price_list,
|
||||
{ onDelete: "CASCADE" }
|
||||
)
|
||||
prices: MoneyAmount[]
|
||||
|
||||
@CreateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
updated_at: Date
|
||||
|
||||
@DeleteDateColumn({ type: resolveDbType("timestamptz") })
|
||||
deleted_at: Date
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert(): undefined | void {
|
||||
if (this.id) {
|
||||
return
|
||||
}
|
||||
const id = ulid()
|
||||
this.id = `pl_${id}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema price_list
|
||||
* title: "Price List"
|
||||
* description: "Price Lists represents a set of prices that overrides the default price for one or more product variants."
|
||||
* x-resourceId: price_list
|
||||
* properties:
|
||||
* id:
|
||||
* description: "The id of the Price List. This value will be prefixed by `pl_`."
|
||||
* type: string
|
||||
* type:
|
||||
* description: "The type of Price List. This can be one of either `sale` or `override`."
|
||||
* type: string
|
||||
* enum:
|
||||
* - sale
|
||||
* - override
|
||||
* starts_at:
|
||||
* description: "The date with timezone that the Price List starts being valid."
|
||||
* type: date-time
|
||||
* ends_at:
|
||||
* description: "The date with timezone that the Price List stops being valid."
|
||||
* type: date-time
|
||||
* customer_groups:
|
||||
* description: "The Customer Groups that the Price List applies to."
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/customer_group"
|
||||
* created_at:
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* updated_at:
|
||||
* description: "The date with timezone at which the resource was last updated."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* deleted_at:
|
||||
* description: "The date with timezone at which the resource was deleted."
|
||||
* type: string
|
||||
* format: date-time
|
||||
*/
|
||||
@@ -0,0 +1,31 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
|
||||
export const moneyAmounts = {
|
||||
amountOne: {
|
||||
id: IdMap.getId("amountOne"),
|
||||
currency_code: "USD",
|
||||
amount: 1,
|
||||
min_quantity: 1,
|
||||
max_quantity: 10,
|
||||
price_list_id: null,
|
||||
}
|
||||
}
|
||||
|
||||
export const MoneyAmountModelMock = {
|
||||
create: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
findOne: jest.fn().mockImplementation(query => {
|
||||
if (query._id === IdMap.getId("amountOne")) {
|
||||
return Promise.resolve(moneyAmounts.amountOne)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
addToPriceList: jest.fn().mockImplementation((priceListId, prices, overrideExisting) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
deletePriceListPrices: jest.fn().mockImplementation((priceListId, moneyAmountIds) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
updatePriceListPrices: jest.fn().mockImplementation((priceListId, updates) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export const PriceListModelMock = {
|
||||
create: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
updateOne: jest.fn().mockImplementation((query, update) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
deleteOne: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
findOne: jest.fn().mockImplementation(query => {
|
||||
if (query.email === "oliver@medusa.com") {
|
||||
return Promise.resolve(customers.testCustomer)
|
||||
}
|
||||
if (query.phone === "12345678") {
|
||||
return Promise.resolve(customers.customerWithPhone)
|
||||
}
|
||||
if (query._id === IdMap.getId("testCustomer")) {
|
||||
return Promise.resolve(customers.testCustomer)
|
||||
}
|
||||
if (query._id === IdMap.getId("deleteId")) {
|
||||
return Promise.resolve(customers.deleteCustomer)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
import partition from "lodash/partition"
|
||||
import {
|
||||
Brackets,
|
||||
EntityRepository,
|
||||
In,
|
||||
IsNull,
|
||||
Not,
|
||||
Repository,
|
||||
Repository
|
||||
} from "typeorm"
|
||||
import { MoneyAmount } from "../models/money-amount"
|
||||
import { PriceListPriceCreateInput, PriceListPriceUpdateInput } from "../types/price-list"
|
||||
|
||||
type Price = Partial<
|
||||
Pick<
|
||||
MoneyAmount,
|
||||
"currency_code" | "region_id" | "sale_amount" | "currency_code"
|
||||
>
|
||||
> & { amount: number }
|
||||
Omit<MoneyAmount, "created_at" | "updated_at" | "deleted_at">
|
||||
> & {
|
||||
amount: number
|
||||
}
|
||||
|
||||
@EntityRepository(MoneyAmount)
|
||||
export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
||||
@@ -21,6 +22,7 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
||||
const pricesNotInPricesPayload = await this.createQueryBuilder()
|
||||
.where({
|
||||
variant_id: variantId,
|
||||
price_list_id: IsNull(),
|
||||
})
|
||||
.andWhere(
|
||||
new Brackets((qb) => {
|
||||
@@ -33,12 +35,13 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
||||
return pricesNotInPricesPayload
|
||||
}
|
||||
|
||||
public async upsertCurrencyPrice(variantId: string, price: Price) {
|
||||
public async upsertVariantCurrencyPrice(variantId: string, price: Price) {
|
||||
let moneyAmount = await this.findOne({
|
||||
where: {
|
||||
currency_code: price.currency_code,
|
||||
variant_id: variantId,
|
||||
region_id: IsNull(),
|
||||
price_list_id: IsNull(),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -50,9 +53,64 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
||||
})
|
||||
} else {
|
||||
moneyAmount.amount = price.amount
|
||||
moneyAmount.sale_amount = price.sale_amount
|
||||
}
|
||||
|
||||
return await this.save(moneyAmount)
|
||||
}
|
||||
|
||||
public async addPriceListPrices(
|
||||
priceListId: string,
|
||||
prices: PriceListPriceCreateInput[],
|
||||
overrideExisting: boolean = false
|
||||
): Promise<MoneyAmount[]> {
|
||||
const toInsert = prices.map((price) => (this.create({
|
||||
...price,
|
||||
price_list_id: priceListId,
|
||||
})))
|
||||
const insertResult = await this.createQueryBuilder()
|
||||
.insert()
|
||||
.orIgnore(true)
|
||||
.into(MoneyAmount)
|
||||
.values(toInsert)
|
||||
.execute()
|
||||
|
||||
if (overrideExisting) {
|
||||
await this.createQueryBuilder()
|
||||
.delete()
|
||||
.from(MoneyAmount)
|
||||
.where({
|
||||
price_list_id: priceListId,
|
||||
id: Not(In(insertResult.identifiers.map((ma) => ma.id))),
|
||||
})
|
||||
.execute()
|
||||
}
|
||||
|
||||
return await this.manager
|
||||
.createQueryBuilder(MoneyAmount, "ma")
|
||||
.select()
|
||||
.where(insertResult.identifiers)
|
||||
.getMany()
|
||||
}
|
||||
|
||||
public async deletePriceListPrices(
|
||||
priceListId: string,
|
||||
moneyAmountIds: string[]
|
||||
): Promise<void> {
|
||||
await this.createQueryBuilder()
|
||||
.delete()
|
||||
.from(MoneyAmount)
|
||||
.where({ price_list_id: priceListId, id: In(moneyAmountIds) })
|
||||
.execute()
|
||||
}
|
||||
|
||||
public async updatePriceListPrices(
|
||||
priceListId: string,
|
||||
updates: PriceListPriceUpdateInput[]
|
||||
): Promise<MoneyAmount[]> {
|
||||
const [existingPrices, newPrices] = partition(updates, (update) => update.id)
|
||||
|
||||
const newPriceEntities = newPrices.map((price) => (this.create({ ...price, price_list_id: priceListId })))
|
||||
|
||||
return await this.save([...existingPrices, ...newPriceEntities])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { PriceList } from "../models/price-list"
|
||||
|
||||
@EntityRepository(PriceList)
|
||||
export class PriceListRepository extends Repository<PriceList> {}
|
||||
@@ -0,0 +1,43 @@
|
||||
export const PriceListServiceMock = {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
|
||||
create: jest.fn().mockImplementation((f) => {
|
||||
return Promise.resolve(f)
|
||||
}),
|
||||
|
||||
retrieve: jest.fn().mockImplementation((f) => {
|
||||
return Promise.resolve(f)
|
||||
}),
|
||||
|
||||
update: jest.fn().mockImplementation((id, update) => {
|
||||
return Promise.resolve({ id, ...update })
|
||||
}),
|
||||
|
||||
delete: jest.fn().mockImplementation((id) => {
|
||||
return Promise.resolve(id)
|
||||
}),
|
||||
|
||||
addPrices: jest.fn().mockImplementation((id, prices) => {
|
||||
return Promise.resolve({ id, prices })
|
||||
}),
|
||||
|
||||
deletePrices: jest.fn().mockImplementation((id, prices) => {
|
||||
return Promise.resolve({ id, prices })
|
||||
}),
|
||||
|
||||
listAndCount: jest.fn().mockImplementation((fields, config) => {
|
||||
return Promise.resolve([[{ id: "pl_1" }, { id: "pl_2" }], 2])
|
||||
}),
|
||||
|
||||
upsertCustomerGroups_: jest.fn().mockImplementation((id, groups) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
return PriceListServiceMock
|
||||
})
|
||||
|
||||
export default mock
|
||||
@@ -98,7 +98,9 @@ const variantWithPrices = {
|
||||
id: "price_1",
|
||||
currency_code: "usd",
|
||||
amount: 100,
|
||||
sale_amount: null,
|
||||
price_list_id: null,
|
||||
min_quantity: 1,
|
||||
max_quantity: 10,
|
||||
variant_id: "variant_with_prices",
|
||||
region_id: null,
|
||||
created_at: "2021-03-16T21:24:13.657Z",
|
||||
@@ -109,7 +111,9 @@ const variantWithPrices = {
|
||||
id: "price_2",
|
||||
currency_code: "dk",
|
||||
amount: 100,
|
||||
sale_amount: null,
|
||||
price_list_id: null,
|
||||
min_quantity: 1,
|
||||
max_quantity: 10,
|
||||
variant_id: "variant_with_prices",
|
||||
region_id: null,
|
||||
created_at: "2021-03-16T21:24:13.657Z",
|
||||
@@ -285,8 +289,12 @@ export const ProductVariantServiceMock = {
|
||||
}),
|
||||
delete: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
update: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
setCurrencyPrice: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
setRegionPrice: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
updateVariantPrices: jest.fn().mockImplementation((variantId, prices) => {
|
||||
return Promise.resolve({})
|
||||
}),
|
||||
deleteVariantPrices: jest.fn().mockImplementation((variantId, priceIds) => {
|
||||
return Promise.resolve({})
|
||||
}),
|
||||
updateOptionValue: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
addOptionValue: jest.fn().mockImplementation((variantId, optionId, value) => {
|
||||
return Promise.resolve({})
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import PriceListService from "../price-list"
|
||||
|
||||
describe("PriceListService", () => {
|
||||
const priceListRepository = MockRepository({
|
||||
findOne: (q) => {
|
||||
if (q === IdMap.getId("batman")) {
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
return Promise.resolve({ id: IdMap.getId("ironman") })
|
||||
},
|
||||
create: (data) => {
|
||||
return Promise.resolve({ id: IdMap.getId("ironman"), ...data })
|
||||
},
|
||||
save: (data) => Promise.resolve(data),
|
||||
})
|
||||
|
||||
const customerGroupService = {
|
||||
retrieve: jest.fn((id) => {
|
||||
if (id === IdMap.getId("group")) {
|
||||
return Promise.resolve({ id: IdMap.getId("group") })
|
||||
}
|
||||
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`CustomerGroup with id ${id} was not found`
|
||||
)
|
||||
}),
|
||||
}
|
||||
|
||||
const moneyAmountRepository = MockRepository()
|
||||
|
||||
moneyAmountRepository.addPriceListPrices = jest.fn(() => Promise.resolve())
|
||||
moneyAmountRepository.removePriceListPrices = jest.fn(() => Promise.resolve())
|
||||
moneyAmountRepository.updatePriceListPrices = jest.fn(() => Promise.resolve())
|
||||
|
||||
const defaultRelations = ["prices", "customer_groups"]
|
||||
|
||||
const priceListService = new PriceListService({
|
||||
manager: MockManager,
|
||||
customerGroupService,
|
||||
priceListRepository,
|
||||
moneyAmountRepository,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("retrieve", () => {
|
||||
it("successfully retrieves a price list", async () => {
|
||||
const result = await priceListService.retrieve(IdMap.getId("ironman"))
|
||||
|
||||
expect(priceListRepository.findOne).toHaveBeenCalledTimes(1)
|
||||
expect(priceListRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { id: IdMap.getId("ironman") },
|
||||
})
|
||||
|
||||
expect(result.id).toEqual(IdMap.getId("ironman"))
|
||||
})
|
||||
|
||||
it("fails on non-existing product variant id", async () => {
|
||||
try {
|
||||
await priceListService.retrieve(IdMap.getId("batman"))
|
||||
} catch (error) {
|
||||
expect(error.message).toBe(
|
||||
`Price list with id: ${IdMap.getId("batman")} was not found`
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
it("creates a new Price List", async () => {
|
||||
const result = await priceListService.create({
|
||||
name: "VIP winter sale",
|
||||
description: "Winter sale for VIP customers. 25% off selected items.",
|
||||
type: "sale",
|
||||
status: "active",
|
||||
starts_at: "2022-07-01T00:00:00.000Z",
|
||||
ends_at: "2022-07-31T00:00:00.000Z",
|
||||
customer_groups: [{ id: IdMap.getId("group") }],
|
||||
prices: [
|
||||
{
|
||||
amount: 100,
|
||||
currency_code: "usd",
|
||||
min_quantity: 1,
|
||||
max_quantity: 100,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(priceListRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(priceListRepository.create).toHaveBeenCalledWith({
|
||||
name: "VIP winter sale",
|
||||
description: "Winter sale for VIP customers. 25% off selected items.",
|
||||
type: "sale",
|
||||
status: "active",
|
||||
starts_at: "2022-07-01T00:00:00.000Z",
|
||||
ends_at: "2022-07-31T00:00:00.000Z",
|
||||
})
|
||||
expect(customerGroupService.retrieve).toHaveBeenCalledTimes(1)
|
||||
expect(customerGroupService.retrieve).toHaveBeenCalledWith(
|
||||
IdMap.getId("group")
|
||||
)
|
||||
expect(moneyAmountRepository.addPriceListPrices).toHaveBeenCalledTimes(1)
|
||||
expect(moneyAmountRepository.addPriceListPrices).toHaveBeenCalledWith(
|
||||
IdMap.getId("ironman"),
|
||||
[
|
||||
{
|
||||
amount: 100,
|
||||
currency_code: "usd",
|
||||
min_quantity: 1,
|
||||
max_quantity: 100,
|
||||
},
|
||||
]
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
||||
import idMap from "medusa-test-utils/dist/id-map"
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import ProductVariantService from "../product-variant"
|
||||
|
||||
const eventBusService = {
|
||||
@@ -376,7 +375,6 @@ describe("ProductVariantService", () => {
|
||||
{
|
||||
currency_code: "dkk",
|
||||
amount: 1000,
|
||||
sale_amount: 750,
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -388,7 +386,6 @@ describe("ProductVariantService", () => {
|
||||
{
|
||||
currency_code: "dkk",
|
||||
amount: 1000,
|
||||
sale_amount: 750,
|
||||
},
|
||||
]
|
||||
)
|
||||
@@ -435,6 +432,10 @@ describe("ProductVariantService", () => {
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve(oldPrices))
|
||||
|
||||
moneyAmountRepository.deleteVariantPrices = jest
|
||||
.fn()
|
||||
.mockImplementation((variant_id, price_ids) => Promise.resolve({}))
|
||||
|
||||
const productVariantRepository = MockRepository({
|
||||
findOne: (query) => Promise.resolve({ id: IdMap.getId("ironman") }),
|
||||
})
|
||||
@@ -456,114 +457,9 @@ describe("ProductVariantService", () => {
|
||||
.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()
|
||||
|
||||
moneyAmountRepository.upsertCurrencyPrice = jest
|
||||
.fn()
|
||||
.mockImplementation((variantId, price) => {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("test-amount"),
|
||||
variant_id: IdMap.getId(variantId),
|
||||
...price,
|
||||
})
|
||||
})
|
||||
|
||||
const productVariantService = new ProductVariantService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
moneyAmountRepository,
|
||||
productVariantRepository,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls upsert price with given currency", async () => {
|
||||
await productVariantService.setCurrencyPrice(IdMap.getId("ironman"), {
|
||||
currency_code: "usd",
|
||||
amount: 100,
|
||||
})
|
||||
|
||||
expect(moneyAmountRepository.upsertCurrencyPrice).toHaveBeenCalledTimes(1)
|
||||
expect(moneyAmountRepository.upsertCurrencyPrice).toHaveBeenCalledWith(
|
||||
IdMap.getId("ironman"),
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 100,
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getRegionPrice", () => {
|
||||
@@ -590,7 +486,6 @@ describe("ProductVariantService", () => {
|
||||
currency_code: "dkk",
|
||||
region_id: IdMap.getId("california"),
|
||||
amount: 1000,
|
||||
sale_amount: 750,
|
||||
})
|
||||
}
|
||||
return Promise.resolve({
|
||||
@@ -629,7 +524,8 @@ describe("ProductVariantService", () => {
|
||||
IdMap.getId("california")
|
||||
)
|
||||
|
||||
expect(result).toBe(750)
|
||||
// TODO: Update once PriceStrategy is implemented
|
||||
expect(result).toBe(1000)
|
||||
})
|
||||
|
||||
it("fails if no price is found", async () => {
|
||||
@@ -646,7 +542,7 @@ describe("ProductVariantService", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("setRegionPrice", () => {
|
||||
describe("updateVariantPrices", () => {
|
||||
const moneyAmountRepository = MockRepository({
|
||||
findOne: (query) => {
|
||||
if (query.where.region_id === IdMap.getId("cali")) {
|
||||
@@ -657,14 +553,47 @@ describe("ProductVariantService", () => {
|
||||
variant_id: IdMap.getId("ironman"),
|
||||
currency_code: "dkk",
|
||||
amount: 750,
|
||||
sale_amount: 500,
|
||||
})
|
||||
},
|
||||
create: (p) => p,
|
||||
remove: () => Promise.resolve(),
|
||||
})
|
||||
|
||||
const oldPrices = [
|
||||
{
|
||||
currency_code: "dkk",
|
||||
amount: 1000,
|
||||
variant_id: "ironman",
|
||||
region_id: null,
|
||||
},
|
||||
]
|
||||
|
||||
moneyAmountRepository.findVariantPricesNotIn = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve(oldPrices))
|
||||
|
||||
moneyAmountRepository.upsertVariantCurrencyPrice = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve())
|
||||
|
||||
const regionService = {
|
||||
list: jest.fn().mockImplementation((config) => {
|
||||
const idOrIds = config.id
|
||||
|
||||
if (Array.isArray(idOrIds)) {
|
||||
return Promise.resolve(
|
||||
idOrIds.map((id) => ({ id, currency_code: "usd" }))
|
||||
)
|
||||
} else {
|
||||
return Promise.resolve([{ id: idOrIds, currency_code: "usd" }])
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
const productVariantService = new ProductVariantService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
regionService,
|
||||
moneyAmountRepository,
|
||||
})
|
||||
|
||||
@@ -672,17 +601,43 @@ describe("ProductVariantService", () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully creates a price if none exist with given region id", async () => {
|
||||
await productVariantService.setRegionPrice(IdMap.getId("ironman"), {
|
||||
it("successfully removes obsolete prices and calls save on new/existing prices", async () => {
|
||||
await productVariantService.updateVariantPrices("ironman", [
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 4000,
|
||||
},
|
||||
])
|
||||
|
||||
expect(
|
||||
moneyAmountRepository.findVariantPricesNotIn
|
||||
).toHaveBeenCalledTimes(1)
|
||||
|
||||
expect(
|
||||
moneyAmountRepository.upsertVariantCurrencyPrice
|
||||
).toHaveBeenCalledTimes(1)
|
||||
expect(
|
||||
moneyAmountRepository.upsertVariantCurrencyPrice
|
||||
).toHaveBeenCalledWith("ironman", {
|
||||
currency_code: "usd",
|
||||
amount: 100,
|
||||
region_id: IdMap.getId("cali"),
|
||||
amount: 4000,
|
||||
})
|
||||
|
||||
expect(moneyAmountRepository.remove).toHaveBeenCalledTimes(1)
|
||||
expect(moneyAmountRepository.remove).toHaveBeenCalledWith(oldPrices)
|
||||
})
|
||||
|
||||
it("successfully creates new a region price", async () => {
|
||||
await productVariantService.updateVariantPrices(IdMap.getId("ironman"), [
|
||||
{
|
||||
amount: 100,
|
||||
region_id: IdMap.getId("cali"),
|
||||
},
|
||||
])
|
||||
|
||||
expect(moneyAmountRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(moneyAmountRepository.create).toHaveBeenCalledWith({
|
||||
variant_id: IdMap.getId("ironman"),
|
||||
currency_code: "usd",
|
||||
region_id: IdMap.getId("cali"),
|
||||
amount: 100,
|
||||
})
|
||||
@@ -690,22 +645,26 @@ describe("ProductVariantService", () => {
|
||||
expect(moneyAmountRepository.save).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("successfully updates a price", async () => {
|
||||
await productVariantService.setRegionPrice(IdMap.getId("ironman"), {
|
||||
currency_code: "dkk",
|
||||
amount: 750,
|
||||
sale_amount: 500,
|
||||
})
|
||||
it("successfully creates a currency price", async () => {
|
||||
await productVariantService.updateVariantPrices(IdMap.getId("ironman"), [
|
||||
{
|
||||
id: IdMap.getId("dkk"),
|
||||
currency_code: "dkk",
|
||||
amount: 750,
|
||||
},
|
||||
])
|
||||
|
||||
expect(moneyAmountRepository.create).toHaveBeenCalledTimes(0)
|
||||
|
||||
expect(moneyAmountRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(moneyAmountRepository.save).toHaveBeenCalledWith({
|
||||
variant_id: IdMap.getId("ironman"),
|
||||
expect(
|
||||
moneyAmountRepository.upsertVariantCurrencyPrice
|
||||
).toHaveBeenCalledTimes(1)
|
||||
expect(
|
||||
moneyAmountRepository.upsertVariantCurrencyPrice
|
||||
).toHaveBeenCalledWith(IdMap.getId("ironman"), {
|
||||
id: IdMap.getId("dkk"),
|
||||
currency_code: "dkk",
|
||||
amount: 750,
|
||||
sale_amount: 500,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { Validator, MedusaError } from "medusa-core-utils"
|
||||
import { parse, toSeconds } from "iso8601-duration"
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { Brackets, ILike } from "typeorm"
|
||||
import { formatException } from "../utils/exception-formatter"
|
||||
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { CustomerGroupService } from "."
|
||||
import { CustomerGroup } from "../models/customer-group"
|
||||
import { PriceList } from "../models/price-list"
|
||||
import { MoneyAmountRepository } from "../repositories/money-amount"
|
||||
import { PriceListRepository } from "../repositories/price-list"
|
||||
import { FindConfig } from "../types/common"
|
||||
import {
|
||||
CreatePriceListInput,
|
||||
FilterablePriceListProps,
|
||||
PriceListPriceCreateInput,
|
||||
UpdatePriceListInput,
|
||||
} from "../types/price-list"
|
||||
import { formatException } from "../utils/exception-formatter"
|
||||
|
||||
type PriceListConstructorProps = {
|
||||
manager: EntityManager
|
||||
customerGroupService: CustomerGroupService
|
||||
priceListRepository: typeof PriceListRepository
|
||||
moneyAmountRepository: typeof MoneyAmountRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides layer to manipulate product tags.
|
||||
* @extends BaseService
|
||||
*/
|
||||
class PriceListService extends BaseService {
|
||||
private manager_: EntityManager
|
||||
private customerGroupService_: CustomerGroupService
|
||||
private priceListRepo_: typeof PriceListRepository
|
||||
private moneyAmountRepo_: typeof MoneyAmountRepository
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
customerGroupService,
|
||||
priceListRepository,
|
||||
moneyAmountRepository,
|
||||
}: PriceListConstructorProps) {
|
||||
super()
|
||||
this.manager_ = manager
|
||||
this.customerGroupService_ = customerGroupService
|
||||
this.priceListRepo_ = priceListRepository
|
||||
this.moneyAmountRepo_ = moneyAmountRepository
|
||||
}
|
||||
|
||||
withTransaction(transactionManager: EntityManager): PriceListService {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new PriceListService({
|
||||
manager: transactionManager,
|
||||
customerGroupService: this.customerGroupService_,
|
||||
priceListRepository: this.priceListRepo_,
|
||||
moneyAmountRepository: this.moneyAmountRepo_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a product tag by id.
|
||||
* @param {string} priceListId - the id of the product tag to retrieve
|
||||
* @param {Object} config - the config to retrieve the tag by
|
||||
* @return {Promise<PriceList>} the collection.
|
||||
*/
|
||||
async retrieve(
|
||||
priceListId: string,
|
||||
config: FindConfig<PriceList> = {}
|
||||
): Promise<PriceList> {
|
||||
const priceListRepo = this.manager_.getCustomRepository(this.priceListRepo_)
|
||||
|
||||
const query = this.buildQuery_({ id: priceListId }, config)
|
||||
const priceList = await priceListRepo.findOne(query)
|
||||
|
||||
if (!priceList) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Price list with id: ${priceListId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return priceList
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Price List
|
||||
* @param {CreatePriceListInput} priceListObject - the Price List to create
|
||||
* @return {Promise<PriceList>} created Price List
|
||||
*/
|
||||
async create(priceListObject: CreatePriceListInput): Promise<PriceList> {
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const priceListRepo = manager.getCustomRepository(this.priceListRepo_)
|
||||
const moneyAmountRepo = manager.getCustomRepository(this.moneyAmountRepo_)
|
||||
|
||||
const { prices, customer_groups, ...rest } = priceListObject
|
||||
|
||||
try {
|
||||
const entity = priceListRepo.create(rest)
|
||||
|
||||
const priceList = await priceListRepo.save(entity)
|
||||
|
||||
if (prices) {
|
||||
await moneyAmountRepo.addPriceListPrices(priceList.id, prices)
|
||||
}
|
||||
|
||||
if (customer_groups) {
|
||||
await this.upsertCustomerGroups_(priceList.id, customer_groups)
|
||||
}
|
||||
|
||||
const result = await this.retrieve(priceList.id, {
|
||||
relations: ["prices", "customer_groups"],
|
||||
})
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
throw formatException(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a Price List
|
||||
* @param {string} id - the id of the Product List to update
|
||||
* @param {UpdatePriceListInput} update - the update to apply
|
||||
* @returns {Promise<PriceList>} updated Price List
|
||||
*/
|
||||
async update(id: string, update: UpdatePriceListInput): Promise<PriceList> {
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const priceListRepo = manager.getCustomRepository(this.priceListRepo_)
|
||||
const moneyAmountRepo = manager.getCustomRepository(this.moneyAmountRepo_)
|
||||
|
||||
const priceList = await this.retrieve(id, { select: ["id"] })
|
||||
|
||||
const { prices, customer_groups, ...rest } = update
|
||||
|
||||
for (const [key, value] of Object.entries(rest)) {
|
||||
priceList[key] = value
|
||||
}
|
||||
|
||||
await priceListRepo.save(priceList)
|
||||
|
||||
if (prices) {
|
||||
await moneyAmountRepo.updatePriceListPrices(id, prices)
|
||||
}
|
||||
|
||||
if (customer_groups) {
|
||||
await this.upsertCustomerGroups_(id, customer_groups)
|
||||
}
|
||||
|
||||
const result = await this.retrieve(id, {
|
||||
relations: ["prices", "customer_groups"],
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds prices to a price list in bulk, optionally replacing all existing prices
|
||||
* @param id - id of the price list
|
||||
* @param prices - prices to add
|
||||
* @param replace - whether to replace existing prices
|
||||
* @returns {Promise<PriceList>} updated Price List
|
||||
*/
|
||||
async addPrices(
|
||||
id: string,
|
||||
prices: PriceListPriceCreateInput[],
|
||||
replace = false
|
||||
): Promise<PriceList> {
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const moneyAmountRepo = manager.getCustomRepository(this.moneyAmountRepo_)
|
||||
|
||||
const priceList = await this.retrieve(id, { select: ["id"] })
|
||||
|
||||
await moneyAmountRepo.addPriceListPrices(priceList.id, prices, replace)
|
||||
|
||||
const result = await this.retrieve(priceList.id, {
|
||||
relations: ["prices"],
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes prices from a price list and deletes the removed prices in bulk
|
||||
* @param id - id of the price list
|
||||
* @param priceIds - ids of the prices to delete
|
||||
* @returns {Promise<void>} updated Price List
|
||||
*/
|
||||
async deletePrices(id: string, priceIds: string[]): Promise<void> {
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const moneyAmountRepo = manager.getCustomRepository(this.moneyAmountRepo_)
|
||||
|
||||
const priceList = await this.retrieve(id, { select: ["id"] })
|
||||
|
||||
await moneyAmountRepo.deletePriceListPrices(priceList.id, priceIds)
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a Price List
|
||||
* Will never fail due to delete being idempotent.
|
||||
* @param id - id of the price list
|
||||
* @returns {Promise<void>} empty promise
|
||||
*/
|
||||
async delete(id: string): Promise<void> {
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const priceListRepo = manager.getCustomRepository(this.priceListRepo_)
|
||||
|
||||
const priceList = await priceListRepo.findOne({ where: { id: id } })
|
||||
if (!priceList) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
await priceListRepo.delete(priceList)
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists Price Lists
|
||||
* @param {Object} selector - the query object for find
|
||||
* @param {Object} config - the config to be used for find
|
||||
* @return {Promise<PriceList[]>} the result of the find operation
|
||||
*/
|
||||
async list(
|
||||
selector: FilterablePriceListProps = {},
|
||||
config: FindConfig<PriceList> = { skip: 0, take: 20 }
|
||||
): Promise<PriceList[]> {
|
||||
const priceListRepo = this.manager_.getCustomRepository(this.priceListRepo_)
|
||||
|
||||
const query = this.buildQuery_(selector, config)
|
||||
return await priceListRepo.find(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists Price Lists and adds count
|
||||
* @param {Object} selector - the query object for find
|
||||
* @param {Object} config - the config to be used for find
|
||||
* @return {Promise} the result of the find operation
|
||||
*/
|
||||
async listAndCount(
|
||||
selector: FilterablePriceListProps = {},
|
||||
config: FindConfig<PriceList> = { skip: 0, take: 20 }
|
||||
): Promise<[PriceList[], number]> {
|
||||
const priceListRepo = this.manager_.getCustomRepository(this.priceListRepo_)
|
||||
|
||||
const query = this.buildQuery_(selector, config)
|
||||
return await priceListRepo.findAndCount(query)
|
||||
}
|
||||
|
||||
async upsertCustomerGroups_(
|
||||
priceListId: string,
|
||||
customerGroups: { id: string }[]
|
||||
): Promise<void> {
|
||||
const priceListRepo = this.manager_.getCustomRepository(this.priceListRepo_)
|
||||
const priceList = await this.retrieve(priceListId, { select: ["id"] })
|
||||
|
||||
const groups: CustomerGroup[] = []
|
||||
|
||||
for (const cg of customerGroups) {
|
||||
const customerGroup = await this.customerGroupService_.retrieve(cg.id)
|
||||
groups.push(customerGroup)
|
||||
}
|
||||
|
||||
priceList.customer_groups = groups
|
||||
|
||||
await priceListRepo.save(priceList)
|
||||
}
|
||||
}
|
||||
|
||||
export default PriceListService
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { Brackets, EntityManager, ILike, In, SelectQueryBuilder } from "typeorm"
|
||||
import { MoneyAmount } from "../models/money-amount"
|
||||
import { Brackets, EntityManager, ILike, SelectQueryBuilder } from "typeorm"
|
||||
import { MoneyAmount } from ".."
|
||||
import { Product } from "../models/product"
|
||||
import { ProductOptionValue } from "../models/product-option-value"
|
||||
import { ProductVariant } from "../models/product-variant"
|
||||
@@ -230,7 +230,6 @@ class ProductVariantService extends BaseService {
|
||||
await this.setRegionPrice(result.id, {
|
||||
amount: price.amount,
|
||||
region_id: price.region_id,
|
||||
sale_amount: price.sale_amount,
|
||||
})
|
||||
} else {
|
||||
await this.setCurrencyPrice(result.id, price)
|
||||
@@ -317,6 +316,13 @@ class ProductVariantService extends BaseService {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a variant's prices.
|
||||
* Deletes any prices that are not in the update object, and is not associated with a price list.
|
||||
* @param variantId - the id of variant variant
|
||||
* @param prices - the update prices
|
||||
* @returns {Promise<void>} empty promise
|
||||
*/
|
||||
async updateVariantPrices(
|
||||
variantId: string,
|
||||
prices: ProductVariantPrice[]
|
||||
@@ -337,7 +343,6 @@ class ProductVariantService extends BaseService {
|
||||
await this.setRegionPrice(variantId, {
|
||||
region_id: price.region_id,
|
||||
amount: price.amount,
|
||||
sale_amount: price.sale_amount || undefined,
|
||||
})
|
||||
} else {
|
||||
await this.setCurrencyPrice(variantId, price)
|
||||
@@ -348,25 +353,6 @@ class ProductVariantService extends BaseService {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default price for the given currency.
|
||||
* @param {string} variantId - the id of the variant to set prices for
|
||||
* @param {ProductVariantPrice} price - the price for the variant
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async setCurrencyPrice(
|
||||
variantId: string,
|
||||
price: ProductVariantPrice
|
||||
): Promise<MoneyAmount> {
|
||||
return this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const moneyAmountRepo = manager.getCustomRepository(
|
||||
this.moneyAmountRepository_
|
||||
)
|
||||
|
||||
return await moneyAmountRepo.upsertCurrencyPrice(variantId, price)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the price specific to a region. If no region specific money amount
|
||||
* exists the function will try to use a currency price. If no default
|
||||
@@ -406,17 +392,12 @@ class ProductVariantService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
// Always return sale price, if present
|
||||
if (moneyAmount.sale_amount) {
|
||||
return moneyAmount.sale_amount
|
||||
}
|
||||
|
||||
return moneyAmount.amount
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the price of a specific region
|
||||
* Sets the default price of a specific region
|
||||
* @param {string} variantId - the id of the variant to update
|
||||
* @param {string} price - the price for the variant.
|
||||
* @return {Promise} the result of the update operation
|
||||
@@ -434,6 +415,7 @@ class ProductVariantService extends BaseService {
|
||||
where: {
|
||||
variant_id: variantId,
|
||||
region_id: price.region_id,
|
||||
price_list_id: null,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -444,7 +426,6 @@ class ProductVariantService extends BaseService {
|
||||
})
|
||||
} else {
|
||||
moneyAmount.amount = price.amount
|
||||
moneyAmount.sale_amount = price.sale_amount
|
||||
}
|
||||
|
||||
const result = await moneyAmountRepo.save(moneyAmount)
|
||||
@@ -452,6 +433,25 @@ class ProductVariantService extends BaseService {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default price for the given currency.
|
||||
* @param {string} variantId - the id of the variant to set prices for
|
||||
* @param {ProductVariantPrice} price - the price for the variant
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async setCurrencyPrice(
|
||||
variantId: string,
|
||||
price: ProductVariantPrice
|
||||
): Promise<MoneyAmount> {
|
||||
return this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const moneyAmountRepo = manager.getCustomRepository(
|
||||
this.moneyAmountRepository_
|
||||
)
|
||||
|
||||
return await moneyAmountRepo.upsertVariantCurrencyPrice(variantId, price)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates variant's option value.
|
||||
* Option value must be of type string or number.
|
||||
@@ -636,7 +636,10 @@ class ProductVariantService extends BaseService {
|
||||
this.productVariantRepository_
|
||||
)
|
||||
|
||||
const variant = await variantRepo.findOne({ where: { id: variantId } })
|
||||
const variant = await variantRepo.findOne({
|
||||
where: { id: variantId },
|
||||
relations: ["prices"],
|
||||
})
|
||||
|
||||
if (!variant) {
|
||||
return Promise.resolve()
|
||||
@@ -662,7 +665,10 @@ class ProductVariantService extends BaseService {
|
||||
* @param {Object} metadata - the metadata to set
|
||||
* @return {Object} updated metadata object
|
||||
*/
|
||||
setMetadata_(variant: ProductVariant, metadata: object): Record<string, any> {
|
||||
setMetadata_(
|
||||
variant: ProductVariant,
|
||||
metadata: object
|
||||
): Record<string, unknown> {
|
||||
const existing = variant.metadata || {}
|
||||
const newData = {}
|
||||
for (const [key, value] of Object.entries(metadata)) {
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Type } from "class-transformer"
|
||||
import {
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Validate,
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { PriceList } from "../models/price-list"
|
||||
import { DateComparisonOperator } from "./common"
|
||||
import { XorConstraint } from "./validators/xor"
|
||||
|
||||
export enum PriceListType {
|
||||
SALE = "sale",
|
||||
OVERRIDE = "override",
|
||||
}
|
||||
|
||||
export enum PriceListStatus {
|
||||
ACTIVE = "active",
|
||||
DRAFT = "draft",
|
||||
}
|
||||
|
||||
export class FilterablePriceListProps {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
id?: string
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
q?: string
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(PriceListStatus, { each: true })
|
||||
status?: PriceListStatus[]
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(PriceListType, { each: true })
|
||||
type?: PriceListType[]
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DateComparisonOperator)
|
||||
created_at?: DateComparisonOperator
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DateComparisonOperator)
|
||||
updated_at?: DateComparisonOperator
|
||||
|
||||
@ValidateNested()
|
||||
@IsOptional()
|
||||
@Type(() => DateComparisonOperator)
|
||||
deleted_at?: DateComparisonOperator
|
||||
}
|
||||
|
||||
export class AdminPriceListPricesUpdateReq {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
id?: string
|
||||
|
||||
@ValidateIf((o) => !o.id)
|
||||
@Validate(XorConstraint, ["currency_code"])
|
||||
region_id?: string
|
||||
|
||||
@ValidateIf((o) => !o.id)
|
||||
@Validate(XorConstraint, ["region_id"])
|
||||
currency_code?: string
|
||||
|
||||
@IsString()
|
||||
variant_id: string
|
||||
|
||||
@IsInt()
|
||||
amount: number
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
min_quantity?: number
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
max_quantity?: number
|
||||
}
|
||||
|
||||
export class AdminPriceListPricesCreateReq {
|
||||
@Validate(XorConstraint, ["currency_code"])
|
||||
region_id?: string
|
||||
|
||||
@Validate(XorConstraint, ["region_id"])
|
||||
currency_code?: string
|
||||
|
||||
@IsInt()
|
||||
amount: number
|
||||
|
||||
@IsString()
|
||||
variant_id: string
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
min_quantity?: number
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
max_quantity?: number
|
||||
}
|
||||
|
||||
export type CreatePriceListInput = {
|
||||
name: string
|
||||
description: string
|
||||
type: PriceListType
|
||||
status?: PriceListStatus
|
||||
prices: AdminPriceListPricesCreateReq[]
|
||||
customer_groups?: { id: string }[]
|
||||
}
|
||||
|
||||
export type UpdatePriceListInput = Partial<
|
||||
Pick<
|
||||
PriceList,
|
||||
"name" | "description" | "starts_at" | "ends_at" | "status" | "type"
|
||||
>
|
||||
> & {
|
||||
prices?: AdminPriceListPricesUpdateReq[]
|
||||
customer_groups?: { id: string }[]
|
||||
}
|
||||
|
||||
export type PriceListPriceUpdateInput = {
|
||||
id?: string
|
||||
variant_id?: string
|
||||
region_id?: string
|
||||
currency_code?: string
|
||||
amount?: number
|
||||
min_quantity?: number
|
||||
max_quantity?: number
|
||||
}
|
||||
|
||||
export type PriceListPriceCreateInput = {
|
||||
region_id?: string
|
||||
currency_code?: string
|
||||
amount: number
|
||||
min_quantity?: number
|
||||
max_quantity?: number
|
||||
}
|
||||
@@ -1,16 +1,28 @@
|
||||
import { IsBoolean, IsNumber, IsString, ValidateNested } from "class-validator"
|
||||
import {
|
||||
IsBoolean,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Validate,
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { IsType } from "../utils/validators/is-type"
|
||||
import {
|
||||
DateComparisonOperator,
|
||||
NumericalComparisonOperator,
|
||||
StringComparisonOperator,
|
||||
} from "./common"
|
||||
import { XorConstraint } from "./validators/xor"
|
||||
|
||||
export type ProductVariantPrice = {
|
||||
id?: string
|
||||
currency_code?: string
|
||||
region_id?: string
|
||||
amount: number
|
||||
sale_amount?: number | undefined
|
||||
min_quantity?: number
|
||||
max_quantity?: number
|
||||
}
|
||||
|
||||
export type ProductVariantOption = {
|
||||
@@ -130,3 +142,47 @@ export class FilterableProductVariantProps {
|
||||
@IsType([DateComparisonOperator])
|
||||
updated_at?: DateComparisonOperator
|
||||
}
|
||||
|
||||
export class ProductVariantPricesUpdateReq {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
id?: string
|
||||
|
||||
@ValidateIf((o) => !o.id)
|
||||
@Validate(XorConstraint, ["currency_code"])
|
||||
region_id?: string
|
||||
|
||||
@ValidateIf((o) => !o.id)
|
||||
@Validate(XorConstraint, ["region_id"])
|
||||
currency_code?: string
|
||||
|
||||
@IsInt()
|
||||
amount: number
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
min_quantity?: number
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
max_quantity?: number
|
||||
}
|
||||
|
||||
export class ProductVariantPricesCreateReq {
|
||||
@Validate(XorConstraint, ["currency_code"])
|
||||
region_id?: string
|
||||
|
||||
@Validate(XorConstraint, ["region_id"])
|
||||
currency_code?: string
|
||||
|
||||
@IsInt()
|
||||
amount: number
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
min_quantity?: number
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
max_quantity?: number
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user