From 14366f536decc88546658e23521961a82409e842 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Tue, 14 Jun 2022 10:11:18 +0200 Subject: [PATCH] fix: adds tax calculation to product pricing (#1354) * fix: adds tax calculation to product pricing * fix: dedupe * fix: merge inconsistencies * fix: merge inconsistencies --- .../admin/__snapshots__/product.js.snap | 45 ++ .../api/__tests__/admin/product.js | 2 +- .../__snapshots__/product-variants.js.snap | 31 ++ .../store/__snapshots__/products.js.snap | 20 + .../api/__tests__/store/product-variants.js | 2 +- .../api/routes/admin/products/add-option.ts | 7 +- .../routes/admin/products/create-product.ts | 6 +- .../routes/admin/products/delete-variant.ts | 10 +- .../api/routes/admin/products/get-product.ts | 7 +- .../src/api/routes/admin/products/index.ts | 8 +- .../routes/admin/products/update-product.ts | 6 +- .../routes/admin/products/update-variant.ts | 13 +- .../store/products/__tests__/get-product.js | 8 - .../store/products/__tests__/list-products.js | 8 - .../api/routes/store/products/get-product.ts | 32 +- .../routes/store/products/list-products.ts | 38 +- .../store/variants/__tests__/get-variant.js | 4 - .../api/routes/store/variants/get-variant.ts | 32 +- .../routes/store/variants/list-variants.ts | 39 +- .../products/admin-list-products.ts | 16 +- .../src/repositories/product-variant.ts | 9 +- .../medusa/src/services/__mocks__/pricing.js | 17 + .../src/services/__tests__/line-item.js | 54 ++- packages/medusa/src/services/index.ts | 1 + packages/medusa/src/services/line-item.ts | 21 +- packages/medusa/src/services/pricing.ts | 389 ++++++++++++++++++ .../medusa/src/services/product-variant.ts | 136 +----- packages/medusa/src/services/product.js | 207 +--------- packages/medusa/src/services/tax-provider.ts | 19 +- packages/medusa/src/types/pricing.ts | 30 ++ yarn.lock | 9 +- 31 files changed, 792 insertions(+), 434 deletions(-) create mode 100644 packages/medusa/src/services/__mocks__/pricing.js create mode 100644 packages/medusa/src/services/pricing.ts create mode 100644 packages/medusa/src/types/pricing.ts diff --git a/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap index a4a7db30fd..4f26a78ef8 100644 --- a/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap +++ b/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap @@ -79,6 +79,8 @@ Array [ "allow_backorder": false, "barcode": "test-barcode", "calculated_price": null, + "calculated_price_incl_tax": null, + "calculated_tax": null, "created_at": Any, "deleted_at": null, "ean": "test-ean", @@ -105,6 +107,8 @@ Array [ ], "origin_country": null, "original_price": null, + "original_price_incl_tax": null, + "original_tax": null, "prices": Array [ Object { "amount": 100, @@ -123,6 +127,7 @@ Array [ ], "product_id": StringMatching /\\^test-\\*/, "sku": "test-sku", + "tax_rates": null, "title": "Test variant", "upc": "test-upc", "updated_at": Any, @@ -133,6 +138,8 @@ Array [ "allow_backorder": false, "barcode": null, "calculated_price": null, + "calculated_price_incl_tax": null, + "calculated_tax": null, "created_at": Any, "deleted_at": null, "ean": "test-ean2", @@ -159,6 +166,8 @@ Array [ ], "origin_country": null, "original_price": null, + "original_price_incl_tax": null, + "original_tax": null, "prices": Array [ Object { "amount": 100, @@ -177,6 +186,7 @@ Array [ ], "product_id": StringMatching /\\^test-\\*/, "sku": "test-sku2", + "tax_rates": null, "title": "Test variant rank (2)", "upc": "test-upc2", "updated_at": Any, @@ -187,6 +197,8 @@ Array [ "allow_backorder": false, "barcode": "test-barcode 1", "calculated_price": null, + "calculated_price_incl_tax": null, + "calculated_tax": null, "created_at": Any, "deleted_at": null, "ean": "test-ean1", @@ -213,6 +225,8 @@ Array [ ], "origin_country": null, "original_price": null, + "original_price_incl_tax": null, + "original_tax": null, "prices": Array [ Object { "amount": 100, @@ -231,6 +245,7 @@ Array [ ], "product_id": StringMatching /\\^test-\\*/, "sku": "test-sku1", + "tax_rates": null, "title": "Test variant rank (1)", "upc": "test-upc1", "updated_at": Any, @@ -241,6 +256,8 @@ Array [ "allow_backorder": false, "barcode": "test-barcode-sale", "calculated_price": null, + "calculated_price_incl_tax": null, + "calculated_tax": null, "created_at": Any, "deleted_at": null, "ean": "test-ean-sale", @@ -267,6 +284,8 @@ Array [ ], "origin_country": null, "original_price": null, + "original_price_incl_tax": null, + "original_tax": null, "prices": Array [ Object { "amount": 1000, @@ -285,6 +304,7 @@ Array [ ], "product_id": StringMatching /\\^test-\\*/, "sku": "test-sku-sale", + "tax_rates": null, "title": "Test variant", "upc": "test-upc-sale", "updated_at": Any, @@ -353,6 +373,8 @@ Array [ "allow_backorder": false, "barcode": null, "calculated_price": null, + "calculated_price_incl_tax": null, + "calculated_tax": null, "created_at": Any, "deleted_at": null, "ean": "test-ean4", @@ -379,6 +401,8 @@ Array [ ], "origin_country": null, "original_price": null, + "original_price_incl_tax": null, + "original_tax": null, "prices": Array [ Object { "amount": 100, @@ -397,6 +421,7 @@ Array [ ], "product_id": StringMatching /\\^test-\\*/, "sku": "test-sku4", + "tax_rates": null, "title": "Test variant rank (2)", "upc": "test-upc4", "updated_at": Any, @@ -407,6 +432,8 @@ Array [ "allow_backorder": false, "barcode": null, "calculated_price": null, + "calculated_price_incl_tax": null, + "calculated_tax": null, "created_at": Any, "deleted_at": null, "ean": "test-ean3", @@ -433,6 +460,8 @@ Array [ ], "origin_country": null, "original_price": null, + "original_price_incl_tax": null, + "original_tax": null, "prices": Array [ Object { "amount": 100, @@ -451,6 +480,7 @@ Array [ ], "product_id": StringMatching /\\^test-\\*/, "sku": "test-sku3", + "tax_rates": null, "title": "Test variant rank (2)", "upc": "test-upc3", "updated_at": Any, @@ -609,6 +639,8 @@ Array [ "allow_backorder": false, "barcode": null, "calculated_price": null, + "calculated_price_incl_tax": null, + "calculated_tax": null, "created_at": Any, "deleted_at": null, "ean": null, @@ -635,6 +667,8 @@ Array [ ], "origin_country": null, "original_price": null, + "original_price_incl_tax": null, + "original_tax": null, "prices": Array [ Object { "amount": 100, @@ -653,6 +687,7 @@ Array [ ], "product_id": StringMatching /\\^prod_\\*/, "sku": null, + "tax_rates": null, "title": "Test variant", "upc": null, "updated_at": Any, @@ -769,6 +804,8 @@ Object { "allow_backorder": false, "barcode": null, "calculated_price": null, + "calculated_price_incl_tax": null, + "calculated_tax": null, "created_at": Any, "deleted_at": null, "ean": null, @@ -805,6 +842,8 @@ Object { ], "origin_country": null, "original_price": null, + "original_price_incl_tax": null, + "original_tax": null, "prices": Array [ Object { "amount": 100, @@ -851,6 +890,7 @@ Object { ], "product_id": StringMatching /\\^prod_\\*/, "sku": null, + "tax_rates": null, "title": "Test variant", "upc": null, "updated_at": Any, @@ -933,6 +973,8 @@ Object { "allow_backorder": false, "barcode": "test-barcode", "calculated_price": null, + "calculated_price_incl_tax": null, + "calculated_tax": null, "created_at": Any, "deleted_at": null, "ean": "test-ean", @@ -959,6 +1001,8 @@ Object { ], "origin_country": null, "original_price": null, + "original_price_incl_tax": null, + "original_tax": null, "prices": Array [ Object { "amount": 75, @@ -977,6 +1021,7 @@ Object { ], "product_id": "test-product", "sku": "test-sku", + "tax_rates": null, "title": "Test variant", "upc": "test-upc", "updated_at": Any, diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index 3d76453df1..c9951c5f5c 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -22,7 +22,7 @@ describe("/admin/products", () => { 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 () => { diff --git a/integration-tests/api/__tests__/store/__snapshots__/product-variants.js.snap b/integration-tests/api/__tests__/store/__snapshots__/product-variants.js.snap index 24ae2723b4..7086baa4dc 100644 --- a/integration-tests/api/__tests__/store/__snapshots__/product-variants.js.snap +++ b/integration-tests/api/__tests__/store/__snapshots__/product-variants.js.snap @@ -6,6 +6,8 @@ Object { "allow_backorder": false, "barcode": "test-barcode", "calculated_price": null, + "calculated_price_incl_tax": null, + "calculated_tax": null, "created_at": Any, "deleted_at": null, "ean": "test-ean", @@ -32,6 +34,8 @@ Object { ], "origin_country": null, "original_price": null, + "original_price_incl_tax": null, + "original_tax": null, "prices": Array [ Object { "amount": 100, @@ -51,6 +55,7 @@ Object { "product": Any, "product_id": "test-product", "sku": "test-sku", + "tax_rates": null, "title": "Test variant", "upc": "test-upc", "updated_at": Any, @@ -66,7 +71,9 @@ Object { "allow_backorder": false, "barcode": "test-barcode", "calculated_price": 80, + "calculated_price_incl_tax": null, "calculated_price_type": "sale", + "calculated_tax": null, "created_at": Any, "deleted_at": null, "ean": "test-ean", @@ -93,6 +100,8 @@ Object { ], "origin_country": null, "original_price": 100, + "original_price_incl_tax": null, + "original_tax": null, "prices": Array [ Object { "amount": 100, @@ -137,6 +146,7 @@ Object { "product": Any, "product_id": "test-product", "sku": "test-sku", + "tax_rates": null, "title": "Test variant", "upc": "test-upc", "updated_at": Any, @@ -153,6 +163,8 @@ Object { "allow_backorder": false, "barcode": null, "calculated_price": null, + "calculated_price_incl_tax": null, + "calculated_tax": null, "created_at": Any, "deleted_at": null, "ean": null, @@ -179,6 +191,8 @@ Object { ], "origin_country": null, "original_price": null, + "original_price_incl_tax": null, + "original_tax": null, "prices": Array [ Object { "amount": 100, @@ -198,6 +212,7 @@ Object { "product": Any, "product_id": Any, "sku": null, + "tax_rates": null, "title": "test2", "upc": null, "updated_at": Any, @@ -214,7 +229,9 @@ Object { "allow_backorder": false, "barcode": "test-barcode", "calculated_price": 80, + "calculated_price_incl_tax": 80, "calculated_price_type": "sale", + "calculated_tax": 0, "created_at": Any, "deleted_at": null, "ean": "test-ean", @@ -241,6 +258,8 @@ Object { ], "origin_country": null, "original_price": 100, + "original_price_incl_tax": 100, + "original_tax": 0, "prices": Array [ Object { "amount": 100, @@ -285,6 +304,13 @@ Object { "product": Any, "product_id": "test-product", "sku": "test-sku", + "tax_rates": Array [ + Object { + "code": "default", + "name": "default", + "rate": 0, + }, + ], "title": "Test variant", "upc": "test-upc", "updated_at": Any, @@ -301,6 +327,8 @@ Object { "allow_backorder": false, "barcode": "test-barcode", "calculated_price": null, + "calculated_price_incl_tax": null, + "calculated_tax": null, "created_at": Any, "deleted_at": null, "ean": "test-ean", @@ -327,6 +355,8 @@ Object { ], "origin_country": null, "original_price": null, + "original_price_incl_tax": null, + "original_tax": null, "prices": Array [ Object { "amount": 100, @@ -346,6 +376,7 @@ Object { "product": Any, "product_id": "test-product", "sku": "test-sku", + "tax_rates": null, "title": "Test variant", "upc": "test-upc", "updated_at": Any, diff --git a/integration-tests/api/__tests__/store/__snapshots__/products.js.snap b/integration-tests/api/__tests__/store/__snapshots__/products.js.snap index d998ac8523..d5177b53c5 100644 --- a/integration-tests/api/__tests__/store/__snapshots__/products.js.snap +++ b/integration-tests/api/__tests__/store/__snapshots__/products.js.snap @@ -131,6 +131,8 @@ Object { "allow_backorder": false, "barcode": "test-barcode", "calculated_price": null, + "calculated_price_incl_tax": null, + "calculated_tax": null, "created_at": Any, "deleted_at": null, "ean": "test-ean", @@ -157,6 +159,8 @@ Object { ], "origin_country": null, "original_price": null, + "original_price_incl_tax": null, + "original_tax": null, "prices": Array [ Object { "amount": 100, @@ -200,6 +204,7 @@ Object { ], "product_id": "test-product", "sku": "test-sku", + "tax_rates": null, "title": "Test variant", "upc": "test-upc", "updated_at": Any, @@ -210,6 +215,8 @@ Object { "allow_backorder": false, "barcode": null, "calculated_price": null, + "calculated_price_incl_tax": null, + "calculated_tax": null, "created_at": Any, "deleted_at": null, "ean": "test-ean2", @@ -236,6 +243,8 @@ Object { ], "origin_country": null, "original_price": null, + "original_price_incl_tax": null, + "original_tax": null, "prices": Array [ Object { "amount": 100, @@ -279,6 +288,7 @@ Object { ], "product_id": "test-product", "sku": "test-sku2", + "tax_rates": null, "title": "Test variant rank (2)", "upc": "test-upc2", "updated_at": Any, @@ -289,6 +299,8 @@ Object { "allow_backorder": false, "barcode": "test-barcode 1", "calculated_price": null, + "calculated_price_incl_tax": null, + "calculated_tax": null, "created_at": Any, "deleted_at": null, "ean": "test-ean1", @@ -315,6 +327,8 @@ Object { ], "origin_country": null, "original_price": null, + "original_price_incl_tax": null, + "original_tax": null, "prices": Array [ Object { "amount": 100, @@ -358,6 +372,7 @@ Object { ], "product_id": "test-product", "sku": "test-sku1", + "tax_rates": null, "title": "Test variant rank (1)", "upc": "test-upc1", "updated_at": Any, @@ -385,6 +400,8 @@ Object { "allow_backorder": false, "barcode": null, "calculated_price": null, + "calculated_price_incl_tax": null, + "calculated_tax": null, "created_at": Any, "deleted_at": null, "ean": null, @@ -399,6 +416,8 @@ Object { "mid_code": null, "origin_country": null, "original_price": null, + "original_price_incl_tax": null, + "original_tax": null, "prices": Array [ Object { "amount": 100, @@ -417,6 +436,7 @@ Object { ], "product_id": Any, "sku": null, + "tax_rates": null, "title": "test-variant", "upc": null, "updated_at": Any, diff --git a/integration-tests/api/__tests__/store/product-variants.js b/integration-tests/api/__tests__/store/product-variants.js index b11bcfbfbc..a97508f0b0 100644 --- a/integration-tests/api/__tests__/store/product-variants.js +++ b/integration-tests/api/__tests__/store/product-variants.js @@ -15,7 +15,7 @@ describe("/store/variants", () => { 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 () => { diff --git a/packages/medusa/src/api/routes/admin/products/add-option.ts b/packages/medusa/src/api/routes/admin/products/add-option.ts index c91ebc8a91..445dd35292 100644 --- a/packages/medusa/src/api/routes/admin/products/add-option.ts +++ b/packages/medusa/src/api/routes/admin/products/add-option.ts @@ -1,6 +1,6 @@ import { IsString } from "class-validator" import { defaultAdminProductFields, defaultAdminProductRelations } from "." -import { ProductService } from "../../../../services" +import { ProductService, PricingService } from "../../../../services" import { validator } from "../../../../utils/validator" /** @@ -42,13 +42,16 @@ export default async (req, res) => { ) const productService: ProductService = req.scope.resolve("productService") + const pricingService: PricingService = req.scope.resolve("pricingService") await productService.addOption(id, validated.title) - const product = await productService.retrieve(id, { + const rawProduct = await productService.retrieve(id, { select: defaultAdminProductFields, relations: defaultAdminProductRelations, }) + const [product] = await pricingService.setProductPrices([rawProduct]) + res.json({ product }) } diff --git a/packages/medusa/src/api/routes/admin/products/create-product.ts b/packages/medusa/src/api/routes/admin/products/create-product.ts index ef82b7a49d..830a6561c9 100644 --- a/packages/medusa/src/api/routes/admin/products/create-product.ts +++ b/packages/medusa/src/api/routes/admin/products/create-product.ts @@ -13,6 +13,7 @@ import { EntityManager } from "typeorm" import { defaultAdminProductFields, defaultAdminProductRelations } from "." import { ProductService, + PricingService, ProductVariantService, ShippingProfileService, } from "../../../../services" @@ -211,6 +212,7 @@ export default async (req, res) => { const validated = await validator(AdminPostProductsReq, req.body) const productService: ProductService = req.scope.resolve("productService") + const pricingService: PricingService = req.scope.resolve("pricingService") const productVariantService: ProductVariantService = req.scope.resolve( "productVariantService" ) @@ -270,11 +272,13 @@ export default async (req, res) => { } }) - const product = await productService.retrieve(newProduct.id, { + const rawProduct = await productService.retrieve(newProduct.id, { select: defaultAdminProductFields, relations: defaultAdminProductRelations, }) + const [product] = await pricingService.setProductPrices([rawProduct]) + res.json({ product }) } diff --git a/packages/medusa/src/api/routes/admin/products/delete-variant.ts b/packages/medusa/src/api/routes/admin/products/delete-variant.ts index 17740cc435..3a28989f4f 100644 --- a/packages/medusa/src/api/routes/admin/products/delete-variant.ts +++ b/packages/medusa/src/api/routes/admin/products/delete-variant.ts @@ -1,5 +1,9 @@ import { defaultAdminProductFields, defaultAdminProductRelations } from "." -import { ProductService, ProductVariantService } from "../../../../services" +import { + ProductService, + PricingService, + ProductVariantService, +} from "../../../../services" /** * @oas [delete] /products/{id}/variants/{variant_id} @@ -35,6 +39,7 @@ export default async (req, res) => { "productVariantService" ) const productService: ProductService = req.scope.resolve("productService") + const pricingService: PricingService = req.scope.resolve("pricingService") await productVariantService.delete(variant_id) @@ -42,11 +47,12 @@ export default async (req, res) => { select: defaultAdminProductFields, relations: defaultAdminProductRelations, }) + const [product] = await pricingService.setProductPrices([data]) res.json({ variant_id, object: "product-variant", deleted: true, - product: data, + product, }) } diff --git a/packages/medusa/src/api/routes/admin/products/get-product.ts b/packages/medusa/src/api/routes/admin/products/get-product.ts index 60d3145ac0..3935385888 100644 --- a/packages/medusa/src/api/routes/admin/products/get-product.ts +++ b/packages/medusa/src/api/routes/admin/products/get-product.ts @@ -1,5 +1,5 @@ import { defaultAdminProductFields, defaultAdminProductRelations } from "." -import { ProductService } from "../../../../services" +import { ProductService, PricingService } from "../../../../services" /** * @oas [get] /products/{id} @@ -25,11 +25,14 @@ export default async (req, res) => { const { id } = req.params const productService: ProductService = req.scope.resolve("productService") + const pricingService: PricingService = req.scope.resolve("pricingService") - const product = await productService.retrieve(id, { + const rawProduct = await productService.retrieve(id, { select: defaultAdminProductFields, relations: defaultAdminProductRelations, }) + const [product] = await pricingService.setProductPrices([rawProduct]) + res.json({ product }) } diff --git a/packages/medusa/src/api/routes/admin/products/index.ts b/packages/medusa/src/api/routes/admin/products/index.ts index c4f756856e..397bf78550 100644 --- a/packages/medusa/src/api/routes/admin/products/index.ts +++ b/packages/medusa/src/api/routes/admin/products/index.ts @@ -1,5 +1,6 @@ import { Router } from "express" import "reflect-metadata" +import { PricedProduct } from "../../../../types/pricing" import { Product, ProductTag, ProductType } from "../../../.." import { PaginatedResponse } from "../../../../types/common" import middlewares from "../../../middlewares" @@ -102,10 +103,7 @@ export const defaultAdminProductFields = [ "metadata", ] -export const defaultAdminGetProductsVariantsFields = [ - "id", - "product_id" -] +export const defaultAdminGetProductsVariantsFields = ["id", "product_id"] export const allowedAdminProductFields = [ "id", @@ -166,7 +164,7 @@ export type AdminProductsDeleteRes = { } export type AdminProductsListRes = PaginatedResponse & { - products: Product[] + products: (PricedProduct | Product)[] } export type AdminProductsListTypesRes = { diff --git a/packages/medusa/src/api/routes/admin/products/update-product.ts b/packages/medusa/src/api/routes/admin/products/update-product.ts index d80e986e74..66306a5ce1 100644 --- a/packages/medusa/src/api/routes/admin/products/update-product.ts +++ b/packages/medusa/src/api/routes/admin/products/update-product.ts @@ -17,7 +17,7 @@ import { defaultAdminProductRelations, ProductStatus, } from "." -import { ProductService } from "../../../../services" +import { ProductService, PricingService } from "../../../../services" import { ProductVariantPricesUpdateReq } from "../../../../types/product-variant" import { validator } from "../../../../utils/validator" @@ -210,13 +210,15 @@ export default async (req, res) => { const validated = await validator(AdminPostProductsProductReq, req.body) const productService: ProductService = req.scope.resolve("productService") + const pricingService: PricingService = req.scope.resolve("pricingService") await productService.update(id, validated) - const product = await productService.retrieve(id, { + const rawProduct = await productService.retrieve(id, { select: defaultAdminProductFields, relations: defaultAdminProductRelations, }) + const [product] = await pricingService.setProductPrices([rawProduct]) res.json({ product }) } diff --git a/packages/medusa/src/api/routes/admin/products/update-variant.ts b/packages/medusa/src/api/routes/admin/products/update-variant.ts index 64489cfbc1..894a6db7fa 100644 --- a/packages/medusa/src/api/routes/admin/products/update-variant.ts +++ b/packages/medusa/src/api/routes/admin/products/update-variant.ts @@ -9,7 +9,11 @@ import { ValidateNested, } from "class-validator" import { defaultAdminProductFields, defaultAdminProductRelations } from "." -import { ProductService, ProductVariantService } from "../../../../services" +import { + ProductService, + PricingService, + ProductVariantService, +} from "../../../../services" import { PriceSelectionParams } from "../../../../types/price-selection" import { ProductVariantPricesUpdateReq } from "../../../../types/product-variant" import { validator } from "../../../../utils/validator" @@ -134,6 +138,7 @@ export default async (req, res) => { const validatedQueryParams = await validator(PriceSelectionParams, req.query) const productService: ProductService = req.scope.resolve("productService") + const pricingService: PricingService = req.scope.resolve("pricingService") const productVariantService: ProductVariantService = req.scope.resolve( "productVariantService" ) @@ -143,14 +148,14 @@ export default async (req, res) => { ...validated, }) - await productVariantService.retrieve(variant_id) - - const product = await productService.retrieve(id, { + const rawProduct = await productService.retrieve(id, { select: defaultAdminProductFields, relations: defaultAdminProductRelations, ...validatedQueryParams, }) + const [product] = await pricingService.setProductPrices([rawProduct]) + res.json({ product }) } diff --git a/packages/medusa/src/api/routes/store/products/__tests__/get-product.js b/packages/medusa/src/api/routes/store/products/__tests__/get-product.js index 62057b0812..e440993945 100644 --- a/packages/medusa/src/api/routes/store/products/__tests__/get-product.js +++ b/packages/medusa/src/api/routes/store/products/__tests__/get-product.js @@ -23,10 +23,6 @@ describe("Get product by id", () => { IdMap.getId("product1"), { relations: defaultStoreProductsRelations, - cart_id: undefined, - currency_code: undefined, - region_id: undefined, - include_discount_prices: true, } ) }) @@ -59,10 +55,6 @@ describe("Get product by id", () => { IdMap.getId("variantsWithPrices"), { relations: defaultStoreProductsRelations, - cart_id: undefined, - currency_code: undefined, - region_id: undefined, - include_discount_prices: true, } ) }) diff --git a/packages/medusa/src/api/routes/store/products/__tests__/list-products.js b/packages/medusa/src/api/routes/store/products/__tests__/list-products.js index fa838cb708..d99efbe365 100644 --- a/packages/medusa/src/api/routes/store/products/__tests__/list-products.js +++ b/packages/medusa/src/api/routes/store/products/__tests__/list-products.js @@ -23,10 +23,6 @@ describe("GET /store/products", () => { relations: defaultStoreProductsRelations, skip: 0, take: 100, - cart_id: undefined, - currency_code: undefined, - region_id: undefined, - include_discount_prices: true, } ) }) @@ -54,10 +50,6 @@ describe("GET /store/products", () => { relations: defaultStoreProductsRelations, skip: 0, take: 100, - cart_id: undefined, - currency_code: undefined, - region_id: undefined, - include_discount_prices: true, } ) }) diff --git a/packages/medusa/src/api/routes/store/products/get-product.ts b/packages/medusa/src/api/routes/store/products/get-product.ts index 52d6180a2c..53a3a6ee20 100644 --- a/packages/medusa/src/api/routes/store/products/get-product.ts +++ b/packages/medusa/src/api/routes/store/products/get-product.ts @@ -1,5 +1,10 @@ import { defaultStoreProductsRelations } from "." -import { ProductService } from "../../../../services" +import { + ProductService, + PricingService, + CartService, + RegionService, +} from "../../../../services" import { PriceSelectionParams } from "../../../../types/price-selection" import { validator } from "../../../../utils/validator" @@ -30,12 +35,31 @@ export default async (req, res) => { const customer_id = req.user?.customer_id const productService: ProductService = req.scope.resolve("productService") - const product = await productService.retrieve(id, { + const pricingService: PricingService = req.scope.resolve("pricingService") + const cartService: CartService = req.scope.resolve("cartService") + const regionService: RegionService = req.scope.resolve("regionService") + const rawProduct = await productService.retrieve(id, { relations: defaultStoreProductsRelations, + }) + + let regionId = validated.region_id + let currencyCode = validated.currency_code + if (validated.cart_id) { + const cart = await cartService.retrieve(validated.cart_id, { + select: ["id", "region_id"], + }) + const region = await regionService.retrieve(cart.region_id, { + select: ["id", "currency_code"], + }) + regionId = region.id + currencyCode = region.currency_code + } + + const [product] = await pricingService.setProductPrices([rawProduct], { cart_id: validated.cart_id, customer_id: customer_id, - region_id: validated.region_id, - currency_code: validated.currency_code, + region_id: regionId, + currency_code: currencyCode, include_discount_prices: true, }) diff --git a/packages/medusa/src/api/routes/store/products/list-products.ts b/packages/medusa/src/api/routes/store/products/list-products.ts index 944227f163..eef9ff57e7 100644 --- a/packages/medusa/src/api/routes/store/products/list-products.ts +++ b/packages/medusa/src/api/routes/store/products/list-products.ts @@ -9,7 +9,12 @@ import { } from "class-validator" import { omit, pickBy } from "lodash" import { defaultStoreProductsRelations } from "." -import { ProductService } from "../../../../services" +import { + ProductService, + RegionService, + CartService, +} from "../../../../services" +import PricingService from "../../../../services/pricing" import { DateComparisonOperator } from "../../../../types/common" import { PriceSelectionParams } from "../../../../types/price-selection" import { validator } from "../../../../utils/validator" @@ -61,6 +66,9 @@ import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean" */ export default async (req, res) => { const productService: ProductService = req.scope.resolve("productService") + const pricingService: PricingService = req.scope.resolve("pricingService") + const cartService: CartService = req.scope.resolve("cartService") + const regionService: RegionService = req.scope.resolve("regionService") const validated = await validator(StoreGetProductsParams, req.query) @@ -96,18 +104,34 @@ export default async (req, res) => { : defaultStoreProductsRelations, skip: validated.offset, take: validated.limit, - cart_id: validated.cart_id, - region_id: validated.region_id, - currency_code: validated.currency_code, - customer_id: req.user?.customer_id, - include_discount_prices: true, } - const [products, count] = await productService.listAndCount( + const [rawProducts, count] = await productService.listAndCount( pickBy(filterableFields, (val) => typeof val !== "undefined"), listConfig ) + let regionId = validated.region_id + let currencyCode = validated.currency_code + if (validated.cart_id) { + const cart = await cartService.retrieve(validated.cart_id, { + select: ["id", "region_id"], + }) + const region = await regionService.retrieve(cart.region_id, { + select: ["id", "currency_code"], + }) + regionId = region.id + currencyCode = region.currency_code + } + + const products = await pricingService.setProductPrices(rawProducts, { + cart_id: validated.cart_id, + region_id: regionId, + currency_code: currencyCode, + customer_id: req.user?.customer_id, + include_discount_prices: true, + }) + res.json({ products, count, diff --git a/packages/medusa/src/api/routes/store/variants/__tests__/get-variant.js b/packages/medusa/src/api/routes/store/variants/__tests__/get-variant.js index 4f8a13cc2f..5090457c54 100644 --- a/packages/medusa/src/api/routes/store/variants/__tests__/get-variant.js +++ b/packages/medusa/src/api/routes/store/variants/__tests__/get-variant.js @@ -16,10 +16,6 @@ describe("Get variant by id", () => { expect(ProductVariantServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(ProductVariantServiceMock.retrieve).toHaveBeenCalledWith("1", { relations: ["prices", "options"], - cart_id: undefined, - currency_code: undefined, - region_id: undefined, - include_discount_prices: true, }) }) diff --git a/packages/medusa/src/api/routes/store/variants/get-variant.ts b/packages/medusa/src/api/routes/store/variants/get-variant.ts index 424eff61aa..2c3442dc75 100644 --- a/packages/medusa/src/api/routes/store/variants/get-variant.ts +++ b/packages/medusa/src/api/routes/store/variants/get-variant.ts @@ -1,5 +1,10 @@ import { defaultStoreVariantRelations } from "." -import ProductVariantService from "../../../../services/product-variant" +import { + CartService, + RegionService, + ProductVariantService, + PricingService, +} from "../../../../services" import { PriceSelectionParams } from "../../../../types/price-selection" import { validator } from "../../../../utils/validator" @@ -30,15 +35,34 @@ export default async (req, res) => { const variantService: ProductVariantService = req.scope.resolve( "productVariantService" ) + const pricingService: PricingService = req.scope.resolve("pricingService") + const cartService: CartService = req.scope.resolve("cartService") + const regionService: RegionService = req.scope.resolve("regionService") const customer_id = req.user?.customer_id - const variant = await variantService.retrieve(id, { + const rawVariant = await variantService.retrieve(id, { relations: defaultStoreVariantRelations, + }) + + let regionId = validated.region_id + let currencyCode = validated.currency_code + if (validated.cart_id) { + const cart = await cartService.retrieve(validated.cart_id, { + select: ["id", "region_id"], + }) + const region = await regionService.retrieve(cart.region_id, { + select: ["id", "currency_code"], + }) + regionId = region.id + currencyCode = region.currency_code + } + + const [variant] = await pricingService.setVariantPrices([rawVariant], { cart_id: validated.cart_id, customer_id: customer_id, - region_id: validated.region_id, - currency_code: validated.currency_code, + region_id: regionId, + currency_code: currencyCode, include_discount_prices: true, }) diff --git a/packages/medusa/src/api/routes/store/variants/list-variants.ts b/packages/medusa/src/api/routes/store/variants/list-variants.ts index 06f9634f2a..f57eb240b7 100644 --- a/packages/medusa/src/api/routes/store/variants/list-variants.ts +++ b/packages/medusa/src/api/routes/store/variants/list-variants.ts @@ -3,7 +3,12 @@ import { omit } from "lodash" import { IsInt, IsOptional, IsString } from "class-validator" import { defaultStoreVariantRelations } from "." import { FilterableProductVariantProps } from "../../../../types/product-variant" -import ProductVariantService from "../../../../services/product-variant" +import { + CartService, + RegionService, + ProductVariantService, + PricingService, +} from "../../../../services" import { validator } from "../../../../utils/validator" import { IsType } from "../../../../utils/validators/is-type" import { NumericalComparisonOperator } from "../../../../types/common" @@ -50,11 +55,6 @@ export default async (req, res) => { : defaultStoreVariantRelations, skip: offset, take: limit, - cart_id: validated.cart_id, - region_id: validated.region_id, - currency_code: validated.currency_code, - customer_id: customer_id, - include_discount_prices: true, } const filterableFields: FilterableProductVariantProps = omit(validated, [ @@ -71,10 +71,35 @@ export default async (req, res) => { filterableFields.id = validated.ids.split(",") } + const pricingService: PricingService = req.scope.resolve("pricingService") const variantService: ProductVariantService = req.scope.resolve( "productVariantService" ) - const variants = await variantService.list(filterableFields, listConfig) + const cartService: CartService = req.scope.resolve("cartService") + const regionService: RegionService = req.scope.resolve("regionService") + + const rawVariants = await variantService.list(filterableFields, listConfig) + + let regionId = validated.region_id + let currencyCode = validated.currency_code + if (validated.cart_id) { + const cart = await cartService.retrieve(validated.cart_id, { + select: ["id", "region_id"], + }) + const region = await regionService.retrieve(cart.region_id, { + select: ["id", "currency_code"], + }) + regionId = region.id + currencyCode = region.currency_code + } + + const variants = await pricingService.setVariantPrices(rawVariants, { + cart_id: validated.cart_id, + region_id: regionId, + currency_code: currencyCode, + customer_id: customer_id, + include_discount_prices: true, + }) res.json({ variants }) } diff --git a/packages/medusa/src/controllers/products/admin-list-products.ts b/packages/medusa/src/controllers/products/admin-list-products.ts index 19011efd11..334ef46f98 100644 --- a/packages/medusa/src/controllers/products/admin-list-products.ts +++ b/packages/medusa/src/controllers/products/admin-list-products.ts @@ -3,7 +3,7 @@ import { AdminProductsListRes } from "../../api" import { pickBy } from "lodash" import { MedusaError } from "medusa-core-utils" import { Product } from "../../models/product" -import { ProductService } from "../../services" +import { ProductService, PricingService } from "../../services" import { getListConfig } from "../../utils/get-query-config" import { FilterableProductProps } from "../../types/product" @@ -24,10 +24,11 @@ const listAndCount = async ( body?: object, context: ListContext = { limit: 50, offset: 0 } ): Promise => { - const productService: ProductService = scope.resolve("productService") const { limit, offset, allowedFields, defaultFields, defaultRelations } = context + const productService: ProductService = scope.resolve("productService") + const pricingService: PricingService = scope.resolve("pricingService") let includeFields: (keyof Product)[] | undefined if (context.fields) { includeFields = context.fields.split(",") as (keyof Product)[] @@ -67,11 +68,20 @@ const listAndCount = async ( orderBy ) - const [products, count] = await productService.listAndCount( + const [rawProducts, count] = await productService.listAndCount( pickBy(query, (val) => typeof val !== "undefined"), listConfig ) + let products = rawProducts + + const includesPricing = ["variants", "variants.prices"].every((relation) => + listConfig?.relations?.includes(relation) + ) + if (includesPricing) { + products = await pricingService.setProductPrices(rawProducts) + } + return { products, count, diff --git a/packages/medusa/src/repositories/product-variant.ts b/packages/medusa/src/repositories/product-variant.ts index 117881bd1c..c1109b4109 100644 --- a/packages/medusa/src/repositories/product-variant.ts +++ b/packages/medusa/src/repositories/product-variant.ts @@ -82,8 +82,7 @@ export class ProductVariantRepository extends Repository { ): Promise { const entitiesIdsWithRelations = await Promise.all( Object.entries(groupedRelations).map(([toplevel, rels]) => { - let querybuilder = this.createQueryBuilder("pv") - querybuilder = querybuilder.leftJoinAndSelect( + let querybuilder = this.createQueryBuilder("pv").leftJoinAndSelect( `pv.${toplevel}`, toplevel ) @@ -94,20 +93,20 @@ export class ProductVariantRepository extends Repository { continue } // Regex matches all '.' except the rightmost - querybuilder = querybuilder.leftJoinAndSelect( + querybuilder.leftJoinAndSelect( rel.replace(/\.(?=[^.]*\.)/g, "__"), rel.replace(".", "__") ) } if (withDeleted) { - querybuilder = querybuilder + querybuilder .where("pv.id IN (:...entitiesIds)", { entitiesIds: entityIds, }) .withDeleted() } else { - querybuilder = querybuilder.where( + querybuilder.where( "pv.deleted_at IS NULL AND pv.id IN (:...entitiesIds)", { entitiesIds: entityIds, diff --git a/packages/medusa/src/services/__mocks__/pricing.js b/packages/medusa/src/services/__mocks__/pricing.js new file mode 100644 index 0000000000..7730379e57 --- /dev/null +++ b/packages/medusa/src/services/__mocks__/pricing.js @@ -0,0 +1,17 @@ +export const PricingServiceMock = { + withTransaction: function () { + return this + }, + setProductPrices: jest.fn().mockImplementation((prod) => { + return Promise.resolve(prod) + }), + setVariantPrices: jest.fn().mockImplementation((variant) => { + return Promise.resolve(variant) + }), +} + +const mock = jest.fn().mockImplementation(() => { + return PricingServiceMock +}) + +export default mock diff --git a/packages/medusa/src/services/__tests__/line-item.js b/packages/medusa/src/services/__tests__/line-item.js index d36bd8363a..e9cd5d6b9a 100644 --- a/packages/medusa/src/services/__tests__/line-item.js +++ b/packages/medusa/src/services/__tests__/line-item.js @@ -4,7 +4,7 @@ import LineItemService from "../line-item" describe("LineItemService", () => { describe("create", () => { const lineItemRepository = MockRepository({ - create: (data) => data + create: (data) => data, }) const cartRepository = MockRepository({ @@ -15,7 +15,7 @@ describe("LineItemService", () => { }) const regionService = { - withTransaction: function() { + withTransaction: function () { return this }, retrieve: () => { @@ -26,10 +26,10 @@ describe("LineItemService", () => { } const productVariantService = { - withTransaction: function() { + withTransaction: function () { return this }, - retrieve: query => { + retrieve: (query) => { if (query === IdMap.getId("test-giftcard")) { return { id: IdMap.getId("test-giftcard"), @@ -54,8 +54,25 @@ describe("LineItemService", () => { getRegionPrice: () => 100, } + const pricingService = { + withTransaction: function () { + return this + }, + getProductVariantPricingById: () => { + return { + calculated_price: 100, + } + }, + getProductVariantPricing: () => { + return { + calculated_price: 100, + } + }, + } + const lineItemService = new LineItemService({ manager: MockManager, + pricingService, lineItemRepository, productVariantService, regionService, @@ -119,19 +136,22 @@ describe("LineItemService", () => { }) expect(lineItemRepository.create).toHaveBeenCalledTimes(2) - expect(lineItemRepository.create).toHaveBeenNthCalledWith(2, expect.objectContaining({ - allow_discounts: false, - variant_id: IdMap.getId("test-giftcard"), - cart_id: IdMap.getId("test-cart"), - title: "Test product", - description: "Test variant", - thumbnail: "", - unit_price: 100, - quantity: 1, - is_giftcard: true, - should_merge: true, - metadata: {}, - })) + expect(lineItemRepository.create).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + allow_discounts: false, + variant_id: IdMap.getId("test-giftcard"), + cart_id: IdMap.getId("test-cart"), + title: "Test product", + description: "Test variant", + thumbnail: "", + unit_price: 100, + quantity: 1, + is_giftcard: true, + should_merge: true, + metadata: {}, + }) + ) }) }) diff --git a/packages/medusa/src/services/index.ts b/packages/medusa/src/services/index.ts index 1567701c18..7f5e36a001 100644 --- a/packages/medusa/src/services/index.ts +++ b/packages/medusa/src/services/index.ts @@ -39,3 +39,4 @@ export { default as UserService } from "./user" export { default as TaxRateService } from "./tax-rate" export { default as TaxProviderService } from "./tax-provider" export { default as ProductTypeService } from "./product-type" +export { default as PricingService } from "./pricing" diff --git a/packages/medusa/src/services/line-item.ts b/packages/medusa/src/services/line-item.ts index 55e55a802c..f11e1ca0b1 100644 --- a/packages/medusa/src/services/line-item.ts +++ b/packages/medusa/src/services/line-item.ts @@ -3,7 +3,12 @@ import { BaseService } from "medusa-interfaces" import { EntityManager } from "typeorm" import { LineItemRepository } from "../repositories/line-item" import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line" -import { ProductService, RegionService, ProductVariantService } from "./index" +import { + PricingService, + ProductService, + RegionService, + ProductVariantService, +} from "./index" import { CartRepository } from "../repositories/cart" import { LineItem } from "../models/line-item" import LineItemAdjustmentService from "./line-item-adjustment" @@ -17,6 +22,7 @@ type InjectedDependencies = { cartRepository: typeof CartRepository productVariantService: ProductVariantService productService: ProductService + pricingService: PricingService regionService: RegionService lineItemAdjustmentService: LineItemAdjustmentService } @@ -41,6 +47,7 @@ class LineItemService extends BaseService { lineItemTaxLineRepository, productVariantService, productService, + pricingService, regionService, cartRepository, lineItemAdjustmentService, @@ -52,6 +59,7 @@ class LineItemService extends BaseService { this.itemTaxLineRepo_ = lineItemTaxLineRepository this.productVariantService_ = productVariantService this.productService_ = productService + this.pricingService_ = pricingService this.regionService_ = regionService this.cartRepository_ = cartRepository this.lineItemAdjustmentService_ = lineItemAdjustmentService @@ -68,6 +76,7 @@ class LineItemService extends BaseService { lineItemTaxLineRepository: this.itemTaxLineRepo_, productVariantService: this.productVariantService_, productService: this.productService_, + pricingService: this.pricingService_, regionService: this.regionService_, cartRepository: this.cartRepository_, lineItemAdjustmentService: this.lineItemAdjustmentService_, @@ -201,7 +210,6 @@ class LineItemService extends BaseService { .withTransaction(transactionManager) .retrieve(variantId, { relations: ["product"], - include_discount_prices: true, }), this.regionService_ .withTransaction(transactionManager) @@ -213,18 +221,19 @@ class LineItemService extends BaseService { if (context.unit_price === undefined || context.unit_price === null) { shouldMerge = true - unit_price = await this.productVariantService_ + const variantPricing = await this.pricingService_ .withTransaction(transactionManager) - .getRegionPrice(variant.id, { - regionId: region.id, + .getProductVariantPricingById(variant.id, { + region_id: region.id, quantity: quantity, customer_id: context?.customer_id, include_discount_prices: true, }) + unit_price = variantPricing.calculated_price } const rawLineItem: Partial = { - unit_price: unit_price as number, + unit_price: unit_price, title: variant.product.title, description: variant.title, thumbnail: variant.product.thumbnail, diff --git a/packages/medusa/src/services/pricing.ts b/packages/medusa/src/services/pricing.ts new file mode 100644 index 0000000000..5754a658ed --- /dev/null +++ b/packages/medusa/src/services/pricing.ts @@ -0,0 +1,389 @@ +import { EntityManager } from "typeorm" +import { ProductVariantService, RegionService, TaxProviderService } from "." +import { Product, ProductVariant } from "../models" +import { TaxServiceRate } from "../types/tax-service" +import { + ProductVariantPricing, + TaxedPricing, + PricingContext, + PricedProduct, + PricedVariant, +} from "../types/pricing" +import { TransactionBaseService } from "../interfaces" +import { + IPriceSelectionStrategy, + PriceSelectionContext, +} from "../interfaces/price-selection-strategy" + +type InjectedDependencies = { + manager: EntityManager + productVariantService: ProductVariantService + taxProviderService: TaxProviderService + regionService: RegionService + priceSelectionStrategy: IPriceSelectionStrategy +} + +/** + * Allows retrieval of prices. + * @extends BaseService + */ +class PricingService extends TransactionBaseService { + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + protected readonly regionService: RegionService + protected readonly taxProviderService: TaxProviderService + protected readonly priceSelectionStrategy: IPriceSelectionStrategy + protected readonly productVariantService: ProductVariantService + + constructor({ + manager, + productVariantService, + taxProviderService, + regionService, + priceSelectionStrategy, + }: InjectedDependencies) { + // eslint-disable-next-line prefer-rest-params + super(arguments[0]) + + this.manager_ = manager + this.regionService = regionService + this.taxProviderService = taxProviderService + this.priceSelectionStrategy = priceSelectionStrategy + this.productVariantService = productVariantService + } + + /** + * Collects additional information neccessary for completing the price + * selection. + * @param context - the price selection context to use + * @return The pricing context + */ + async collectPricingContext( + context: PriceSelectionContext + ): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + let automaticTaxes = false + let taxRate = null + let currencyCode = context.currency_code + + if (context.region_id) { + const region = await this.regionService + .withTransaction(transactionManager) + .retrieve(context.region_id, { + select: ["id", "currency_code", "automatic_taxes", "tax_rate"], + }) + + currencyCode = region.currency_code + automaticTaxes = region.automatic_taxes + taxRate = region.tax_rate + } + + return { + price_selection: { + ...context, + currency_code: currencyCode, + }, + automatic_taxes: automaticTaxes, + tax_rate: taxRate, + } + } + ) + } + + /** + * Gets the prices for a product variant + * @param variantPricing - the prices retrieved from a variant + * @param productRates - the tax rates that the product has applied + * @return The tax related variant prices. + */ + async calculateTaxes( + variantPricing: ProductVariantPricing, + productRates: TaxServiceRate[] + ): Promise { + const rate = productRates.reduce( + (accRate: number, nextTaxRate: TaxServiceRate) => { + return accRate + (nextTaxRate.rate || 0) / 100 + }, + 0 + ) + + const taxedPricing: TaxedPricing = { + original_tax: null, + calculated_tax: null, + original_price_incl_tax: null, + calculated_price_incl_tax: null, + tax_rates: productRates, + } + + if (variantPricing.calculated_price !== null) { + const taxAmount = Math.round(variantPricing.calculated_price * rate) + taxedPricing.calculated_tax = taxAmount + taxedPricing.calculated_price_incl_tax = + variantPricing.calculated_price + taxAmount + } + + if (variantPricing.original_price !== null) { + const taxAmount = Math.round(variantPricing.original_price * rate) + taxedPricing.original_tax = taxAmount + taxedPricing.original_price_incl_tax = + variantPricing.original_price + taxAmount + } + + return taxedPricing + } + + private async getProductVariantPricing_( + variantId: string, + taxRates: TaxServiceRate[], + context: PricingContext + ): Promise { + const transactionManager = this.transactionManager_ ?? this.manager_ + const pricing = await this.priceSelectionStrategy + .withTransaction(transactionManager) + .calculateVariantPrice(variantId, context.price_selection) + + const pricingResult: ProductVariantPricing = { + prices: pricing.prices, + original_price: pricing.originalPrice, + calculated_price: pricing.calculatedPrice, + calculated_price_type: pricing.calculatedPriceType, + original_price_incl_tax: null, + calculated_price_incl_tax: null, + original_tax: null, + calculated_tax: null, + tax_rates: null, + } + + if (context.automatic_taxes && context.price_selection.region_id) { + const taxResults = await this.calculateTaxes(pricingResult, taxRates) + + pricingResult.original_price_incl_tax = taxResults.original_price_incl_tax + pricingResult.calculated_price_incl_tax = + taxResults.calculated_price_incl_tax + pricingResult.original_tax = taxResults.original_tax + pricingResult.calculated_tax = taxResults.calculated_tax + pricingResult.tax_rates = taxResults.tax_rates + } + + return pricingResult + } + + /** + * Gets the prices for a product variant. + * @param variant - the id of the variant to get prices for + * @param context - the price selection context to use + * @return The product variant prices + */ + async getProductVariantPricing( + variant: Pick, + context: PriceSelectionContext | PricingContext + ): Promise { + let pricingContext: PricingContext + if ("automatic_taxes" in context) { + pricingContext = context + } else { + pricingContext = await this.collectPricingContext(context) + } + + let productRates: TaxServiceRate[] = [] + if ( + pricingContext.automatic_taxes && + pricingContext.price_selection.region_id + ) { + productRates = await this.taxProviderService.getRegionRatesForProduct( + variant.product_id, + { + id: pricingContext.price_selection.region_id, + tax_rate: pricingContext.tax_rate, + } + ) + } + + return await this.getProductVariantPricing_( + variant.id, + productRates, + pricingContext + ) + } + + /** + * Gets the prices for a product variant by a variant id. + * @param variantId - the id of the variant to get prices for + * @param context - the price selection context to use + * @return The product variant prices + */ + async getProductVariantPricingById( + variantId: string, + context: PriceSelectionContext | PricingContext + ): Promise { + let pricingContext: PricingContext + if ("automatic_taxes" in context) { + pricingContext = context + } else { + pricingContext = await this.collectPricingContext(context) + } + + let productRates: TaxServiceRate[] = [] + if ( + pricingContext.automatic_taxes && + pricingContext.price_selection.region_id + ) { + const { product_id } = await this.productVariantService.retrieve( + variantId, + { select: ["id", "product_id"] } + ) + productRates = await this.taxProviderService.getRegionRatesForProduct( + product_id, + { + id: pricingContext.price_selection.region_id, + tax_rate: pricingContext.tax_rate, + } + ) + } + + return await this.getProductVariantPricing_( + variantId, + productRates, + pricingContext + ) + } + + private async getProductPricing_( + productId: string, + variants: ProductVariant[], + context: PricingContext + ): Promise> { + const transactionManager = this.transactionManager_ ?? this.manager_ + let taxRates: TaxServiceRate[] = [] + if (context.automatic_taxes && context.price_selection.region_id) { + taxRates = await this.taxProviderService + .withTransaction(transactionManager) + .getRegionRatesForProduct(productId, { + id: context.price_selection.region_id, + tax_rate: context.tax_rate, + }) + } + + const pricings = {} + await Promise.all( + variants.map(async ({ id }) => { + const variantPricing = await this.getProductVariantPricing_( + id, + taxRates, + context + ) + pricings[id] = variantPricing + }) + ) + + return pricings + } + + /** + * Gets all the variant prices for a product. All the product's variants will + * be fetched. + * @param product - the product to get pricing for. + * @param context - the price selection context to use + * @return A map of variant ids to their corresponding prices + */ + async getProductPricing( + product: Pick, + context: PriceSelectionContext + ): Promise> { + const pricingContext = await this.collectPricingContext(context) + return await this.getProductPricing_( + product.id, + product.variants, + pricingContext + ) + } + + /** + * Gets all the variant prices for a product by the product id + * @param productId - the id of the product to get prices for + * @param context - the price selection context to use + * @return A map of variant ids to their corresponding prices + */ + async getProductPricingById( + productId: string, + context: PriceSelectionContext + ): Promise> { + const pricingContext = await this.collectPricingContext(context) + const variants = await this.productVariantService.list( + { product_id: productId }, + { select: ["id"] } + ) + return await this.getProductPricing_(productId, variants, pricingContext) + } + + /** + * Set additional prices on a list of product variants. + * @param variants - list of variants on which to set additional prices + * @param context - the price selection context to use + * @return A list of products with variants decorated with prices + */ + async setVariantPrices( + variants: ProductVariant[], + context: PriceSelectionContext + ): Promise { + const pricingContext = await this.collectPricingContext(context) + return await Promise.all( + variants.map(async (variant) => { + const variantPricing = await this.getProductVariantPricing( + variant, + pricingContext + ) + return { + ...variant, + ...variantPricing, + } + }) + ) + } + + /** + * Set additional prices on a list of products. + * @param products - list of products on which to set additional prices + * @param context - the price selection context to use + * @return A list of products with variants decorated with prices + */ + async setProductPrices( + products: Product[], + context: PriceSelectionContext = {} + ): Promise<(Product | PricedProduct)[]> { + const pricingContext = await this.collectPricingContext(context) + return await Promise.all( + products.map(async (product) => { + if (!product?.variants?.length) { + return product + } + + const variantPricing = await this.getProductPricing_( + product.id, + product.variants, + pricingContext + ) + + const pricedVariants = product.variants.map( + (productVariant): PricedVariant => { + const pricing = variantPricing[productVariant.id] + return { + ...productVariant, + ...pricing, + } + } + ) + + const pricedProduct = { + ...product, + variants: pricedVariants, + } + + return pricedProduct + }) + ) + } +} + +export default PricingService diff --git a/packages/medusa/src/services/product-variant.ts b/packages/medusa/src/services/product-variant.ts index db26f22a07..9715c15c45 100644 --- a/packages/medusa/src/services/product-variant.ts +++ b/packages/medusa/src/services/product-variant.ts @@ -125,12 +125,6 @@ class ProductVariantService extends BaseService { ) const validatedId = this.validateId_(variantId) - const priceIndex = config.relations?.indexOf("prices") ?? -1 - if (priceIndex >= 0 && config.relations) { - config.relations = [...config.relations] - config.relations.splice(priceIndex, 1) - } - const query = this.buildQuery_({ id: validatedId }, config) const variant = await variantRepo.findOne(query) @@ -141,16 +135,7 @@ class ProductVariantService extends BaseService { ) } - return priceIndex >= 0 - ? ((await this.setAdditionalPrices( - variant, - config.currency_code, - config.region_id, - config.cart_id, - config.customer_id, - config.include_discount_prices - )) as ProductVariant) - : variant + return variant } /** @@ -185,16 +170,7 @@ class ProductVariantService extends BaseService { ) } - return priceIndex >= 0 - ? ((await this.setAdditionalPrices( - variant, - config.currency_code, - config.region_id, - config.cart_id, - config.customer_id, - config.include_discount_prices - )) as ProductVariant) - : variant + return variant } /** @@ -617,12 +593,6 @@ class ProductVariantService extends BaseService { this.productVariantRepository_ ) - const priceIndex = config.relations?.indexOf("prices") ?? -1 - if (priceIndex >= 0 && config.relations) { - config.relations = [...config.relations] - config.relations.splice(priceIndex, 1) - } - const { q, query, relations } = this.prepareListQuery_(selector, config) if (q) { @@ -634,17 +604,6 @@ class ProductVariantService extends BaseService { raw.map((i) => i.id), query.withDeleted ?? false ) - if (priceIndex >= 0) { - const res = await this.setAdditionalPrices( - variants, - config.currency_code, - config.region_id, - config.cart_id, - config.customer_id, - config.include_discount_prices - ) - return [res as ProductVariant[], count] - } return [variants, count] } @@ -654,18 +613,6 @@ class ProductVariantService extends BaseService { query ) - if (priceIndex >= 0) { - const res = await this.setAdditionalPrices( - variants, - config.currency_code, - config.region_id, - config.cart_id, - config.customer_id, - config.include_discount_prices - ) - return [res as ProductVariant[], count] - } - return [variants, count] } @@ -722,18 +669,7 @@ class ProductVariantService extends BaseService { } } - const variants = await productVariantRepo.find(query) - - return priceIndex >= 0 - ? ((await this.setAdditionalPrices( - variants, - config.currency_code, - config.region_id, - config.cart_id, - config.customer_id, - config.include_discount_prices - )) as ProductVariant[]) - : variants + return await productVariantRepo.find(query) } /** @@ -883,72 +819,6 @@ class ProductVariantService extends BaseService { return qb } - - /** - * Set additional prices on a list of variants. - * @param {ProductVariant | ProductVariant[] } variant variant on which to set additional prices - * @param {string} currency_code currency code to fetch prices for - * @param {string} region_id region to fetch prices for - * @param {string} cart_id string of cart to use as a basis for getting currency and region - * @param {string} customer_id id of potentially logged in customer, used to get prices valid for their customer groups - * @param {boolean} include_discount_prices should result include discount pricing - * @return {Promise} A list of variants with variants decorated with "additional_prices" - */ - async setAdditionalPrices( - variant, - currency_code, - region_id, - cart_id, - customer_id, - include_discount_prices = false - ): Promise { - return this.atomicPhase_(async (manager) => { - const cartRepo = manager.getCustomRepository(this.cartRepository_) - - let regionId = region_id - let currencyCode = currency_code - - if (cart_id) { - const cart = await cartRepo.findOne({ - where: { id: cart_id }, - relations: ["region"], - }) - - regionId = cart.region.id - currencyCode = cart.region.currency_code - } - - const variantArray = Array.isArray(variant) ? variant : [variant] - - const priceSelectionStrategy = - this.priceSelectionStrategy_.withTransaction(manager) - - const variantsWithPrices = await Promise.all( - variantArray.map(async (v) => { - const prices = await priceSelectionStrategy.calculateVariantPrice( - v.id, - { - region_id: regionId, - currency_code: currencyCode, - cart_id: cart_id, - customer_id: customer_id, - include_discount_prices: include_discount_prices, - } - ) - - return { - ...v, - prices: prices.prices, - original_price: prices.originalPrice, - calculated_price: prices.calculatedPrice, - calculated_price_type: prices.calculatedPriceType, - } - }) - ) - - return Array.isArray(variant) ? variantsWithPrices : variantsWithPrices[0] - }) - } } export default ProductVariantService diff --git a/packages/medusa/src/services/product.js b/packages/medusa/src/services/product.js index 7f1fa1cc5a..39def3b4fc 100644 --- a/packages/medusa/src/services/product.js +++ b/packages/medusa/src/services/product.js @@ -22,13 +22,10 @@ class ProductService extends BaseService { productOptionRepository, eventBusService, productVariantService, - productCollectionService, productTypeRepository, productTagRepository, imageRepository, searchService, - cartRepository, - priceSelectionStrategy, }) { super() @@ -50,9 +47,6 @@ class ProductService extends BaseService { /** @private @const {ProductVariantService} */ this.productVariantService_ = productVariantService - /** @private @const {ProductCollectionService} */ - this.productCollectionService_ = productCollectionService - /** @private @const {ProductCollectionService} */ this.productTypeRepository_ = productTypeRepository @@ -64,12 +58,6 @@ class ProductService extends BaseService { /** @private @const {SearchService} */ this.searchService_ = searchService - - /** @private @const {CartRepository} */ - this.cartRepository_ = cartRepository - - /** @private @const {IPriceSelectionStrategy} */ - this.priceSelectionStrategy_ = priceSelectionStrategy } withTransaction(transactionManager) { @@ -84,12 +72,9 @@ class ProductService extends BaseService { productOptionRepository: this.productOptionRepository_, eventBusService: this.eventBus_, productVariantService: this.productVariantService_, - productCollectionService: this.productCollectionService_, productTagRepository: this.productTagRepository_, productTypeRepository: this.productTypeRepository_, imageRepository: this.imageRepository_, - cartRepository: this.cartRepository_, - priceSelectionStrategy: this.priceSelectionStrategy_, }) cloned.transactionManager_ = transactionManager @@ -117,11 +102,6 @@ class ProductService extends BaseService { const productRepo = this.manager_.getCustomRepository( this.productRepository_ ) - const priceIndex = config.relations?.indexOf("variants.prices") ?? -1 - if (priceIndex >= 0 && config.relations) { - config.relations = [...config.relations] - config.relations.splice(priceIndex, 1) - } const { q, query, relations } = this.prepareListQuery_(selector, config) @@ -135,18 +115,7 @@ class ProductService extends BaseService { return products } - const products = productRepo.findWithRelations(relations, query) - - return priceIndex > -1 - ? await this.setAdditionalPrices( - products, - config.currency_code, - config.region_id, - config.cart_id, - config.customer_id, - config.include_discount_prices - ) - : products + return await productRepo.findWithRelations(relations, query) } /** @@ -173,43 +142,17 @@ class ProductService extends BaseService { this.productRepository_ ) - const priceIndex = config.relations?.indexOf("variants.prices") ?? -1 - if (priceIndex >= 0 && config.relations) { - config.relations = [...config.relations] - config.relations.splice(priceIndex, 1) - } - const { q, query, relations } = this.prepareListQuery_(selector, config) - let products - let count if (q) { - ;[products, count] = await productRepo.getFreeTextSearchResultsAndCount( + return await productRepo.getFreeTextSearchResultsAndCount( q, query, relations ) - } else { - ;[products, count] = await productRepo.findWithRelationsAndCount( - relations, - query - ) } - if (priceIndex > -1) { - const productsWithAdditionalPrices = await this.setAdditionalPrices( - products, - config.currency_code, - config.region_id, - config.cart_id, - config.customer_id, - config.include_discount_prices - ) - - return [productsWithAdditionalPrices, count] - } else { - return [products, count] - } + return await productRepo.findWithRelationsAndCount(relations, query) } /** @@ -239,12 +182,6 @@ class ProductService extends BaseService { ) const validatedId = this.validateId_(productId) - const priceIndex = config.relations?.indexOf("variants.prices") ?? -1 - if (priceIndex >= 0 && config.relations) { - config.relations = [...config.relations] - config.relations.splice(priceIndex, 1) - } - const query = { where: { id: validatedId } } if (config.relations && config.relations.length > 0) { @@ -266,16 +203,7 @@ class ProductService extends BaseService { ) } - return priceIndex > -1 - ? await this.setAdditionalPrices( - product, - config.currency_code, - config.region_id, - config.cart_id, - config.customer_id, - config.include_discount_prices - ) - : product + return product } /** @@ -290,12 +218,6 @@ class ProductService extends BaseService { this.productRepository_ ) - const priceIndex = config.relations?.indexOf("variants.prices") ?? -1 - if (priceIndex >= 0 && config.relations) { - config.relations = [...config.relations] - config.relations.splice(priceIndex, 1) - } - const query = { where: { handle: productHandle } } if (config.relations && config.relations.length > 0) { @@ -317,16 +239,7 @@ class ProductService extends BaseService { ) } - return priceIndex > -1 - ? await this.setAdditionalPrices( - product, - config.currency_code, - config.region_id, - config.cart_id, - config.customer_id, - config.include_discount_prices - ) - : product + return product } /** @@ -341,12 +254,6 @@ class ProductService extends BaseService { this.productRepository_ ) - const priceIndex = config.relations?.indexOf("variants.prices") ?? -1 - if (priceIndex >= 0 && config.relations) { - config.relations = [...config.relations] - config.relations.splice(priceIndex, 1) - } - const query = { where: { external_id: externalId } } if (config.relations && config.relations.length > 0) { @@ -368,16 +275,7 @@ class ProductService extends BaseService { ) } - return priceIndex > -1 - ? await this.setAdditionalPrices( - product, - config.currency_code, - config.region_id, - config.cart_id, - config.customer_id, - config.include_discount_prices - ) - : product + return product } /** @@ -942,29 +840,12 @@ class ProductService extends BaseService { async decorate(productId, fields = [], expandFields = [], config = {}) { const requiredFields = ["id", "metadata"] - const priceIndex = expandFields.indexOf("variants.prices") ?? -1 - if (priceIndex >= 0 && expandFields.length) { - expandFields = [...expandFields] - expandFields.splice(priceIndex, 1) - } - fields = fields.concat(requiredFields) - const product = await this.retrieve(productId, { + return await this.retrieve(productId, { select: fields, relations: expandFields, }) - - return priceIndex > -1 - ? await this.setAdditionalPrices( - product, - config.currency_code, - config.region_id, - config.cart_id, - config.customer_id, - config.include_discount_prices - ) - : product } /** @@ -1000,80 +881,6 @@ class ProductService extends BaseService { q, } } - - /** - * Set additional prices on a list of products. - * @param {Product[] | Product} products list of products on which to set additional prices - * @param {string} currency_code currency code to fetch prices for - * @param {string} region_id region to fetch prices for - * @param {string} cart_id string of cart to use as a basis for getting currency and region - * @param {string} customer_id id of potentially logged in customer, used to get prices valid for their customer groups - * @param {boolean} include_discount_prices indication wether or not to include sales prices in result - * @return {Promise} A list of products with variants decorated with "additional_prices" - */ - async setAdditionalPrices( - products, - currency_code, - region_id, - cart_id, - customer_id, - include_discount_prices = false - ) { - return this.atomicPhase_(async (manager) => { - const cartRepo = this.manager_.getCustomRepository(this.cartRepository_) - - let regionId = region_id - let currencyCode = currency_code - - if (cart_id) { - const cart = await cartRepo.findOne({ - where: { id: cart_id }, - relations: ["region"], - }) - - regionId = cart.region.id - currencyCode = cart.region.currency_code - } - - const productArray = Array.isArray(products) ? products : [products] - - const priceSelectionStrategy = - this.priceSelectionStrategy_.withTransaction(manager) - - const productsWithPrices = await Promise.all( - productArray.map(async (p) => { - if (p.variants?.length) { - p.variants = await Promise.all( - p.variants.map(async (v) => { - const prices = - await priceSelectionStrategy.calculateVariantPrice(v.id, { - region_id: regionId, - currency_code: currencyCode, - cart_id: cart_id, - customer_id: customer_id, - include_discount_prices, - }) - - return { - ...v, - prices: prices.prices, - original_price: prices.originalPrice, - calculated_price: prices.calculatedPrice, - calculated_price_type: prices.calculatedPriceType, - } - }) - ) - } - - return p - }) - ) - - return Array.isArray(products) - ? productsWithPrices - : productsWithPrices[0] - }) - } } export default ProductService diff --git a/packages/medusa/src/services/tax-provider.ts b/packages/medusa/src/services/tax-provider.ts index 0f96d2c916..23460fc058 100644 --- a/packages/medusa/src/services/tax-provider.ts +++ b/packages/medusa/src/services/tax-provider.ts @@ -28,6 +28,11 @@ import TaxRateService from "./tax-rate" const CACHE_TIME = 30 // seconds +type RegionDetails = { + id: string + tax_rate: number | null +} + /** * Finds tax providers and assists in tax related operations. */ @@ -321,14 +326,14 @@ class TaxProviderService extends BaseService { * Gets the tax rates configured for a shipping option. The rates are cached * between calls. * @param optionId - the option id of the shipping method. - * @param region - the region to get configured rates for. + * @param regionDetails - the region to get configured rates for. * @return the tax rates configured for the shipping option. */ async getRegionRatesForShipping( optionId: string, - region: Region + regionDetails: RegionDetails ): Promise { - const cacheHit = await this.getCacheEntry(optionId, region.id) + const cacheHit = await this.getCacheEntry(optionId, regionDetails.id) if (cacheHit) { return cacheHit } @@ -336,7 +341,7 @@ class TaxProviderService extends BaseService { let toReturn: TaxServiceRate[] = [] const optionRates = await this.taxRateService_.listByShippingOption( optionId, - { region_id: region.id } + { region_id: regionDetails.id } ) if (optionRates.length > 0) { @@ -352,14 +357,14 @@ class TaxProviderService extends BaseService { if (toReturn.length === 0) { toReturn = [ { - rate: region.tax_rate, + rate: regionDetails.tax_rate, name: "default", code: "default", }, ] } - await this.setCache(optionId, region.id, toReturn) + await this.setCache(optionId, regionDetails.id, toReturn) return toReturn } @@ -373,7 +378,7 @@ class TaxProviderService extends BaseService { */ async getRegionRatesForProduct( productId: string, - region: Region + region: RegionDetails ): Promise { const cacheHit = await this.getCacheEntry(productId, region.id) if (cacheHit) { diff --git a/packages/medusa/src/types/pricing.ts b/packages/medusa/src/types/pricing.ts new file mode 100644 index 0000000000..b0c1a5d3bb --- /dev/null +++ b/packages/medusa/src/types/pricing.ts @@ -0,0 +1,30 @@ +import { MoneyAmount, ProductVariant, Product } from "../models" +import { TaxServiceRate } from "./tax-service" +import { PriceSelectionContext } from "../interfaces/price-selection-strategy" + +export type ProductVariantPricing = { + prices: MoneyAmount[] + original_price: number | null + calculated_price: number | null + calculated_price_type?: string | null +} & TaxedPricing + +export type TaxedPricing = { + original_price_incl_tax: number | null + calculated_price_incl_tax: number | null + original_tax: number | null + calculated_tax: number | null + tax_rates: TaxServiceRate[] | null +} + +export type PricingContext = { + price_selection: PriceSelectionContext + automatic_taxes: boolean + tax_rate: number | null +} + +export type PricedVariant = Partial & ProductVariantPricing + +export type PricedProduct = Omit, "variants"> & { + variants: PricedVariant[] +} diff --git a/yarn.lock b/yarn.lock index e6e63d81eb..f224426b42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7507,7 +7507,7 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@^4.17.13": +"@types/express@*", "@types/express@^4.17.13": version "4.17.13" resolved "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz" integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== @@ -7744,6 +7744,13 @@ "@types/bson" "*" "@types/node" "*" +"@types/multer@^1.4.7": + version "1.4.7" + resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.7.tgz#89cf03547c28c7bbcc726f029e2a76a7232cc79e" + integrity sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA== + dependencies: + "@types/express" "*" + "@types/node-fetch@2", "@types/node-fetch@^2.5.7": version "2.5.12" resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz"