feat: Totals calculation (#6980)

This commit is contained in:
Carlos R. L. Rodrigues authored and GitHub committed 2024-04-07 16:50:40 +02:00
1 parent 7302d078a9
commit b6879f017a
48 files changed
+1674 -571

No files matched your search

@@ -0,0 +1,637 @@
import { decorateCartTotals } from "../../totals"
describe("Total calculation", function () {
it("should calculate carts with items + taxes", function () {
const cart = {
items: [
{
unit_price: 30,
quantity: 2,
tax_lines: [
{
rate: 10,
},
],
},
{
unit_price: 5,
quantity: 1,
tax_lines: [
{
rate: 50,
},
],
},
],
}
const serialized = JSON.parse(JSON.stringify(decorateCartTotals(cart)))
expect(serialized).toEqual({
items: [
{
unit_price: 30,
quantity: 2,
tax_lines: [
{
rate: 10,
total: 6,
subtotal: 6,
},
],
subtotal: 60,
total: 66,
original_total: 66,
discount_total: 0,
discount_tax_total: 0,
tax_total: 6,
original_tax_total: 6,
},
{
unit_price: 5,
quantity: 1,
tax_lines: [
{
rate: 50,
total: 2.5,
subtotal: 2.5,
},
],
subtotal: 5,
total: 7.5,
original_total: 7.5,
discount_total: 0,
discount_tax_total: 0,
tax_total: 2.5,
original_tax_total: 2.5,
},
],
total: 73.5,
subtotal: 65,
tax_total: 8.5,
discount_total: 0,
discount_tax_total: 0,
item_total: 73.5,
item_subtotal: 65,
item_tax_total: 8.5,
original_total: 73.5,
original_tax_total: 8.5,
original_item_subtotal: 65,
original_item_total: 73.5,
original_item_tax_total: 8.5,
})
})
it("should calculate carts with items + taxes + adjustments", function () {
const cart = {
items: [
{
unit_price: 50,
quantity: 2,
tax_lines: [
{
rate: 10,
},
],
adjustments: [
{
amount: 10,
},
],
},
],
}
const serialized = JSON.parse(JSON.stringify(decorateCartTotals(cart)))
expect(serialized).toEqual({
items: [
{
unit_price: 50,
quantity: 2,
tax_lines: [
{
rate: 10,
total: 9,
subtotal: 10,
},
],
adjustments: [
{
amount: 10,
total: 11,
subtotal: 10,
},
],
subtotal: 100,
total: 99,
original_total: 110,
discount_total: 10,
discount_tax_total: 1,
tax_total: 9,
original_tax_total: 10,
},
],
total: 99,
subtotal: 100,
tax_total: 9,
discount_total: 10,
discount_tax_total: 1,
original_total: 100,
original_tax_total: 10,
item_total: 99,
item_subtotal: 100,
item_tax_total: 9,
original_item_total: 110,
original_item_subtotal: 100,
original_item_tax_total: 10,
})
})
it("should calculate carts with shipping_methods + items + taxes + discounts with is_tax_inclusive", function () {
const cartMixed = {
items: [
{
unit_price: 100,
quantity: 1,
is_tax_inclusive: true,
tax_lines: [
{
rate: 10,
},
],
adjustments: [
{
amount: 10,
},
],
},
{
unit_price: 10,
quantity: 1,
is_tax_inclusive: false,
tax_lines: [
{
rate: 10,
},
],
adjustments: [
{
amount: 3,
},
],
},
],
shipping_methods: [
{
amount: 10,
is_tax_inclusive: true,
tax_lines: [
{
rate: 5,
},
],
adjustments: [
{
amount: 2,
},
],
},
{
amount: 5,
is_tax_inclusive: false,
tax_lines: [
{
rate: 10,
},
],
adjustments: [
{
amount: 2,
},
],
},
],
}
const serializedMixed = JSON.parse(
JSON.stringify(decorateCartTotals(cartMixed))
)
expect(serializedMixed).toEqual({
items: [
{
unit_price: 100,
quantity: 1,
is_tax_inclusive: true,
tax_lines: [
{
rate: 10,
total: 8.181818181818182,
subtotal: 9.090909090909092,
},
],
adjustments: [
{
amount: 10,
subtotal: 9.090909090909092,
total: 10,
},
],
subtotal: 90.9090909090909,
total: 90,
original_total: 100,
discount_total: 10,
discount_tax_total: 1,
tax_total: 8.181818181818182,
original_tax_total: 9.090909090909092,
},
{
unit_price: 10,
quantity: 1,
is_tax_inclusive: false,
tax_lines: [
{
rate: 10,
total: 0.7,
subtotal: 1,
},
],
adjustments: [
{
amount: 3,
subtotal: 3,
total: 3.3,
},
],
subtotal: 10,
total: 7.7,
original_total: 11,
discount_total: 3,
discount_tax_total: 0.3,
tax_total: 0.7,
original_tax_total: 1,
},
],
shipping_methods: [
{
amount: 10,
is_tax_inclusive: true,
tax_lines: [
{
rate: 5,
total: 0.38095238095238093,
subtotal: 0.47619047619047616,
},
],
adjustments: [
{
amount: 2,
subtotal: 1.9047619047619047,
total: 2,
},
],
subtotal: 10.380952380952381,
total: 8,
original_total: 10,
discount_total: 2,
discount_tax_total: 0.1,
tax_total: 0.38095238095238093,
original_tax_total: 0.47619047619047616,
},
{
amount: 5,
is_tax_inclusive: false,
tax_lines: [
{
rate: 10,
total: 0.3,
subtotal: 0.5,
},
],
adjustments: [
{
amount: 2,
subtotal: 2,
total: 2.2,
},
],
subtotal: 5,
total: 3.3,
original_total: 5.5,
discount_total: 2,
discount_tax_total: 0.2,
tax_total: 0.3,
original_tax_total: 0.5,
},
],
total: 104.77186147186147,
subtotal: 100.9090909090909,
tax_total: 9.562770562770563,
discount_total: 17,
discount_tax_total: 1.6,
original_total: 110.47619047619048,
original_tax_total: 11.067099567099566,
item_total: 97.7,
item_subtotal: 100.9090909090909,
item_tax_total: 8.881818181818181,
original_item_total: 111,
original_item_subtotal: 100.9090909090909,
original_item_tax_total: 10.090909090909092,
shipping_total: 11.3,
shipping_subtotal: 15.380952380952381,
shipping_tax_total: 0.680952380952381,
original_shipping_tax_total: 0.9761904761904762,
original_shipping_tax_subtotal: 15.380952380952381,
original_shipping_total: 15.5,
})
})
it("should calculate carts with items + taxes with is_tax_inclusive", function () {
const cartWithTax = {
items: [
{
unit_price: 50,
quantity: 2,
is_tax_inclusive: true,
tax_lines: [
{
rate: 10,
},
],
},
],
}
const cartWithoutTax = {
items: [
{
unit_price: 50,
quantity: 2,
is_tax_inclusive: false,
tax_lines: [
{
rate: 10,
},
],
},
],
}
const cartMixed = {
items: [...cartWithTax.items, ...cartWithoutTax.items],
}
const serializedWith = JSON.parse(
JSON.stringify(decorateCartTotals(cartWithTax))
)
const serializedWithout = JSON.parse(
JSON.stringify(decorateCartTotals(cartWithoutTax))
)
const serializedMixed = JSON.parse(
JSON.stringify(decorateCartTotals(cartMixed))
)
expect(serializedWith).toEqual({
items: [
{
unit_price: 50,
quantity: 2,
is_tax_inclusive: true,
tax_lines: [
{
rate: 10,
total: 9.090909090909092,
subtotal: 9.090909090909092,
},
],
subtotal: 90.9090909090909,
total: 100,
original_total: 100,
discount_total: 0,
discount_tax_total: 0,
tax_total: 9.090909090909092,
original_tax_total: 9.090909090909092,
},
],
total: 100,
subtotal: 90.9090909090909,
tax_total: 9.090909090909092,
discount_total: 0,
discount_tax_total: 0,
original_total: 100,
original_tax_total: 9.090909090909092,
item_total: 100,
item_subtotal: 90.9090909090909,
item_tax_total: 9.090909090909092,
original_item_total: 100,
original_item_subtotal: 90.9090909090909,
original_item_tax_total: 9.090909090909092,
})
expect(serializedWithout).toEqual({
items: [
{
unit_price: 50,
quantity: 2,
is_tax_inclusive: false,
tax_lines: [
{
rate: 10,
total: 10,
subtotal: 10,
},
],
subtotal: 100,
total: 110,
original_total: 110,
discount_total: 0,
discount_tax_total: 0,
tax_total: 10,
original_tax_total: 10,
},
],
total: 110,
subtotal: 100,
tax_total: 10,
discount_total: 0,
discount_tax_total: 0,
original_total: 110,
original_tax_total: 10,
item_total: 110,
item_subtotal: 100,
item_tax_total: 10,
original_item_total: 110,
original_item_subtotal: 100,
original_item_tax_total: 10,
})
expect(serializedMixed).toEqual({
items: [
{
unit_price: 50,
quantity: 2,
is_tax_inclusive: true,
tax_lines: [
{
rate: 10,
total: 9.090909090909092,
subtotal: 9.090909090909092,
},
],
subtotal: 90.9090909090909,
total: 100,
original_total: 100,
discount_total: 0,
discount_tax_total: 0,
tax_total: 9.090909090909092,
original_tax_total: 9.090909090909092,
},
{
unit_price: 50,
quantity: 2,
is_tax_inclusive: false,
tax_lines: [
{
rate: 10,
total: 10,
subtotal: 10,
},
],
subtotal: 100,
total: 110,
original_total: 110,
discount_total: 0,
discount_tax_total: 0,
tax_total: 10,
original_tax_total: 10,
},
],
total: 210,
subtotal: 190.9090909090909,
tax_total: 19.09090909090909,
discount_total: 0,
discount_tax_total: 0,
original_total: 210,
original_tax_total: 19.09090909090909,
item_total: 210,
item_subtotal: 190.9090909090909,
item_tax_total: 19.09090909090909,
original_item_total: 210,
original_item_subtotal: 190.9090909090909,
original_item_tax_total: 19.09090909090909,
})
})
it("should calculate carts with items + taxes + adjustments + shipping methods", function () {
const cart = {
items: [
{
unit_price: 50,
quantity: 2,
tax_lines: [
{
rate: 10,
},
],
adjustments: [
{
amount: 20,
},
],
},
],
shipping_methods: [
{
amount: 25,
tax_lines: [
{
rate: 10,
},
],
adjustments: [
{
amount: 2,
},
],
},
],
}
const serialized = JSON.parse(JSON.stringify(decorateCartTotals(cart)))
expect(serialized).toEqual({
items: [
{
unit_price: 50,
quantity: 2,
tax_lines: [
{
rate: 10,
total: 8,
subtotal: 10,
},
],
adjustments: [
{
amount: 20,
total: 22,
subtotal: 20,
},
],
subtotal: 100,
total: 88,
original_total: 110,
discount_total: 20,
discount_tax_total: 2,
tax_total: 8,
original_tax_total: 10,
},
],
shipping_methods: [
{
amount: 25,
tax_lines: [
{
rate: 10,
total: 2.3,
subtotal: 2.5,
},
],
adjustments: [
{
amount: 2,
total: 2.2,
subtotal: 2,
},
],
subtotal: 25,
total: 25.3,
original_total: 27.5,
discount_total: 2,
discount_tax_total: 0.2,
tax_total: 2.3,
original_tax_total: 2.5,
},
],
total: 113.6,
subtotal: 100,
tax_total: 10.3,
discount_total: 22,
discount_tax_total: 2.2,
original_total: 118,
original_tax_total: 12.5,
item_total: 88,
item_subtotal: 100,
item_tax_total: 8,
original_item_total: 110,
original_item_subtotal: 100,
original_item_tax_total: 10,
shipping_total: 25.3,
shipping_subtotal: 25,
shipping_tax_total: 2.3,
original_shipping_tax_total: 2.5,
original_shipping_tax_subtotal: 25,
original_shipping_total: 27.5,
})
})
})
@@ -0,0 +1,40 @@
import { AdjustmentLineDTO, BigNumberInput } from "@medusajs/types"
import { isDefined } from "../../common"
import { BigNumber } from "../big-number"
import { MathBN } from "../math"
export function calculateAdjustmentTotal({
adjustments,
includesTax,
taxRate,
}: {
adjustments: Pick<AdjustmentLineDTO, "amount">[]
includesTax?: boolean
taxRate?: BigNumberInput
}) {
let total = MathBN.convert(0)
for (const adj of adjustments) {
if (!isDefined(adj.amount)) {
continue
}
total = MathBN.add(total, adj.amount)
if (isDefined(taxRate)) {
const rate = MathBN.div(taxRate, 100)
let taxAmount = MathBN.mult(adj.amount, rate)
if (includesTax) {
taxAmount = MathBN.div(taxAmount, MathBN.add(1, rate))
adj["subtotal"] = new BigNumber(MathBN.sub(adj.amount, taxAmount))
adj["total"] = new BigNumber(adj.amount)
} else {
adj["subtotal"] = new BigNumber(adj.amount)
adj["total"] = new BigNumber(MathBN.add(adj.amount, taxAmount))
}
}
}
return total
}
+1 -1
View File
@@ -119,6 +119,6 @@ export class BigNumber {
}
valueOf() {
return this.bignumber_
return this.numeric_
}
}
+227
View File
@@ -0,0 +1,227 @@
import { BigNumberInput, CartLikeWithTotals } from "@medusajs/types"
import { BigNumber } from "../big-number"
import { GetItemTotalInput, getLineItemsTotals } from "../line-item"
import { MathBN } from "../math"
import {
GetShippingMethodTotalInput,
getShippingMethodsTotals,
} from "../shipping-method"
import { transformPropertiesToBigNumber } from "../transform-properties-to-bignumber"
interface TotalsConfig {
includeTaxes?: boolean
}
export interface DecorateCartLikeInputDTO {
items?: {
id?: string
unit_price: BigNumberInput
quantity: BigNumberInput
adjustments?: { amount: BigNumberInput }[]
tax_lines?: {
rate: BigNumberInput
is_tax_inclusive?: boolean
}[]
}[]
shipping_methods?: {
id?: string
amount: BigNumberInput
adjustments?: { amount: BigNumberInput }[]
tax_lines?: {
rate: BigNumberInput
is_tax_inclusive?: boolean
}[]
}[]
region?: {
automatic_taxes?: boolean
}
}
export function decorateCartTotals(
cartLike: DecorateCartLikeInputDTO,
config: TotalsConfig = {}
): CartLikeWithTotals {
transformPropertiesToBigNumber(cartLike)
const items = (cartLike.items ?? []) as unknown as GetItemTotalInput[]
const shippingMethods = (cartLike.shipping_methods ??
[]) as unknown as GetShippingMethodTotalInput[]
const includeTax = config?.includeTaxes || cartLike.region?.automatic_taxes
const itemsTotals = getLineItemsTotals(items, {
includeTax,
})
const shippingMethodsTotals = getShippingMethodsTotals(shippingMethods, {
includeTax,
})
let subtotal = MathBN.convert(0)
let discountTotal = MathBN.convert(0)
let discountTaxTotal = MathBN.convert(0)
let itemsSubtotal = MathBN.convert(0)
let itemsSubtotalWithoutTaxes = MathBN.convert(0)
let itemsTotal = MathBN.convert(0)
let itemsOriginalTotal = MathBN.convert(0)
let itemsOriginalSubtotal = MathBN.convert(0)
let itemsTaxTotal = MathBN.convert(0)
let itemsOriginalTaxTotal = MathBN.convert(0)
let shippingSubtotal = MathBN.convert(0)
let shippingTotal = MathBN.convert(0)
let shippingOriginalTotal = MathBN.convert(0)
let shippingOriginalSubtotal = MathBN.convert(0)
let shippingTaxTotal = MathBN.convert(0)
let shippingOriginalTaxTotal = MathBN.convert(0)
let shippingOriginalTaxSubtotal = MathBN.convert(0)
const cartItems = items.map((item, index) => {
const itemTotals = Object.assign(item, itemsTotals[item.id ?? index] ?? {})
const itemSubtotal = itemTotals.subtotal
const itemTotal = MathBN.convert(itemTotals.total)
const itemOriginalTotal = MathBN.convert(itemTotals.original_total)
const itemTaxTotal = MathBN.convert(itemTotals.tax_total)
const itemOriginalTaxTotal = MathBN.convert(itemTotals.original_tax_total)
const itemDiscountTotal = MathBN.convert(itemTotals.discount_total)
const itemDiscountTaxTotal = MathBN.convert(itemTotals.discount_tax_total)
subtotal = MathBN.add(subtotal, itemSubtotal)
discountTotal = MathBN.add(discountTotal, itemDiscountTotal)
discountTaxTotal = MathBN.add(discountTaxTotal, itemDiscountTaxTotal)
itemsTotal = MathBN.add(itemsTotal, itemTotal)
itemsOriginalTotal = MathBN.add(itemsOriginalTotal, itemOriginalTotal)
itemsOriginalSubtotal = MathBN.add(itemsOriginalSubtotal, itemSubtotal)
itemsSubtotal = MathBN.add(itemsSubtotal, itemSubtotal)
itemsTaxTotal = MathBN.add(itemsTaxTotal, itemTaxTotal)
itemsOriginalTaxTotal = MathBN.add(
itemsOriginalTaxTotal,
itemOriginalTaxTotal
)
return itemTotals
})
const cartShippingMethods = shippingMethods.map((shippingMethod, index) => {
const methodTotals = Object.assign(
shippingMethod,
shippingMethodsTotals[shippingMethod.id ?? index] ?? {}
)
const methodSubtotal = MathBN.convert(methodTotals.subtotal)
const methodTotal = MathBN.convert(methodTotals.total)
const methodOriginalTotal = MathBN.convert(methodTotals.original_total)
const methodTaxTotal = MathBN.convert(methodTotals.tax_total)
const methodOriginalTaxTotal = MathBN.convert(
methodTotals.original_tax_total
)
const methodDiscountTotal = MathBN.convert(methodTotals.discount_total)
const methodDiscountTaxTotal = MathBN.convert(
methodTotals.discount_tax_total
)
shippingSubtotal = MathBN.add(shippingSubtotal, methodSubtotal)
shippingTotal = MathBN.add(shippingTotal, methodTotal)
shippingOriginalTotal = MathBN.add(
shippingOriginalTotal,
methodOriginalTotal
)
shippingOriginalSubtotal = MathBN.add(
shippingOriginalSubtotal,
methodSubtotal
)
shippingTaxTotal = MathBN.add(shippingTaxTotal, methodTaxTotal)
shippingOriginalTaxTotal = MathBN.add(
shippingOriginalTaxTotal,
methodOriginalTaxTotal
)
shippingOriginalTaxSubtotal = MathBN.add(
shippingOriginalTaxSubtotal,
methodSubtotal
)
discountTotal = MathBN.add(discountTotal, methodDiscountTotal)
discountTaxTotal = MathBN.add(discountTaxTotal, methodDiscountTaxTotal)
return methodTotals
})
const taxTotal = MathBN.add(itemsTaxTotal, shippingTaxTotal)
const originalTaxTotal = MathBN.add(
itemsOriginalTaxTotal,
shippingOriginalTaxTotal
)
// TODO: Gift Card calculations
const originalTempTotal = MathBN.add(
subtotal,
shippingOriginalTotal,
originalTaxTotal
)
const originalTotal = MathBN.sub(originalTempTotal, discountTotal)
// TODO: subtract (cart.gift_card_total + cart.gift_card_tax_total)
const tempTotal = MathBN.add(subtotal, shippingTotal, taxTotal)
const total = MathBN.sub(tempTotal, discountTotal)
const cart = { ...cartLike } as any
cart.total = new BigNumber(total)
cart.subtotal = new BigNumber(subtotal)
cart.tax_total = new BigNumber(taxTotal)
cart.discount_total = new BigNumber(discountTotal)
cart.discount_tax_total = new BigNumber(discountTaxTotal)
// cart.gift_card_total = giftCardTotal.total || 0
// cart.gift_card_tax_total = giftCardTotal.tax_total || 0
cart.original_total = new BigNumber(originalTotal)
cart.original_tax_total = new BigNumber(originalTaxTotal)
// cart.original_gift_card_total =
// cart.original_gift_card_tax_total =
if (cartLike.items) {
cart.items = cartItems
cart.item_total = new BigNumber(itemsTotal)
cart.item_subtotal = new BigNumber(itemsSubtotal)
cart.item_tax_total = new BigNumber(itemsTaxTotal)
cart.original_item_total = new BigNumber(itemsOriginalTotal)
cart.original_item_subtotal = new BigNumber(itemsOriginalSubtotal)
cart.original_item_tax_total = new BigNumber(itemsOriginalTaxTotal)
}
if (cart.shipping_methods) {
cart.shipping_methods = cartShippingMethods
cart.shipping_total = new BigNumber(shippingTotal)
cart.shipping_subtotal = new BigNumber(shippingSubtotal)
cart.shipping_tax_total = new BigNumber(shippingTaxTotal)
cart.original_shipping_tax_total = new BigNumber(shippingOriginalTaxTotal)
cart.original_shipping_tax_subtotal = new BigNumber(
shippingOriginalTaxSubtotal
)
cart.original_shipping_total = new BigNumber(shippingOriginalTotal)
}
return cart
}
+3 -237
View File
@@ -1,240 +1,6 @@
import {
BigNumberRawValue,
CartDTO,
CartShippingMethodDTO,
} from "@medusajs/types"
import { BigNumber as BigNumberJs } from "bignumber.js"
import { BigNumber } from "./big-number"
import { toBigNumberJs } from "./to-big-number-js"
export * from "./cart"
export * from "./line-item"
export * from "./math"
export * from "./promotion"
export * from "./to-big-number-js"
export * from "./shipping-method"
export * from "./transform-properties-to-bignumber"
type GetLineItemTotalsContext = {
includeTax?: boolean
taxRate?: number | null
}
interface GetShippingMethodTotalInput extends CartShippingMethodDTO {
raw_amount: BigNumberRawValue
}
interface GetItemTotalInput {
id: string
unit_price: BigNumber
quantity: number
is_tax_inclusive?: boolean
tax_total?: BigNumber
original_tax_total?: BigNumber
}
interface GetItemTotalOutput {
quantity: number
unit_price: BigNumber
subtotal: BigNumber
total: BigNumber
original_total: BigNumber
discount_total: BigNumber
tax_total: BigNumber
original_tax_total: BigNumber
}
export function getShippingMethodTotals(
shippingMethods: GetShippingMethodTotalInput[],
context: { includeTax?: boolean }
) {
const { includeTax } = context
const shippingMethodsTotals = {}
for (const shippingMethod of shippingMethods) {
shippingMethodsTotals[shippingMethod.id] = getShippingMethodTotals_(
shippingMethod,
{
includeTax,
}
)
}
return shippingMethodsTotals
}
export function getShippingMethodTotals_(
shippingMethod: GetShippingMethodTotalInput,
context: { includeTax?: boolean }
) {
const { amount, taxTotal, originalTaxTotal } = toBigNumberJs(shippingMethod, [
"amount",
"tax_total",
"original_tax_total",
])
const amountBn = new BigNumber(amount)
const totals = {
amount: amountBn,
total: amountBn,
original_total: amountBn,
subtotal: amountBn,
tax_total: new BigNumber(taxTotal),
original_tax_total: new BigNumber(originalTaxTotal),
}
const isTaxInclusive = context.includeTax ?? shippingMethod.is_tax_inclusive
if (isTaxInclusive) {
const subtotal = amount.minus(taxTotal)
totals.subtotal = new BigNumber(subtotal)
} else {
const originalTotal = amount.plus(originalTaxTotal)
const total = amount.plus(taxTotal)
totals.original_total = new BigNumber(originalTotal)
totals.total = new BigNumber(total)
}
return totals
}
export function getLineItemTotals(
items: GetItemTotalInput[],
context: GetLineItemTotalsContext
): { [itemId: string]: GetItemTotalOutput } {
const itemsTotals: { [itemId: string]: GetItemTotalOutput } = {}
for (const item of items) {
itemsTotals[item.id] = getTotalsForSingleLineItem(item, {
includeTax: context.includeTax,
})
}
return itemsTotals
}
function getTotalsForSingleLineItem(
item: GetItemTotalInput,
context: GetLineItemTotalsContext
) {
const { unitPrice, taxTotal, originalTaxTotal } = toBigNumberJs(item, [
"unit_price",
"tax_total",
"original_tax_total",
])
const subtotal = unitPrice.times(item.quantity)
const discountTotal = BigNumberJs(0)
const total = subtotal.minus(discountTotal)
const totals: GetItemTotalOutput = {
quantity: item.quantity,
unit_price: item.unit_price,
subtotal: new BigNumber(subtotal),
total: new BigNumber(total),
original_total: new BigNumber(subtotal),
discount_total: new BigNumber(discountTotal),
tax_total: new BigNumber(taxTotal),
original_tax_total: new BigNumber(originalTaxTotal),
}
const isTaxInclusive = context.includeTax ?? item.is_tax_inclusive
if (isTaxInclusive) {
const subtotal = unitPrice.times(totals.quantity).minus(originalTaxTotal)
const subtotalBn = new BigNumber(subtotal)
totals.subtotal = subtotalBn
totals.total = subtotalBn
totals.original_total = subtotalBn
} else {
const newTotal = total.plus(taxTotal)
const originalTotal = subtotal.plus(originalTaxTotal)
totals.total = new BigNumber(newTotal)
totals.original_total = new BigNumber(originalTotal)
}
return totals
}
export function decorateCartTotals(
{
shippingMethods = [],
items = [],
}: {
items: GetItemTotalInput[]
shippingMethods: GetShippingMethodTotalInput[]
},
totalsConfig: { includeTaxes?: boolean } = {}
): CartDTO {
let cart: any = {}
const includeTax = totalsConfig?.includeTaxes
const itemsTotals = getLineItemTotals(items, {
includeTax,
})
const shippingMethodsTotals = getShippingMethodTotals(shippingMethods, {
includeTax,
})
const subtotal = BigNumberJs(0)
const discountTotal = BigNumberJs(0)
const itemTaxTotal = BigNumberJs(0)
const shippingTotal = BigNumberJs(0)
const shippingTaxTotal = BigNumberJs(0)
cart.items = items.map((item) => {
const itemTotals = Object.assign(item, itemsTotals[item.id] ?? {})
const subtotal = BigNumberJs(itemTotals.subtotal.raw!.value)
const discountTotal = BigNumberJs(itemTotals.discount_total.raw!.value)
const itemTaxTotal = BigNumberJs(itemTotals.tax_total.raw!.value)
subtotal.plus(subtotal)
discountTotal.plus(discountTotal)
itemTaxTotal.plus(itemTaxTotal)
return itemTotals
})
cart.shipping_methods = shippingMethods.map((shippingMethod) => {
const methodTotals = Object.assign(
shippingMethod,
shippingMethodsTotals[shippingMethod.id] ?? {}
)
const total = BigNumberJs(methodTotals.total.raw!.value)
const methodTaxTotal = BigNumberJs(methodTotals.tax_total.raw!.value)
shippingTotal.plus(total)
shippingTaxTotal.plus(methodTaxTotal)
return methodTotals
})
const taxTotal = itemTaxTotal.plus(shippingTaxTotal)
// TODO: Discount + Gift Card calculations
// TODO: subtract (cart.gift_card_total + cart.discount_total + cart.gift_card_tax_total)
const total = subtotal.plus(shippingTotal).plus(shippingTotal).plus(taxTotal)
cart.total = new BigNumber(total)
cart.subtotal = new BigNumber(subtotal)
cart.discount_total = new BigNumber(discountTotal)
cart.item_tax_total = new BigNumber(itemTaxTotal)
cart.shipping_total = new BigNumber(shippingTotal)
cart.shipping_tax_total = new BigNumber(shippingTaxTotal)
cart.tax_total = new BigNumber(taxTotal)
// cart.discount_total = Math.round(cart.discount_total)
// cart.gift_card_total = giftCardTotal.total || 0
// cart.gift_card_tax_total = giftCardTotal.tax_total || 0
return cart as CartDTO
}
@@ -0,0 +1,126 @@
import { AdjustmentLineDTO, TaxLineDTO } from "@medusajs/types"
import { calculateAdjustmentTotal } from "../adjustment"
import { BigNumber } from "../big-number"
import { MathBN } from "../math"
import { calculateTaxTotal } from "../tax"
interface GetLineItemsTotalsContext {
includeTax?: boolean
}
export interface GetItemTotalInput {
id: string
unit_price: BigNumber
quantity: BigNumber
is_tax_inclusive?: boolean
tax_lines?: Pick<TaxLineDTO, "rate">[]
adjustments?: Pick<AdjustmentLineDTO, "amount">[]
}
export interface GetItemTotalOutput {
quantity: BigNumber
unit_price: BigNumber
subtotal: BigNumber
total: BigNumber
original_total: BigNumber
discount_total: BigNumber
discount_tax_total: BigNumber
tax_total: BigNumber
original_tax_total: BigNumber
}
export function getLineItemsTotals(
items: GetItemTotalInput[],
context: GetLineItemsTotalsContext
) {
const itemsTotals = {}
let index = 0
for (const item of items) {
itemsTotals[item.id ?? index] = getLineItemTotals(item, {
includeTax: context.includeTax || item.is_tax_inclusive,
})
index++
}
return itemsTotals
}
function getLineItemTotals(
item: GetItemTotalInput,
context: GetLineItemsTotalsContext
) {
const subtotal = MathBN.mult(item.unit_price, item.quantity)
const sumTaxRate = MathBN.sum(
...((item.tax_lines ?? []).map((taxLine) => taxLine.rate) ?? [])
)
const discountTotal = calculateAdjustmentTotal({
adjustments: item.adjustments || [],
includesTax: context.includeTax,
taxRate: sumTaxRate,
})
const discountTaxTotal = MathBN.mult(
discountTotal,
MathBN.div(sumTaxRate, 100)
)
const total = MathBN.sub(subtotal, discountTotal)
const totals: GetItemTotalOutput = {
quantity: item.quantity,
unit_price: item.unit_price,
subtotal: new BigNumber(subtotal),
total: new BigNumber(total),
original_total: new BigNumber(subtotal),
discount_total: new BigNumber(discountTotal),
discount_tax_total: new BigNumber(discountTaxTotal),
tax_total: new BigNumber(0),
original_tax_total: new BigNumber(0),
}
const taxableAmountWithDiscount = MathBN.sub(subtotal, discountTotal)
const taxableAmount = subtotal
const taxTotal = calculateTaxTotal({
taxLines: item.tax_lines || [],
includesTax: context.includeTax,
taxableAmount: taxableAmountWithDiscount,
setTotalField: "total",
})
totals.tax_total = new BigNumber(taxTotal)
const originalTaxTotal = calculateTaxTotal({
taxLines: item.tax_lines || [],
includesTax: context.includeTax,
taxableAmount,
setTotalField: "subtotal",
})
totals.original_tax_total = new BigNumber(originalTaxTotal)
const isTaxInclusive = context.includeTax ?? item.is_tax_inclusive
if (isTaxInclusive) {
totals.subtotal = new BigNumber(
MathBN.sub(
MathBN.mult(item.unit_price, totals.quantity),
originalTaxTotal
)
)
} else {
const newTotal = MathBN.add(total, totals.tax_total)
const originalTotal = MathBN.add(subtotal, totals.original_tax_total)
totals.total = new BigNumber(newTotal)
totals.original_total = new BigNumber(originalTotal)
}
return totals
}
+1 -1
View File
@@ -31,7 +31,7 @@ export class MathBN {
}
static sum(...nums: BNInput[]): BigNumberJS {
return MathBN.add(...nums)
return MathBN.add(0, ...(nums ?? [0]))
}
static sub(...nums: BNInput[]): BigNumberJS {
+58 -1
View File
@@ -1 +1,58 @@
export * from "./totals"
import {
ApplicationMethodAllocation,
ApplicationMethodType,
} from "../../promotion"
function getPromotionValueForPercentage(promotion, lineItemTotal) {
return (promotion.value / 100) * lineItemTotal
}
function getPromotionValueForFixed(promotion, lineItemTotal, lineItemsTotal) {
if (promotion.allocation === ApplicationMethodAllocation.ACROSS) {
return (lineItemTotal / lineItemsTotal) * promotion.value
}
return promotion.value
}
export function getPromotionValue(promotion, lineItemTotal, lineItemsTotal) {
if (promotion.type === ApplicationMethodType.PERCENTAGE) {
return getPromotionValueForPercentage(promotion, lineItemTotal)
}
return getPromotionValueForFixed(promotion, lineItemTotal, lineItemsTotal)
}
export function getApplicableQuantity(lineItem, maxQuantity) {
if (maxQuantity && lineItem.quantity) {
return Math.min(lineItem.quantity, maxQuantity)
}
return lineItem.quantity
}
function getLineItemUnitPrice(lineItem) {
return lineItem.subtotal / lineItem.quantity
}
export function calculateAdjustmentAmountFromPromotion(
lineItem,
promotion,
lineItemsTotal = 0
) {
const quantity = getApplicableQuantity(lineItem, promotion.max_quantity)
const lineItemTotal = getLineItemUnitPrice(lineItem) * quantity
const applicableTotal = lineItemTotal - promotion.applied_value
if (applicableTotal <= 0) {
return applicableTotal
}
const promotionValue = getPromotionValue(
promotion,
applicableTotal,
lineItemsTotal
)
return Math.min(promotionValue, applicableTotal)
}
@@ -1,58 +0,0 @@
import {
ApplicationMethodAllocation,
ApplicationMethodType,
} from "../../promotion"
function getPromotionValueForPercentage(promotion, lineItemTotal) {
return (promotion.value / 100) * lineItemTotal
}
function getPromotionValueForFixed(promotion, lineItemTotal, lineItemsTotal) {
if (promotion.allocation === ApplicationMethodAllocation.ACROSS) {
return (lineItemTotal / lineItemsTotal) * promotion.value
}
return promotion.value
}
export function getPromotionValue(promotion, lineItemTotal, lineItemsTotal) {
if (promotion.type === ApplicationMethodType.PERCENTAGE) {
return getPromotionValueForPercentage(promotion, lineItemTotal)
}
return getPromotionValueForFixed(promotion, lineItemTotal, lineItemsTotal)
}
export function getApplicableQuantity(lineItem, maxQuantity) {
if (maxQuantity && lineItem.quantity) {
return Math.min(lineItem.quantity, maxQuantity)
}
return lineItem.quantity
}
function getLineItemUnitPrice(lineItem) {
return lineItem.subtotal / lineItem.quantity
}
export function calculateAdjustmentAmountFromPromotion(
lineItem,
promotion,
lineItemsTotal = 0
) {
const quantity = getApplicableQuantity(lineItem, promotion.max_quantity)
const lineItemTotal = getLineItemUnitPrice(lineItem) * quantity
const applicableTotal = lineItemTotal - promotion.applied_value
if (applicableTotal <= 0) {
return applicableTotal
}
const promotionValue = getPromotionValue(
promotion,
applicableTotal,
lineItemsTotal
)
return Math.min(promotionValue, applicableTotal)
}
@@ -0,0 +1,132 @@
import { AdjustmentLineDTO, TaxLineDTO } from "@medusajs/types"
import { calculateAdjustmentTotal } from "../adjustment"
import { BigNumber } from "../big-number"
import { MathBN } from "../math"
import { calculateTaxTotal } from "../tax"
interface GetShippingMethodsTotalsContext {
includeTax?: boolean
}
export interface GetShippingMethodTotalInput {
id: string
amount: BigNumber
is_tax_inclusive?: boolean
tax_lines?: TaxLineDTO[]
adjustments?: Pick<AdjustmentLineDTO, "amount">[]
}
export interface GetShippingMethodTotalOutput {
amount: BigNumber
subtotal: BigNumber
total: BigNumber
original_total: BigNumber
discount_total: BigNumber
discount_tax_total: BigNumber
tax_total: BigNumber
original_tax_total: BigNumber
}
export function getShippingMethodsTotals(
shippingMethods: GetShippingMethodTotalInput[],
context: GetShippingMethodsTotalsContext
) {
const { includeTax } = context
const shippingMethodsTotals = {}
let index = 0
for (const shippingMethod of shippingMethods) {
shippingMethodsTotals[shippingMethod.id ?? index] = getShippingMethodTotals(
shippingMethod,
{
includeTax: includeTax || shippingMethod.is_tax_inclusive,
}
)
index++
}
return shippingMethodsTotals
}
export function getShippingMethodTotals(
shippingMethod: GetShippingMethodTotalInput,
context: GetShippingMethodsTotalsContext
) {
const amount = MathBN.convert(shippingMethod.amount)
const subtotal = MathBN.convert(shippingMethod.amount)
const sumTaxRate = MathBN.sum(
...(shippingMethod.tax_lines?.map((taxLine) => taxLine.rate) ?? [])
)
const discountTotal = calculateAdjustmentTotal({
adjustments: shippingMethod.adjustments || [],
includesTax: context.includeTax,
taxRate: sumTaxRate,
})
const discountTaxTotal = MathBN.mult(
discountTotal,
MathBN.div(sumTaxRate, 100)
)
const total = MathBN.sub(amount, discountTotal)
const totals: GetShippingMethodTotalOutput = {
amount: new BigNumber(amount),
subtotal: new BigNumber(subtotal),
total: new BigNumber(total),
original_total: new BigNumber(amount),
discount_total: new BigNumber(discountTotal),
discount_tax_total: new BigNumber(discountTaxTotal),
tax_total: new BigNumber(0),
original_tax_total: new BigNumber(0),
}
const taxLines = shippingMethod.tax_lines || []
const taxableAmountWithDiscount = MathBN.sub(subtotal, discountTotal)
const taxableAmount = subtotal
const taxTotal = calculateTaxTotal({
taxLines,
includesTax: context.includeTax,
taxableAmount: taxableAmountWithDiscount,
setTotalField: "total",
})
totals.tax_total = new BigNumber(taxTotal)
const originalTaxTotal = calculateTaxTotal({
taxLines,
includesTax: context.includeTax,
taxableAmount,
setTotalField: "subtotal",
})
totals.original_tax_total = new BigNumber(originalTaxTotal)
const isTaxInclusive = context.includeTax ?? shippingMethod.is_tax_inclusive
if (isTaxInclusive) {
const subtotal = MathBN.add(shippingMethod.amount, taxTotal)
totals.subtotal = new BigNumber(subtotal)
} else {
const originalTotal = MathBN.add(
shippingMethod.amount,
totals.original_tax_total
)
const total = MathBN.add(totals.total, totals.tax_total)
totals.total = new BigNumber(total)
totals.original_total = new BigNumber(originalTotal)
}
return totals
}
+33
View File
@@ -0,0 +1,33 @@
import { BigNumberInput, TaxLineDTO } from "@medusajs/types"
import { BigNumber } from "../big-number"
import { MathBN } from "../math"
export function calculateTaxTotal({
taxLines,
includesTax,
taxableAmount,
setTotalField,
}: {
taxLines: Pick<TaxLineDTO, "rate">[]
includesTax?: boolean
taxableAmount: BigNumberInput
setTotalField?: string
}) {
let taxTotal = MathBN.convert(0)
for (const taxLine of taxLines) {
const rate = MathBN.div(taxLine.rate, 100)
let taxAmount = MathBN.mult(taxableAmount, rate)
if (includesTax) {
taxAmount = MathBN.div(taxAmount, MathBN.add(1, rate))
}
if (setTotalField) {
;(taxLine as any)[setTotalField] = new BigNumber(taxAmount)
}
taxTotal = MathBN.add(taxTotal, taxAmount)
}
return taxTotal
}
@@ -1,28 +0,0 @@
import { BigNumberInput } from "@medusajs/types"
import { BigNumber as BigNumberJs } from "bignumber.js"
import { isDefined, toCamelCase } from "../common"
import { BigNumber } from "./big-number"
type InputEntity<T, V extends string> = { [key in V]?: InputEntityField }
type InputEntityField = number | string | BigNumber
type Camelize<V extends string> = V extends `${infer A}_${infer B}`
? `${A}${Camelize<Capitalize<B>>}`
: V
type Output<V extends string> = { [key in Camelize<V>]: BigNumberJs }
export function toBigNumberJs<T, V extends string>(
entity: InputEntity<T, V>,
fields: V[]
): Output<V> {
return fields.reduce((acc, field: string) => {
const camelCased = toCamelCase(field)
let val: BigNumberInput = 0
if (isDefined(entity[field])) {
const entityField = entity[field]
val = (entityField?.raw?.value ?? entityField) as number | string
}
acc[camelCased] = new BigNumberJs(val)
return acc
}, {} as Output<V>)
}