feat: price formatting

This commit is contained in:
Victor Gerbrands 2023-05-05 12:28:12 +02:00
parent 575c3a3d0b
commit cce421c418
4 changed files with 108 additions and 17 deletions

43
lib/medusa/helpers.ts Normal file
View File

@ -0,0 +1,43 @@
import { isEmpty } from '../utils';
import { MedusaProductVariant, RegionInfo } from './types';
type ComputeAmountParams = {
amount: number;
region: RegionInfo;
includeTaxes?: boolean;
};
/**
* Takes an amount, a region, and returns the amount as a decimal including or excluding taxes
*/
export const computeAmount = ({ amount, region, includeTaxes = true }: ComputeAmountParams) => {
const toDecimal = convertToDecimal(amount, region.currency_code);
const taxRate = includeTaxes ? getTaxRate(region) : 0;
const amountWithTaxes = toDecimal * (1 + taxRate);
return amountWithTaxes;
};
export const calculateVariantAmount = (variant: MedusaProductVariant) => {
const currencyCode = variant.prices?.[0]?.currency_code ?? 'USD';
const amount = convertToDecimal(variant.prices?.[0]?.amount || 0, currencyCode).toString();
return {
amount,
currencyCode
};
};
// we should probably add a more extensive list
const noDivisionCurrencies = ['krw', 'jpy', 'vnd'];
export const convertToDecimal = (amount: number, currencyCode = 'USD') => {
const divisor = noDivisionCurrencies.includes(currencyCode.toLowerCase()) ? 1 : 100;
return Math.floor(amount) / divisor;
};
const getTaxRate = (region?: RegionInfo) => {
return region && !isEmpty(region) ? region?.tax_rate / 100 : 0;
};

View File

@ -1,6 +1,7 @@
import { isMedusaError } from 'lib/type-guards'; import { isMedusaError } from 'lib/type-guards';
import { mapOptionIds } from 'lib/utils'; import { mapOptionIds } from 'lib/utils';
import { calculateVariantAmount, computeAmount, convertToDecimal } from './helpers';
import { import {
Cart, Cart,
CartItem, CartItem,
@ -67,18 +68,34 @@ const reshapeCart = (cart: MedusaCart): Cart => {
const lines = cart?.items?.map((item) => reshapeLineItem(item)) || []; const lines = cart?.items?.map((item) => reshapeLineItem(item)) || [];
const totalQuantity = lines.length; const totalQuantity = lines.length;
const checkoutUrl = '/'; const checkoutUrl = '/';
const currencyCode = 'EUR'; const currencyCode = cart.region?.currency_code || 'USD';
let subtotalAmount = '0';
if (cart.subtotal && cart.region) {
subtotalAmount = computeAmount({ amount: cart.subtotal, region: cart.region }).toString();
}
let totalAmount = '0';
if (cart.total && cart.region) {
totalAmount = computeAmount({ amount: cart.total, region: cart.region }).toString();
}
let totalTaxAmount = '0';
if (cart.tax_total && cart.region) {
totalTaxAmount = computeAmount({ amount: cart.tax_total, region: cart.region }).toString();
}
const cost = { const cost = {
subtotalAmount: { subtotalAmount: {
amount: cart?.subtotal?.toString() || '0', amount: subtotalAmount,
currencyCode: currencyCode currencyCode: currencyCode
}, },
totalAmount: { totalAmount: {
amount: (cart?.total && cart?.total.toString()) || '0', amount: totalAmount,
currencyCode: currencyCode currencyCode: currencyCode
}, },
totalTaxAmount: { totalTaxAmount: {
amount: (cart?.tax_total && cart?.tax_total.toString()) || '0', amount: totalTaxAmount,
currencyCode: currencyCode currencyCode: currencyCode
} }
}; };
@ -96,10 +113,7 @@ const reshapeLineItem = (lineItem: MedusaLineItem): CartItem => {
const product = { const product = {
title: lineItem.title, title: lineItem.title,
priceRange: { priceRange: {
maxVariantPrice: { maxVariantPrice: calculateVariantAmount(lineItem.variant)
amount: lineItem.variant?.prices?.[0]?.amount.toString() ?? '0',
currencyCode: lineItem.variant?.prices?.[0]?.currency_code ?? 'EUR'
}
}, },
updatedAt: lineItem.updated_at, updatedAt: lineItem.updated_at,
tags: [], tags: [],
@ -128,7 +142,10 @@ const reshapeLineItem = (lineItem: MedusaLineItem): CartItem => {
const cost = { const cost = {
totalAmount: { totalAmount: {
amount: lineItem.total.toString() ?? '0', amount: convertToDecimal(
lineItem.total,
lineItem.variant.prices?.[0]?.currency_code
).toString(),
currencyCode: 'EUR' currencyCode: 'EUR'
} }
}; };
@ -143,9 +160,18 @@ const reshapeLineItem = (lineItem: MedusaLineItem): CartItem => {
}; };
const reshapeProduct = (product: MedusaProduct): Product => { const reshapeProduct = (product: MedusaProduct): Product => {
const variant = product.variants?.[0];
let amount = '0';
let currencyCode = 'USD';
if (variant && variant.prices?.[0]?.amount) {
currencyCode = variant.prices?.[0]?.currency_code ?? 'USD';
amount = convertToDecimal(variant.prices[0].amount, currencyCode).toString();
}
const priceRange = { const priceRange = {
maxVariantPrice: { maxVariantPrice: {
amount: product.variants?.[0]?.prices?.[0]?.amount.toString() ?? '0', amount,
currencyCode: product.variants?.[0]?.prices?.[0]?.currency_code ?? '' currencyCode: product.variants?.[0]?.prices?.[0]?.currency_code ?? ''
} }
}; };
@ -156,7 +182,7 @@ const reshapeProduct = (product: MedusaProduct): Product => {
url: product.images?.[0]?.url ?? '', url: product.images?.[0]?.url ?? '',
altText: product.images?.[0]?.id ?? '' altText: product.images?.[0]?.id ?? ''
}; };
const availableForSale = true; const availableForSale = product.variants?.[0]?.purchasable || true;
const variants = product.variants.map((variant) => const variants = product.variants.map((variant) =>
reshapeProductVariant(variant, product.options) reshapeProductVariant(variant, product.options)
); );
@ -178,7 +204,7 @@ const reshapeProduct = (product: MedusaProduct): Product => {
}; };
const reshapeProductOption = (productOption: MedusaProductOption): ProductOption => { const reshapeProductOption = (productOption: MedusaProductOption): ProductOption => {
const availableForSale = true; const availableForSale = productOption.product?.variants?.[0]?.purchasable || true;
const name = productOption.title; const name = productOption.title;
let values = productOption.values?.map((option) => option.value) || []; let values = productOption.values?.map((option) => option.value) || [];
values = [...new Set(values)]; values = [...new Set(values)];
@ -203,12 +229,9 @@ const reshapeProductVariant = (
value: option.value value: option.value
})); }));
} }
const availableForSale = !!productVariant.inventory_quantity; const availableForSale = productVariant.purchasable || true;
const price = calculateVariantAmount(productVariant);
const price = {
amount: productVariant.prices?.[0]?.amount.toString() ?? 'ß',
currencyCode: productVariant.prices?.[0]?.currency_code ?? ''
};
return { return {
...productVariant, ...productVariant,
availableForSale, availableForSale,

View File

@ -153,6 +153,7 @@ export type MedusaProductVariant = {
created_at: string; created_at: string;
updated_at: string; updated_at: string;
deleted_at: string | null; deleted_at: string | null;
purchasable?: boolean;
}; };
export type ProductVariant = MedusaProductVariant & { export type ProductVariant = MedusaProductVariant & {
@ -279,6 +280,9 @@ export type Region = {
updated_at: string; updated_at: string;
deleted_at?: string | null; deleted_at?: string | null;
metadata?: Record<string, unknown> | null; metadata?: Record<string, unknown> | null;
currency_code: string;
tax_code: string;
tax_rate: number;
}; };
export type FulfillmentProvider = { export type FulfillmentProvider = {
@ -417,3 +421,11 @@ export type CartItem = MedusaLineItem & {
}; };
quantity: number; quantity: number;
}; };
export type RegionInfo = Pick<Region, 'currency_code' | 'tax_code' | 'tax_rate'>;
export type ProductVariantEntity = ConvertDateToString<Omit<ProductVariant, 'beforeInsert'>>;
export type ProductVariantInfo = Pick<ProductVariantEntity, 'prices'>;
type ConvertDateToString<T extends {}> = {
[P in keyof T]: T[P] extends Date ? Date | string : T[P];
};

View File

@ -15,3 +15,16 @@ export const mapOptionIds = (productOptions: MedusaProductOption[]) => {
}); });
return map; return map;
}; };
export const isObject = (input: any) => input instanceof Object;
export const isArray = (input: any) => Array.isArray(input);
export const isEmpty = (input: any) => {
return (
input === null ||
input === undefined ||
(isObject(input) && Object.keys(input).length === 0) ||
(isArray(input) && (input as any[]).length === 0) ||
(typeof input === 'string' && input.trim().length === 0)
);
};