From d91da7b4181d63aa84277a8f81b2305df627e729 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Thu, 21 Nov 2024 14:08:42 +0100 Subject: [PATCH] feat(medusa): enable price list prices for product endpoints (#10197) --- .../__tests__/product/store/product.spec.ts | 415 +++++++++++++++++- .../src/api/store/products/middlewares.ts | 11 +- .../products/set-pricing-context.ts | 10 +- 3 files changed, 412 insertions(+), 24 deletions(-) diff --git a/integration-tests/http/__tests__/product/store/product.spec.ts b/integration-tests/http/__tests__/product/store/product.spec.ts index 0697ac67a7975..e5618d5f4ec45 100644 --- a/integration-tests/http/__tests__/product/store/product.spec.ts +++ b/integration-tests/http/__tests__/product/store/product.spec.ts @@ -1,6 +1,12 @@ import { medusaIntegrationTestRunner } from "@medusajs/test-utils" import { IStoreModuleService } from "@medusajs/types" -import { ApiKeyType, Modules, ProductStatus } from "@medusajs/utils" +import { + ApiKeyType, + Modules, + PriceListStatus, + PriceListType, + ProductStatus, +} from "@medusajs/utils" import qs from "qs" import { adminHeaders, @@ -9,6 +15,7 @@ import { generateStoreHeaders, } from "../../../../helpers/create-admin-user" import { getProductFixture } from "../../../../helpers/fixtures" +import { createAuthenticatedCustomer } from "../../../../modules/helpers/create-authenticated-customer" jest.setTimeout(30000) @@ -27,10 +34,13 @@ medusaIntegrationTestRunner({ let variant2 let variant3 let variant4 + let region let inventoryItem1 let inventoryItem2 let storeHeaders let publishableKey + let storeHeadersWithCustomer + let customer const createProducts = async (data) => { const response = await api.post( @@ -86,6 +96,19 @@ medusaIntegrationTestRunner({ publishableKey = await generatePublishableKey(appContainer) storeHeaders = generateStoreHeaders({ publishableKey }) await createAdminUser(dbConnection, adminHeaders, appContainer) + const result = await createAuthenticatedCustomer(api, storeHeaders, { + first_name: "tony", + last_name: "stark", + email: "tony@stark-industries.com", + }) + + customer = result.customer + storeHeadersWithCustomer = { + headers: { + ...storeHeaders.headers, + authorization: `Bearer ${result.jwt}`, + }, + } const storeModule: IStoreModuleService = appContainer.resolve( Modules.STORE @@ -104,6 +127,14 @@ medusaIntegrationTestRunner({ { currency_code: "dkk" }, ], }) + + region = ( + await api.post( + "/admin/regions", + { name: "Test Region", currency_code: "usd" }, + adminHeaders + ) + ).data.region }) describe("Get products based on publishable key", () => { @@ -954,14 +985,6 @@ medusaIntegrationTestRunner({ }) it("should list products with prices when context is present", async () => { - const region = ( - await api.post( - "/admin/regions", - { name: "Test Region", currency_code: "usd" }, - adminHeaders - ) - ).data.region - let response = await api.get( `/store/products?fields=*variants.calculated_price®ion_id=${region.id}`, storeHeaders @@ -1023,6 +1046,192 @@ medusaIntegrationTestRunner({ expect(response.data.products).toEqual(expectation) }) + describe("with price lists", () => { + let customerGroup + + beforeEach(async () => { + customerGroup = ( + await api.post( + "/admin/customer-groups", + { name: "VIP" }, + adminHeaders + ) + ).data.customer_group + + await api.post( + `/admin/customer-groups/${customerGroup.id}/customers`, + { add: [customer.id] }, + adminHeaders + ) + }) + + it("should list products with prices with a sale price list price", async () => { + const priceList = ( + await api.post( + `/admin/price-lists`, + { + title: "test price list", + description: "test", + status: PriceListStatus.ACTIVE, + type: PriceListType.SALE, + prices: [ + { + amount: 350, + currency_code: "usd", + variant_id: product.variants[0].id, + }, + ], + rules: { customer_group_id: [customerGroup.id] }, + }, + adminHeaders + ) + ).data.price_list + + let response = await api.get( + `/store/products?fields=*variants.calculated_price®ion_id=${region.id}`, + storeHeadersWithCustomer + ) + + const expectation = expect.arrayContaining([ + expect.objectContaining({ + id: product.id, + variants: [ + expect.objectContaining({ + calculated_price: { + id: expect.any(String), + is_calculated_price_price_list: true, + is_calculated_price_tax_inclusive: false, + calculated_amount: 350, + raw_calculated_amount: { + value: "350", + precision: 20, + }, + is_original_price_price_list: false, + is_original_price_tax_inclusive: false, + original_amount: 3000, + raw_original_amount: { + value: "3000", + precision: 20, + }, + currency_code: "usd", + calculated_price: { + id: expect.any(String), + price_list_id: priceList.id, + price_list_type: "sale", + min_quantity: null, + max_quantity: null, + }, + original_price: { + id: expect.any(String), + price_list_id: null, + price_list_type: null, + min_quantity: null, + max_quantity: null, + }, + }, + }), + ], + }), + ]) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(3) + expect(response.data.products).toEqual(expectation) + + // with only region_id + response = await api.get( + `/store/products?region_id=${region.id}`, + storeHeadersWithCustomer + ) + + expect(response.status).toEqual(200) + expect(response.data.products).toEqual(expectation) + }) + + it("should list products with prices with a override price list price", async () => { + const priceList = ( + await api.post( + `/admin/price-lists`, + { + title: "test price list", + description: "test", + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + prices: [ + { + amount: 350, + currency_code: "usd", + variant_id: product.variants[0].id, + }, + ], + rules: { customer_group_id: [customerGroup.id] }, + }, + adminHeaders + ) + ).data.price_list + + let response = await api.get( + `/store/products?fields=*variants.calculated_price®ion_id=${region.id}`, + storeHeadersWithCustomer + ) + + const expectation = expect.arrayContaining([ + expect.objectContaining({ + id: product.id, + variants: [ + expect.objectContaining({ + calculated_price: { + id: expect.any(String), + is_calculated_price_price_list: true, + is_calculated_price_tax_inclusive: false, + calculated_amount: 350, + raw_calculated_amount: { + value: "350", + precision: 20, + }, + is_original_price_price_list: true, + is_original_price_tax_inclusive: false, + original_amount: 350, + raw_original_amount: { + value: "350", + precision: 20, + }, + currency_code: "usd", + calculated_price: { + id: expect.any(String), + price_list_id: priceList.id, + price_list_type: "override", + min_quantity: null, + max_quantity: null, + }, + original_price: { + id: expect.any(String), + price_list_id: priceList.id, + price_list_type: "override", + min_quantity: null, + max_quantity: null, + }, + }, + }), + ], + }), + ]) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(3) + expect(response.data.products).toEqual(expectation) + + // with only region_id + response = await api.get( + `/store/products?region_id=${region.id}`, + storeHeadersWithCustomer + ) + + expect(response.status).toEqual(200) + expect(response.data.products).toEqual(expectation) + }) + }) + describe("with inventory items", () => { let location1 let location2 @@ -1291,14 +1500,6 @@ medusaIntegrationTestRunner({ }) it("should get product with prices when context is present", async () => { - const region = ( - await api.post( - "/admin/regions", - { name: "Test Region", currency_code: "usd" }, - adminHeaders - ) - ).data.region - let response = await api.get( `/store/products/${product.id}?fields=*variants.calculated_price®ion_id=${region.id}`, storeHeaders @@ -1356,6 +1557,186 @@ medusaIntegrationTestRunner({ expect(response.status).toEqual(200) expect(response.data.product).toEqual(expectation) }) + + describe("with price lists", () => { + let customerGroup + + beforeEach(async () => { + customerGroup = ( + await api.post( + "/admin/customer-groups", + { name: "VIP" }, + adminHeaders + ) + ).data.customer_group + + await api.post( + `/admin/customer-groups/${customerGroup.id}/customers`, + { add: [customer.id] }, + adminHeaders + ) + }) + + it("should return product with sale price list prices", async () => { + const priceList = ( + await api.post( + `/admin/price-lists`, + { + title: "test price list", + description: "test", + status: PriceListStatus.ACTIVE, + type: PriceListType.SALE, + prices: [ + { + amount: 350, + currency_code: "usd", + variant_id: product.variants[0].id, + }, + ], + rules: { customer_group_id: [customerGroup.id] }, + }, + adminHeaders + ) + ).data.price_list + + let response = await api.get( + `/store/products/${product.id}?fields=*variants.calculated_price®ion_id=${region.id}`, + storeHeadersWithCustomer + ) + + const expectation = expect.objectContaining({ + id: product.id, + variants: [ + expect.objectContaining({ + calculated_price: { + id: expect.any(String), + is_calculated_price_price_list: true, + is_calculated_price_tax_inclusive: false, + calculated_amount: 350, + raw_calculated_amount: { + value: "350", + precision: 20, + }, + is_original_price_price_list: false, + is_original_price_tax_inclusive: false, + original_amount: 3000, + raw_original_amount: { + value: "3000", + precision: 20, + }, + currency_code: "usd", + calculated_price: { + id: expect.any(String), + price_list_id: priceList.id, + price_list_type: "sale", + min_quantity: null, + max_quantity: null, + }, + original_price: { + id: expect.any(String), + price_list_id: null, + price_list_type: null, + min_quantity: null, + max_quantity: null, + }, + }, + }), + ], + }) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual(expectation) + + // with only region_id + response = await api.get( + `/store/products/${product.id}?region_id=${region.id}`, + storeHeadersWithCustomer + ) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual(expectation) + }) + + it("should list products with prices with a override price list price", async () => { + const priceList = ( + await api.post( + `/admin/price-lists`, + { + title: "test price list", + description: "test", + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + prices: [ + { + amount: 350, + currency_code: "usd", + variant_id: product.variants[0].id, + }, + ], + rules: { customer_group_id: [customerGroup.id] }, + }, + adminHeaders + ) + ).data.price_list + + let response = await api.get( + `/store/products/${product.id}?fields=*variants.calculated_price®ion_id=${region.id}`, + storeHeadersWithCustomer + ) + + const expectation = expect.objectContaining({ + id: product.id, + variants: [ + expect.objectContaining({ + calculated_price: { + id: expect.any(String), + is_calculated_price_price_list: true, + is_calculated_price_tax_inclusive: false, + calculated_amount: 350, + raw_calculated_amount: { + value: "350", + precision: 20, + }, + is_original_price_price_list: true, + is_original_price_tax_inclusive: false, + original_amount: 350, + raw_original_amount: { + value: "350", + precision: 20, + }, + currency_code: "usd", + calculated_price: { + id: expect.any(String), + price_list_id: priceList.id, + price_list_type: "override", + min_quantity: null, + max_quantity: null, + }, + original_price: { + id: expect.any(String), + price_list_id: priceList.id, + price_list_type: "override", + min_quantity: null, + max_quantity: null, + }, + }, + }), + ], + }) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual(expectation) + + // with only region_id + response = await api.get( + `/store/products/${product.id}?region_id=${region.id}`, + storeHeadersWithCustomer + ) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual(expectation) + }) + }) }) describe("Tax handling", () => { diff --git a/packages/medusa/src/api/store/products/middlewares.ts b/packages/medusa/src/api/store/products/middlewares.ts index 344562fd949df..dc10ace14f688 100644 --- a/packages/medusa/src/api/store/products/middlewares.ts +++ b/packages/medusa/src/api/store/products/middlewares.ts @@ -1,19 +1,20 @@ -import { isPresent, ProductStatus } from "@medusajs/framework/utils" +import { validateAndTransformQuery } from "@medusajs/framework" import { applyDefaultFilters, applyParamsAsFilters, + authenticate, clearFiltersByKey, maybeApplyLinkFilter, MiddlewareRoute, setContext, } from "@medusajs/framework/http" +import { isPresent, ProductStatus } from "@medusajs/framework/utils" import { filterByValidSalesChannels, normalizeDataForContext, setPricingContext, setTaxContext, } from "../../utils/middlewares" -import { validateAndTransformQuery } from "@medusajs/framework" import { maybeApplyStockLocationId } from "./helpers" import * as QueryConfig from "./query-config" import { StoreGetProductsParams } from "./validators" @@ -23,6 +24,9 @@ export const storeProductRoutesMiddlewares: MiddlewareRoute[] = [ method: ["GET"], matcher: "/store/products", middlewares: [ + authenticate("customer", ["session", "bearer"], { + allowUnauthenticated: true, + }), validateAndTransformQuery( StoreGetProductsParams, QueryConfig.listProductQueryConfig @@ -60,6 +64,9 @@ export const storeProductRoutesMiddlewares: MiddlewareRoute[] = [ method: ["GET"], matcher: "/store/products/:id", middlewares: [ + authenticate("customer", ["session", "bearer"], { + allowUnauthenticated: true, + }), validateAndTransformQuery( StoreGetProductsParams, QueryConfig.retrieveProductQueryConfig diff --git a/packages/medusa/src/api/utils/middlewares/products/set-pricing-context.ts b/packages/medusa/src/api/utils/middlewares/products/set-pricing-context.ts index 3048b4b20477f..b062851533a5a 100644 --- a/packages/medusa/src/api/utils/middlewares/products/set-pricing-context.ts +++ b/packages/medusa/src/api/utils/middlewares/products/set-pricing-context.ts @@ -1,11 +1,11 @@ -import { MedusaPricingContext } from "@medusajs/framework/types" -import { MedusaError } from "@medusajs/framework/utils" -import { NextFunction } from "express" import { AuthenticatedMedusaRequest, refetchEntities, refetchEntity, } from "@medusajs/framework/http" +import { MedusaPricingContext } from "@medusajs/framework/types" +import { MedusaError } from "@medusajs/framework/utils" +import { NextFunction } from "express" export function setPricingContext() { return async (req: AuthenticatedMedusaRequest, _, next: NextFunction) => { @@ -41,10 +41,10 @@ export function setPricingContext() { } // Find all the customer groups the customer is a part of and set - if (req.user?.customer_id) { + if (req.auth_context?.actor_id) { const customerGroups = await refetchEntities( "customer_group", - { customer_id: req.user?.customer_id }, + { customers: { id: req.auth_context.actor_id } }, req.scope, ["id"] )