Open ebdrup opened 4 months ago
Here is my pricing strategy
import {
AbstractPriceSelectionStrategy,
Customer,
CustomerGroup,
MoneyAmount,
PriceSelectionContext,
PriceSelectionResult,
PriceType,
} from '@medusajs/medusa';
import MoneyAmountRepository from '@medusajs/medusa/dist/repositories/money-amount';
import { TaxServiceRate } from '@medusajs/medusa/dist/types/tax-service';
import { FlagRouter, TaxInclusivePricingFeatureFlag } from '@medusajs/utils';
import { isDefined } from 'medusa-core-utils';
import { EntityManager } from 'typeorm';
class PriceSelectionStrategy extends AbstractPriceSelectionStrategy {
protected manager_: EntityManager;
protected readonly featureFlagRouter_: FlagRouter;
protected moneyAmountRepository_: typeof MoneyAmountRepository;
constructor({
manager,
featureFlagRouter,
moneyAmountRepository,
}: {
manager: EntityManager;
featureFlagRouter: FlagRouter;
moneyAmountRepository: typeof MoneyAmountRepository;
}) {
// @ts-expect-error Not sure
// eslint-disable-next-line prefer-rest-params
super(...arguments);
this.manager_ = manager;
this.moneyAmountRepository_ = moneyAmountRepository;
this.featureFlagRouter_ = featureFlagRouter;
console.info('CONSTRUCTOR PriceSelectionStrategy');
}
async calculateVariantPrice(
data: {
variantId: string;
quantity?: number;
}[],
context: PriceSelectionContext,
): Promise<Map<string, PriceSelectionResult>> {
const dataMap = new Map(data.map((d) => [d.variantId, d]));
console.info(context);
const nonCachedData: {
variantId: string;
quantity?: number;
}[] = [];
const variantPricesMap = new Map<string, PriceSelectionResult>();
nonCachedData.push(...dataMap.values());
let results: Map<string, PriceSelectionResult> = new Map();
if (
this.featureFlagRouter_.isFeatureEnabled(
TaxInclusivePricingFeatureFlag.key,
)
) {
results = await this.calculateVariantPrice_new(nonCachedData, context);
} else {
results = await this.calculateVariantPrice_old(nonCachedData, context);
}
[...results].map(([variantId, prices]) => {
variantPricesMap.set(variantId, prices);
});
return variantPricesMap;
}
private async modifyVariantPrices(
variantPrices: Record<string, MoneyAmount[]>,
customerId?: Customer['id'],
): Promise<Record<string, MoneyAmount[]>> {
if (!customerId) return variantPrices;
const customerGroup = await this.manager_
.createQueryBuilder(CustomerGroup, 'customer_group')
.leftJoinAndSelect('customer_group.customers', 'customers')
.where('customers_customer_group.customer_id = :customerId', {
customerId,
})
.getOne();
const companyId = customerGroup?.metadata.martsCompanyId;
//Based on the region and companyId we set the margins
console.info({ companyId });
const margin = 50000;
console.info('Adding margin', margin);
for (const prices of Object.values(variantPrices)) {
for (const price of prices) {
price.amount = Math.round(price.amount * (1 + margin / 100));
}
}
return variantPrices;
}
private async calculateVariantPrice_new(
data: {
variantId: string;
quantity?: number;
}[],
context: PriceSelectionContext,
): Promise<Map<string, PriceSelectionResult>> {
const moneyRepo = this.activeManager_.withRepository(
this.moneyAmountRepository_,
);
const [variantsPricesUnmodified] =
await moneyRepo.findManyForVariantsInRegion(
data.map((d) => d.variantId),
context.region_id,
context.currency_code,
context.customer_id,
context.include_discount_prices,
);
const variantsPrices = await this.modifyVariantPrices(
variantsPricesUnmodified,
context.customer_id,
);
const variantPricesMap = new Map<string, PriceSelectionResult>();
for (const [variantId, prices] of Object.entries(variantsPrices)) {
const dataItem = data.find((d) => d.variantId === variantId)!;
const result: PriceSelectionResult = {
originalPrice: null,
calculatedPrice: null,
prices,
originalPriceIncludesTax: null,
calculatedPriceIncludesTax: null,
};
if (!prices.length || !context) {
variantPricesMap.set(variantId, result);
}
const taxRate = context.tax_rates?.reduce(
(accRate: number, nextTaxRate: TaxServiceRate) => {
return accRate + (nextTaxRate.rate || 0) / 100;
},
0,
);
for (const ma of prices) {
let isTaxInclusive = ma.currency?.includes_tax || false;
if (ma.price_list?.includes_tax) {
// PriceList specific price so use the PriceList tax setting
isTaxInclusive = ma.price_list.includes_tax;
} else if (ma.region?.includes_tax) {
// Region specific price so use the Region tax setting
isTaxInclusive = ma.region.includes_tax;
}
delete ma.currency;
delete ma.region;
if (
context.region_id &&
ma.region_id === context.region_id &&
ma.price_list_id === null &&
ma.min_quantity === null &&
ma.max_quantity === null
) {
result.originalPriceIncludesTax = isTaxInclusive;
result.originalPrice = ma.amount;
}
if (
context.currency_code &&
ma.currency_code === context.currency_code &&
ma.price_list_id === null &&
ma.min_quantity === null &&
ma.max_quantity === null &&
result.originalPrice === null // region prices take precedence
) {
result.originalPriceIncludesTax = isTaxInclusive;
result.originalPrice = ma.amount;
}
if (
isValidQuantity(ma, dataItem.quantity) &&
isValidAmount(ma.amount, result, isTaxInclusive, taxRate) &&
((context.currency_code &&
ma.currency_code === context.currency_code) ||
(context.region_id && ma.region_id === context.region_id))
) {
result.calculatedPrice = ma.amount;
result.calculatedPriceType = ma.price_list?.type || PriceType.DEFAULT;
result.calculatedPriceIncludesTax = isTaxInclusive;
}
}
variantPricesMap.set(variantId, result);
}
return variantPricesMap;
}
private async calculateVariantPrice_old(
data: {
variantId: string;
quantity?: number;
}[],
context: PriceSelectionContext,
): Promise<Map<string, PriceSelectionResult>> {
const moneyRepo = this.activeManager_.withRepository(
this.moneyAmountRepository_,
);
const [variantsPricesUnmodified] =
await moneyRepo.findManyForVariantsInRegion(
data.map((d) => d.variantId),
context.region_id,
context.currency_code,
context.customer_id,
context.include_discount_prices,
);
const variantsPrices = await this.modifyVariantPrices(
variantsPricesUnmodified,
context.customer_id,
);
const variantPricesMap = new Map<string, PriceSelectionResult>();
for (const [variantId, prices] of Object.entries(variantsPrices)) {
const dataItem = data.find((d) => d.variantId === variantId)!;
const result: PriceSelectionResult = {
originalPrice: null,
calculatedPrice: null,
prices,
};
if (!prices.length || !context) {
variantPricesMap.set(variantId, result);
}
for (const ma of prices) {
delete ma.currency;
delete ma.region;
if (
context.region_id &&
ma.region_id === context.region_id &&
ma.price_list_id === null &&
ma.min_quantity === null &&
ma.max_quantity === null
) {
result.originalPrice = ma.amount;
}
if (
context.currency_code &&
ma.currency_code === context.currency_code &&
ma.price_list_id === null &&
ma.min_quantity === null &&
ma.max_quantity === null &&
result.originalPrice === null // region prices take precedence
) {
result.originalPrice = ma.amount;
}
if (
isValidQuantity(ma, dataItem.quantity) &&
(result.calculatedPrice === null ||
ma.amount < result.calculatedPrice) &&
((context.currency_code &&
ma.currency_code === context.currency_code) ||
(context.region_id && ma.region_id === context.region_id))
) {
result.calculatedPrice = ma.amount;
result.calculatedPriceType = ma.price_list?.type || PriceType.DEFAULT;
}
}
variantPricesMap.set(variantId, result);
}
return variantPricesMap;
}
}
const isValidAmount = (
amount: number,
result: PriceSelectionResult,
isTaxInclusive: boolean,
taxRate?: number,
): boolean => {
if (result.calculatedPrice === null) {
return true;
}
if (isTaxInclusive === result.calculatedPriceIncludesTax) {
// if both or neither are tax inclusive compare equally
return amount < result.calculatedPrice;
}
if (typeof taxRate !== 'undefined') {
return isTaxInclusive
? amount < (1 + taxRate) * result.calculatedPrice
: (1 + taxRate) * amount < result.calculatedPrice;
}
// if we dont have a taxrate we can't compare mixed prices
return false;
};
const isValidQuantity = (price: MoneyAmount, quantity?: number): boolean =>
(isDefined(quantity) && isValidPriceWithQuantity(price, quantity)) ||
(typeof quantity === 'undefined' && isValidPriceWithoutQuantity(price));
const isValidPriceWithoutQuantity = (price: MoneyAmount): boolean =>
!!(
(!price.max_quantity && !price.min_quantity) ||
((!price.min_quantity || price.min_quantity === 0) && price.max_quantity)
);
const isValidPriceWithQuantity = (
price: MoneyAmount,
quantity: number,
): boolean =>
(!price.min_quantity || price.min_quantity <= quantity) &&
(!price.max_quantity || price.max_quantity >= quantity);
export default PriceSelectionStrategy;
The console log of the pricing strategy is:
CONSTRUCTOR PriceSelectionStrategy
{
cart_id: 'cart_01HV68CY8ZN96DSTEXR0S65KCS',
region_id: 'reg_01HMBFAX5CV30TN32NEPNF7V2Q',
currency_code: 'dkk',
customer_id: undefined,
include_discount_prices: true,
tax_rates: [ { rate: 25, name: 'default', code: 'default' } ]
}
And for the cart with ID cart_01HV68CY8ZN96DSTEXR0S65KCS
the DB shows it has a customer_id:
I would expect the customer_id of the context of the pricing strategy being logged, to be set to the customer_id of the cart
@ebdrup right now, only signed-in customers will get customer-specific pricing. The customer_id
of the context is set using the authenticated session object:
This does not immediately solve your need (unless your customers will be signed in), but thought I'd highlight, so you know why customer_id
shows up as undefined
.
Bug report (I think)
Describe the bug
I have created a customer in medusa, and I have created a cart in medusa and updated the cart with the customer id of the customer I created.
Now I list products with medusa.products.list and pass the cart_id as the cart I have created.
I have then implemented a Pricing Strategy, that gets run when I call medusa.products.list, but for some reason the customer_id is not set in the context passed to the pricing strategy.
The context in the pricing strategy has the right cart_id. When I look up the cart in the medusa DB for that id, the cart does have a customer_id set, but the pricing strategy does not have the customer_id of the cart in it's context.
When adding items to the cart, the pricing strategy gets called with a context that has the right cart_id and customer_id.
How do I make the customer_id be set in the context of the pricing strategy, when doing medusa.products.list?
System information
Medusa version (including plugins): 1.20.1 Node.js version: v20.9.0 Database: Postgesql Operating system: OSX Browser (if relevant):
Steps to reproduce the behavior
I have created a customer in medusa, and I have created a cart in medusa and updated the cart with the customer id of the customer I created.
Now I list products with medusa.products.list and pass the cart_id as the cart I have created.
I have then implemented a Pricing Strategy, that gets run when I call medusa.products.list, but for some reason the customer_id is not set in the context passed to the pricing strategy.
Expected behavior
The customer_id is set in the context of the Pricing strategy, when calling medusa.products.list with a cart_id of a cart that has a customer assigned.