From a54dc68db7a7d476cf4bf8d36c122c7f34629c90 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Sun, 21 Aug 2022 18:26:25 +0700 Subject: [PATCH] feat(medusa): Filtering Customer Orders (#975) --- .changeset/khaki-spiders-hug.md | 5 + .../api/__tests__/store/customer.js | 148 ++++++++++++++++- .../src/api/routes/store/customers/index.ts | 17 +- .../api/routes/store/customers/list-orders.ts | 151 +++++++++++++----- packages/medusa/src/services/order.ts | 5 + packages/medusa/src/types/global.ts | 2 +- 6 files changed, 280 insertions(+), 48 deletions(-) create mode 100644 .changeset/khaki-spiders-hug.md diff --git a/.changeset/khaki-spiders-hug.md b/.changeset/khaki-spiders-hug.md new file mode 100644 index 0000000000..d6c2fc4767 --- /dev/null +++ b/.changeset/khaki-spiders-hug.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +Allow filtering of customer orders diff --git a/integration-tests/api/__tests__/store/customer.js b/integration-tests/api/__tests__/store/customer.js index a3e8fdbdd4..f9d3137b42 100644 --- a/integration-tests/api/__tests__/store/customer.js +++ b/integration-tests/api/__tests__/store/customer.js @@ -1,5 +1,5 @@ const path = require("path") -const { Address, Customer } = require("@medusajs/medusa") +const { Address, Customer, Order, Region } = require("@medusajs/medusa") const setupServer = require("../../../helpers/setup-server") const { useApi } = require("../../../helpers/use-api") @@ -19,7 +19,7 @@ describe("/store/customers", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd }) + medusaProcess = await setupServer({ cwd, verbose: false }) }) afterAll(async () => { @@ -89,6 +89,150 @@ describe("/store/customers", () => { }) }) + describe("GET /store/customers/me/orders", () => { + beforeEach(async () => { + const manager = dbConnection.manager + await manager.query(`ALTER SEQUENCE order_display_id_seq RESTART WITH 1`) + + await manager.insert(Address, { + id: "addr_test", + first_name: "String", + last_name: "Stringson", + address_1: "String st", + city: "Stringville", + postal_code: "1236", + province: "ca", + country_code: "us", + }) + + await manager.insert(Region, { + id: "region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + await manager.insert(Customer, { + id: "test_customer", + first_name: "John", + last_name: "Deere", + email: "john@deere.com", + password_hash: + "c2NyeXB0AAEAAAABAAAAAVMdaddoGjwU1TafDLLlBKnOTQga7P2dbrfgf3fB+rCD/cJOMuGzAvRdKutbYkVpuJWTU39P7OpuWNkUVoEETOVLMJafbI8qs8Qx/7jMQXkN", // password matching "test" + has_account: true, + }) + + await manager.insert(Customer, { + id: "test_customer1", + first_name: "John", + last_name: "Deere", + email: "joh1n@deere.com", + password_hash: + "c2NyeXB0AAEAAAABAAAAAVMdaddoGjwU1TafDLLlBKnOTQga7P2dbrfgf3fB+rCD/cJOMuGzAvRdKutbYkVpuJWTU39P7OpuWNkUVoEETOVLMJafbI8qs8Qx/7jMQXkN", // password matching "test" + has_account: true, + }) + + await manager.insert(Order, { + id: "order_test_completed", + email: "test1@email.com", + display_id: 1, + customer_id: "test_customer", + region_id: "region", + status: "completed", + tax_rate: 0, + currency_code: "usd", + }) + + await manager.insert(Order, { + id: "order_test_completed1", + email: "test1@email.com", + display_id: 2, + customer_id: "test_customer1", + region_id: "region", + status: "completed", + tax_rate: 0, + currency_code: "usd", + }) + + await manager.insert(Order, { + id: "order_test_canceled", + email: "test1@email.com", + display_id: 3, + customer_id: "test_customer", + region_id: "region", + status: "canceled", + tax_rate: 0, + currency_code: "usd", + }) + }) + + afterEach(async () => { + await doAfterEach() + }) + + it("looks up completed orders", async () => { + const api = useApi() + + const authResponse = await api.post("/store/auth", { + email: "john@deere.com", + password: "test", + }) + + const [authCookie] = authResponse.headers["set-cookie"][0].split(";") + + const response = await api + .get("/store/customers/me/orders?status[]=completed", { + headers: { + Cookie: authCookie, + }, + }) + .catch((err) => { + return err.response + }) + expect(response.status).toEqual(200) + expect(response.data.orders[0].display_id).toEqual(1) + expect(response.data.orders[0].email).toEqual("test1@email.com") + expect(response.data.orders.length).toEqual(1) + }) + + it("looks up cancelled and completed orders", async () => { + const api = useApi() + + const authResponse = await api.post("/store/auth", { + email: "john@deere.com", + password: "test", + }) + + const [authCookie] = authResponse.headers["set-cookie"][0].split(";") + + const response = await api + .get( + "/store/customers/me/orders?status[]=completed&status[]=canceled", + { + headers: { + Cookie: authCookie, + }, + } + ) + .catch((err) => { + return console.log(err.response.data.message) + }) + + expect(response.status).toEqual(200) + expect(response.data.orders).toEqual([ + expect.objectContaining({ + display_id: 3, + status: "canceled", + }), + expect.objectContaining({ + display_id: 1, + status: "completed", + }), + ]) + expect(response.data.orders.length).toEqual(2) + }) + }) + describe("POST /store/customers/me", () => { beforeEach(async () => { const manager = dbConnection.manager diff --git a/packages/medusa/src/api/routes/store/customers/index.ts b/packages/medusa/src/api/routes/store/customers/index.ts index f884948201..5fcbf2d8aa 100644 --- a/packages/medusa/src/api/routes/store/customers/index.ts +++ b/packages/medusa/src/api/routes/store/customers/index.ts @@ -1,7 +1,12 @@ import { Router } from "express" import { Customer, Order } from "../../../.." import { PaginatedResponse } from "../../../../types/common" -import middlewares from "../../../middlewares" +import middlewares, { transformQuery } from "../../../middlewares" +import { StoreGetCustomersCustomerOrdersParams } from "./list-orders" +import { + defaultStoreOrdersRelations, + defaultStoreOrdersFields, +} from "../orders" const route = Router() @@ -34,7 +39,15 @@ export default (app, container) => { route.get("/me", middlewares.wrap(require("./get-customer").default)) route.post("/me", middlewares.wrap(require("./update-customer").default)) - route.get("/me/orders", middlewares.wrap(require("./list-orders").default)) + route.get( + "/me/orders", + transformQuery(StoreGetCustomersCustomerOrdersParams, { + defaultFields: defaultStoreOrdersFields, + defaultRelations: defaultStoreOrdersRelations, + isList: true, + }), + middlewares.wrap(require("./list-orders").default) + ) route.post( "/me/addresses", diff --git a/packages/medusa/src/api/routes/store/customers/list-orders.ts b/packages/medusa/src/api/routes/store/customers/list-orders.ts index 642f52d761..979219cea0 100644 --- a/packages/medusa/src/api/routes/store/customers/list-orders.ts +++ b/packages/medusa/src/api/routes/store/customers/list-orders.ts @@ -1,14 +1,20 @@ -import { IsNumber, IsOptional, IsString } from "class-validator" -import { - allowedStoreOrdersFields, - allowedStoreOrdersRelations, -} from "../orders" -import { FindConfig } from "../../../../types/common" -import { Order } from "../../../../models" - -import OrderService from "../../../../services/order" import { Type } from "class-transformer" -import { validator } from "../../../../utils/validator" +import { + IsEnum, + IsNumber, + IsOptional, + IsString, + ValidateNested, +} from "class-validator" +import { Request, Response } from "express" +import { MedusaError } from "medusa-core-utils" +import { + FulfillmentStatus, + OrderStatus, + PaymentStatus, +} from "../../../../models/order" +import OrderService from "../../../../services/order" +import { DateComparisonOperator } from "../../../../types/common" /** * @oas [get] /customers/me/orders @@ -17,6 +23,20 @@ import { validator } from "../../../../utils/validator" * description: "Retrieves a list of a Customer's Orders." * x-authenticated: true * parameters: + * - (query) q {string} Query used for searching orders. + * - (query) id {string} Id of the order to search for. + * - (query) status {string[]} Status to search for. + * - (query) fulfillment_status {string[]} Fulfillment status to search for. + * - (query) payment_status {string[]} Payment status to search for. + * - (query) display_id {string} Display id to search for. + * - (query) cart_id {string} to search for. + * - (query) email {string} to search for. + * - (query) region_id {string} to search for. + * - (query) currency_code {string} to search for. + * - (query) tax_rate {string} to search for. + * - (query) cancelled_at {DateComparisonOperator} Date comparison for when resulting orders was cancelled, i.e. less than, greater than etc. + * - (query) created_at {DateComparisonOperator} Date comparison for when resulting orders was created, i.e. less than, greater than etc. + * - (query) updated_at {DateComparisonOperator} Date comparison for when resulting orders was updated, i.e. less than, greater than etc. * - (query) limit=10 {integer} How many orders to return. * - (query) offset=0 {integer} The offset in the resulting orders. * - (query) fields {string} (Comma separated string) Which fields should be included in the resulting orders. @@ -44,50 +64,34 @@ import { validator } from "../../../../utils/validator" * type: integer * description: The number of items per page */ -export default async (req, res) => { - const id: string = req.user.customer_id +export default async (req: Request, res: Response) => { + const id: string | undefined = req.user?.customer_id + + if (!id) { + throw new MedusaError( + MedusaError.Types.UNEXPECTED_STATE, + "Not authorized to list orders" + ) + } const orderService: OrderService = req.scope.resolve("orderService") - const selector = { + req.filterableFields = { + ...req.filterableFields, customer_id: id, } - const validated = await validator( - StoreGetCustomersCustomerOrdersParams, - req.query + const [orders, count] = await orderService.listAndCount( + req.filterableFields, + req.listConfig ) - let includeFields: string[] = [] - if (validated.fields) { - includeFields = validated.fields.split(",") - includeFields = includeFields.filter((f) => - allowedStoreOrdersFields.includes(f) - ) - } + const { limit, offset } = req.validatedQuery - let expandFields: string[] = [] - if (validated.expand) { - expandFields = validated.expand.split(",") - expandFields = expandFields.filter((f) => - allowedStoreOrdersRelations.includes(f) - ) - } - - const listConfig = { - select: includeFields.length ? includeFields : allowedStoreOrdersFields, - relations: expandFields.length ? expandFields : allowedStoreOrdersRelations, - skip: validated.offset, - take: validated.limit, - order: { created_at: "DESC" }, - } as FindConfig - - const [orders, count] = await orderService.listAndCount(selector, listConfig) - - res.json({ orders, count, offset: validated.offset, limit: validated.limit }) + res.json({ orders, count, offset: offset, limit: limit }) } -export class StoreGetCustomersCustomerOrdersParams { +export class StoreGetCustomersCustomerOrdersPaginationParams { @IsOptional() @IsNumber() @Type(() => Number) @@ -106,3 +110,64 @@ export class StoreGetCustomersCustomerOrdersParams { @IsString() expand?: string } + +export class StoreGetCustomersCustomerOrdersParams extends StoreGetCustomersCustomerOrdersPaginationParams { + @IsString() + @IsOptional() + id?: string + + @IsString() + @IsOptional() + q?: string + + @IsOptional() + @IsEnum(OrderStatus, { each: true }) + status?: OrderStatus[] + + @IsOptional() + @IsEnum(FulfillmentStatus, { each: true }) + fulfillment_status?: FulfillmentStatus[] + + @IsOptional() + @IsEnum(PaymentStatus, { each: true }) + payment_status?: PaymentStatus[] + + @IsString() + @IsOptional() + display_id?: string + + @IsString() + @IsOptional() + cart_id?: string + + @IsString() + @IsOptional() + email?: string + + @IsString() + @IsOptional() + region_id?: string + + @IsString() + @IsOptional() + currency_code?: string + + @IsString() + @IsOptional() + tax_rate?: string + + @IsOptional() + @ValidateNested() + @Type(() => DateComparisonOperator) + created_at?: DateComparisonOperator + + @IsOptional() + @ValidateNested() + @Type(() => DateComparisonOperator) + updated_at?: DateComparisonOperator + + @ValidateNested() + @IsOptional() + @Type(() => DateComparisonOperator) + canceled_at?: DateComparisonOperator +} diff --git a/packages/medusa/src/services/order.ts b/packages/medusa/src/services/order.ts index ed2bc869b8..34d63507f1 100644 --- a/packages/medusa/src/services/order.ts +++ b/packages/medusa/src/services/order.ts @@ -182,6 +182,11 @@ class OrderService extends TransactionBaseService { ) } + /** + * @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: QuerySelector, config: FindConfig = { diff --git a/packages/medusa/src/types/global.ts b/packages/medusa/src/types/global.ts index 18f0251ab7..8e890e6e22 100644 --- a/packages/medusa/src/types/global.ts +++ b/packages/medusa/src/types/global.ts @@ -9,7 +9,7 @@ declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Express { interface Request { - user?: (User | Customer) & { userId?: string } + user?: (User | Customer) & { customer_id?: string; userId?: string } scope: MedusaContainer validatedQuery: RequestQueryFields & Record validatedBody: unknown