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:
Kasper Fabricius Kristensen
2022-03-18 15:18:50 +01:00
committed by GitHub
co-authored by Philip Korsholm Sebastian Rindom
parent 23f8399c16
commit 5300926db8
52 changed files with 4234 additions and 505 deletions
+21 -20
View File
@@ -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 })