feat: Add necessary middlewares for tax inclusive pricing (#7827)
We are adding tax inclusive pricing calculation when listing products. Two things to keep in mind: - `region_id` will be required if you request calculated prices. - We won't accept `currency_code` anymore, as that will come from the region info (since ultimately a cart and its currency are tied to a region) REF CORE-2376 DEPENDS ON #8003
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
import { MedusaError, isPresent } from "@medusajs/utils"
|
||||
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
|
||||
import { MedusaResponse } from "../../../../types/routing"
|
||||
import { wrapVariantsWithInventoryQuantity } from "../../../utils/middlewares"
|
||||
import { refetchProduct } from "../helpers"
|
||||
import { StoreGetProductsParamsType } from "../validators"
|
||||
import {
|
||||
RequestWithContext,
|
||||
refetchProduct,
|
||||
wrapProductsWithTaxPrices,
|
||||
} from "../helpers"
|
||||
import { StoreGetProductParamsType } from "../validators"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest<StoreGetProductsParamsType>,
|
||||
req: RequestWithContext<StoreGetProductParamsType>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const withInventoryQuantity = req.remoteQueryConfig.fields.some((field) =>
|
||||
@@ -46,5 +50,6 @@ export const GET = async (
|
||||
await wrapVariantsWithInventoryQuantity(req, product.variants || [])
|
||||
}
|
||||
|
||||
await wrapProductsWithTaxPrices(req, [product])
|
||||
res.json({ product })
|
||||
}
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
import { MedusaContainer } from "@medusajs/types"
|
||||
import {
|
||||
ModuleRegistrationName,
|
||||
calculateAmountsWithTax,
|
||||
} from "@medusajs/utils"
|
||||
import { MedusaRequest } from "../../../types/routing"
|
||||
import { refetchEntities, refetchEntity } from "../../utils/refetch-entity"
|
||||
import {
|
||||
MedusaContainer,
|
||||
HttpTypes,
|
||||
TaxableItemDTO,
|
||||
ItemTaxLineDTO,
|
||||
TaxCalculationContext,
|
||||
} from "@medusajs/types"
|
||||
|
||||
export type RequestWithContext<T> = MedusaRequest<T> & {
|
||||
taxContext: {
|
||||
taxLineContext?: TaxCalculationContext
|
||||
taxInclusivityContext?: {
|
||||
automaticTaxes: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const refetchProduct = async (
|
||||
idOrFilter: string | object,
|
||||
@@ -30,3 +49,81 @@ export const maybeApplyStockLocationId = async (req: MedusaRequest, ctx) => {
|
||||
|
||||
return entities.map((entity) => entity.stock_location_id)
|
||||
}
|
||||
|
||||
export const wrapProductsWithTaxPrices = async <T>(
|
||||
req: RequestWithContext<T>,
|
||||
products: HttpTypes.StoreProduct[]
|
||||
) => {
|
||||
// If we are missing the necessary context, we can't calculate the tax, so only `calculated_amount` will be available
|
||||
if (
|
||||
!req.taxContext?.taxInclusivityContext ||
|
||||
!req.taxContext?.taxLineContext
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// If automatic taxes are not enabled, we should skip calculating any tax
|
||||
if (!req.taxContext.taxInclusivityContext.automaticTaxes) {
|
||||
return
|
||||
}
|
||||
|
||||
const taxService = req.scope.resolve(ModuleRegistrationName.TAX)
|
||||
|
||||
const taxRates = (await taxService.getTaxLines(
|
||||
products.map(asTaxItem).flat(),
|
||||
req.taxContext.taxLineContext
|
||||
)) as unknown as ItemTaxLineDTO[]
|
||||
|
||||
const taxRatesMap = new Map<string, ItemTaxLineDTO[]>()
|
||||
taxRates.forEach((taxRate) => {
|
||||
if (!taxRatesMap.has(taxRate.line_item_id)) {
|
||||
taxRatesMap.set(taxRate.line_item_id, [])
|
||||
}
|
||||
|
||||
taxRatesMap.get(taxRate.line_item_id)?.push(taxRate)
|
||||
})
|
||||
|
||||
products.forEach((product) => {
|
||||
product.variants?.forEach((variant) => {
|
||||
if (!variant.calculated_price) {
|
||||
return
|
||||
}
|
||||
|
||||
const taxRatesForVariant = taxRatesMap.get(variant.id) || []
|
||||
const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({
|
||||
taxLines: taxRatesForVariant,
|
||||
amount: variant.calculated_price!.calculated_amount!,
|
||||
includesTax:
|
||||
variant.calculated_price!.is_calculated_price_tax_inclusive!,
|
||||
})
|
||||
|
||||
variant.calculated_price.calculated_amount_with_tax = priceWithTax
|
||||
variant.calculated_price.calculated_amount_without_tax = priceWithoutTax
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const asTaxItem = (product: HttpTypes.StoreProduct): TaxableItemDTO[] => {
|
||||
return product.variants
|
||||
?.map((variant) => {
|
||||
if (!variant.calculated_price) {
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
id: variant.id,
|
||||
product_id: product.id,
|
||||
product_name: product.title,
|
||||
product_categories: product.categories?.map((c) => c.name),
|
||||
// TODO: It is strange that we only accept a single category, revisit the tax module implementation
|
||||
product_category_id: product.categories?.[0]?.id,
|
||||
product_sku: variant.sku,
|
||||
product_type: product.type,
|
||||
product_type_id: product.type_id,
|
||||
quantity: 1,
|
||||
unit_price: variant.calculated_price.calculated_amount,
|
||||
currency_code: variant.calculated_price.currency_code,
|
||||
}
|
||||
})
|
||||
.filter((v) => !!v) as unknown as TaxableItemDTO[]
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@ import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
|
||||
import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter"
|
||||
import {
|
||||
applyDefaultFilters,
|
||||
clearFiltersByKey,
|
||||
filterByValidSalesChannels,
|
||||
normalizeDataForContext,
|
||||
setPricingContext,
|
||||
setTaxContext,
|
||||
} from "../../utils/middlewares"
|
||||
import { setContext } from "../../utils/middlewares/common/set-context"
|
||||
import { validateAndTransformQuery } from "../../utils/validate-query"
|
||||
@@ -47,7 +50,10 @@ export const storeProductRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
return { id: categoryIds, is_internal: false, is_active: true }
|
||||
},
|
||||
}),
|
||||
normalizeDataForContext(),
|
||||
setPricingContext(),
|
||||
setTaxContext(),
|
||||
clearFiltersByKey(["region_id", "country_code", "province", "cart_id"]),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -78,7 +84,10 @@ export const storeProductRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
return { is_internal: false, is_active: true }
|
||||
},
|
||||
}),
|
||||
normalizeDataForContext(),
|
||||
setPricingContext(),
|
||||
setTaxContext(),
|
||||
clearFiltersByKey(["region_id", "country_code", "province", "cart_id"]),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -3,12 +3,13 @@ import {
|
||||
isPresent,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
|
||||
import { MedusaResponse } from "../../../types/routing"
|
||||
import { wrapVariantsWithInventoryQuantity } from "../../utils/middlewares"
|
||||
import { StoreGetProductsParamsType } from "./validators"
|
||||
import { RequestWithContext, wrapProductsWithTaxPrices } from "./helpers"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest<StoreGetProductsParamsType>,
|
||||
req: RequestWithContext<StoreGetProductsParamsType>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
@@ -48,6 +49,7 @@ export const GET = async (
|
||||
)
|
||||
}
|
||||
|
||||
await wrapProductsWithTaxPrices(req, products)
|
||||
res.json({
|
||||
products,
|
||||
count: metadata.count,
|
||||
|
||||
@@ -9,7 +9,17 @@ import {
|
||||
createSelectParams,
|
||||
} from "../../utils/validators"
|
||||
|
||||
export const StoreGetProductParams = createSelectParams()
|
||||
export type StoreGetProductParamsType = z.infer<typeof StoreGetProductParams>
|
||||
|
||||
export const StoreGetProductParams = createSelectParams().merge(
|
||||
// These are used to populate the tax and pricing context
|
||||
z.object({
|
||||
region_id: z.string().optional(),
|
||||
country_code: z.string().optional(),
|
||||
province: z.string().optional(),
|
||||
cart_id: z.string().optional(),
|
||||
})
|
||||
)
|
||||
|
||||
export type StoreGetProductVariantsParamsType = z.infer<
|
||||
typeof StoreGetProductVariantsParams
|
||||
@@ -38,8 +48,12 @@ export const StoreGetProductsParams = createFindParams({
|
||||
}).merge(
|
||||
z
|
||||
.object({
|
||||
// These are used to populate the tax and pricing context
|
||||
region_id: z.string().optional(),
|
||||
currency_code: z.string().optional(),
|
||||
country_code: z.string().optional(),
|
||||
province: z.string().optional(),
|
||||
cart_id: z.string().optional(),
|
||||
|
||||
variants: z
|
||||
.object({
|
||||
status: ProductStatusEnum.array().optional(),
|
||||
|
||||
Reference in New Issue
Block a user