diff --git a/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts b/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts index ac66ef2d20eb7..3148d14b2d597 100644 --- a/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts +++ b/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts @@ -481,14 +481,14 @@ describe("Cart Module Service", () => { const error = await service .addLineItems(createdCart.id, [ { - quantity: 1, + unit_price: 10, title: "test", }, ] as any) .catch((e) => e) expect(error.message).toContain( - "Value for LineItem.unit_price is required, 'undefined' found" + "Value for LineItem.quantity is required, 'undefined' found" ) }) @@ -503,14 +503,14 @@ describe("Cart Module Service", () => { .addLineItems([ { cart_id: createdCart.id, - quantity: 1, + unit_price: 10, title: "test", }, ] as any) .catch((e) => e) expect(error.message).toContain( - "Value for LineItem.unit_price is required, 'undefined' found" + "Value for LineItem.quantity is required, 'undefined' found" ) }) }) diff --git a/packages/cart/src/migrations/CartModuleSetup20240122122952.ts b/packages/cart/src/migrations/CartModuleSetup20240122122952.ts index cea797ef6881d..c178fb50c10a8 100644 --- a/packages/cart/src/migrations/CartModuleSetup20240122122952.ts +++ b/packages/cart/src/migrations/CartModuleSetup20240122122952.ts @@ -80,7 +80,9 @@ export class CartModuleSetup20240122122952 extends Migration { "is_discountable" BOOLEAN NOT NULL DEFAULT TRUE, "is_tax_inclusive" BOOLEAN NOT NULL DEFAULT FALSE, "compare_at_unit_price" NUMERIC NULL, + "raw_compare_at_unit_price" JSONB NULL, "unit_price" NUMERIC NOT NULL, + "raw_unit_price" JSONB NOT NULL, "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(), "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT "cart_line_item_pkey" PRIMARY KEY ("id"), @@ -134,6 +136,7 @@ export class CartModuleSetup20240122122952 extends Migration { "name" TEXT NOT NULL, "description" JSONB NULL, "amount" NUMERIC NOT NULL, + "raw_amount" JSONB NOT NULL, "is_tax_inclusive" BOOLEAN NOT NULL DEFAULT FALSE, "shipping_option_id" TEXT NULL, "data" JSONB NULL, diff --git a/packages/cart/src/models/line-item.ts b/packages/cart/src/models/line-item.ts index 5ab63a65651a4..2cc4dc87acdce 100644 --- a/packages/cart/src/models/line-item.ts +++ b/packages/cart/src/models/line-item.ts @@ -1,9 +1,9 @@ -import { DAL } from "@medusajs/types" -import { generateEntityId } from "@medusajs/utils" +import { BigNumberRawValue, DAL } from "@medusajs/types" +import { BigNumber, generateEntityId } from "@medusajs/utils" import { BeforeCreate, + BeforeUpdate, Cascade, - Check, Collection, Entity, ManyToOne, @@ -109,12 +109,16 @@ export default class LineItem { is_tax_inclusive = false @Property({ columnType: "numeric", nullable: true }) - compare_at_unit_price: number | null = null + compare_at_unit_price?: BigNumber | number | null = null - // TODO: Rework when BigNumber has been introduced - @Property({ columnType: "numeric", serializer: Number }) - @Check({ expression: "unit_price >= 0" }) // TODO: Validate that numeric types work with the expression - unit_price: number + @Property({ columnType: "jsonb", nullable: true }) + raw_compare_at_unit_price: BigNumberRawValue | null = null + + @Property({ columnType: "numeric" }) + unit_price: BigNumber | number + + @Property({ columnType: "jsonb" }) + raw_unit_price: BigNumberRawValue @OneToMany(() => LineItemTaxLine, (taxLine) => taxLine.item, { cascade: [Cascade.REMOVE], @@ -144,10 +148,28 @@ export default class LineItem { @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "cali") + + const val = new BigNumber(this.raw_unit_price ?? this.unit_price) + + this.unit_price = val.numeric + this.raw_unit_price = val.raw! + } + + @BeforeUpdate() + onUpdate() { + const val = new BigNumber(this.raw_unit_price ?? this.unit_price) + + this.unit_price = val.numeric + this.raw_unit_price = val.raw as BigNumberRawValue } @OnInit() onInit() { this.id = generateEntityId(this.id, "cali") + + const val = new BigNumber(this.raw_unit_price ?? this.unit_price) + + this.unit_price = val.numeric + this.raw_unit_price = val.raw! } } diff --git a/packages/cart/src/models/shipping-method.ts b/packages/cart/src/models/shipping-method.ts index c3a2ed448a421..9daa2a1580c8f 100644 --- a/packages/cart/src/models/shipping-method.ts +++ b/packages/cart/src/models/shipping-method.ts @@ -1,4 +1,5 @@ -import { generateEntityId } from "@medusajs/utils" +import { BigNumberRawValue } from "@medusajs/types" +import { BigNumber, generateEntityId } from "@medusajs/utils" import { BeforeCreate, Cascade, @@ -11,6 +12,7 @@ import { PrimaryKey, Property, } from "@mikro-orm/core" +import { BeforeUpdate } from "typeorm" import Cart from "./cart" import ShippingMethodAdjustment from "./shipping-method-adjustment" import ShippingMethodTaxLine from "./shipping-method-tax-line" @@ -37,8 +39,11 @@ export default class ShippingMethod { @Property({ columnType: "jsonb", nullable: true }) description: string | null = null - @Property({ columnType: "numeric", serializer: Number }) - amount: number + @Property({ columnType: "numeric" }) + amount: BigNumber | number + + @Property({ columnType: "jsonb" }) + raw_amount: BigNumberRawValue @Property({ columnType: "boolean" }) is_tax_inclusive = false @@ -92,10 +97,28 @@ export default class ShippingMethod { @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "casm") + + const val = new BigNumber(this.raw_amount ?? this.amount) + + this.amount = val.numeric + this.raw_amount = val.raw! + } + + @BeforeUpdate() + onUpdate() { + const val = new BigNumber(this.raw_amount ?? this.amount) + + this.amount = val.numeric + this.raw_amount = val.raw as BigNumberRawValue } @OnInit() onInit() { this.id = generateEntityId(this.id, "casm") + + const val = new BigNumber(this.raw_amount ?? this.amount) + + this.amount = val.numeric + this.raw_amount = val.raw! } } diff --git a/packages/cart/src/services/cart-module.ts b/packages/cart/src/services/cart-module.ts index 79274eed6b21d..5d404950cbbd1 100644 --- a/packages/cart/src/services/cart-module.ts +++ b/packages/cart/src/services/cart-module.ts @@ -11,11 +11,11 @@ import { import { InjectManager, InjectTransactionManager, - isObject, - isString, MedusaContext, MedusaError, ModulesSdkUtils, + isObject, + isString, } from "@medusajs/utils" import { Address, diff --git a/packages/cart/src/services/index.ts b/packages/cart/src/services/index.ts index 2ed2053ffcb36..440fec6e50ba9 100644 --- a/packages/cart/src/services/index.ts +++ b/packages/cart/src/services/index.ts @@ -1 +1 @@ -export { default as CartModuleService } from "./cart-module" +export { default as CartModuleService } from "./cart-module"; diff --git a/packages/cart/src/types/line-item.ts b/packages/cart/src/types/line-item.ts index b7fd239713a72..caf407251057e 100644 --- a/packages/cart/src/types/line-item.ts +++ b/packages/cart/src/types/line-item.ts @@ -26,7 +26,7 @@ interface PartialUpsertLineItemDTO { export interface CreateLineItemDTO extends PartialUpsertLineItemDTO { title: string quantity: number - unit_price: number + unit_price: number | string cart_id: string } diff --git a/packages/medusa/src/services/new-totals.ts b/packages/medusa/src/services/new-totals.ts index b81215a6eb900..9df49230b0cc7 100644 --- a/packages/medusa/src/services/new-totals.ts +++ b/packages/medusa/src/services/new-totals.ts @@ -79,8 +79,8 @@ export default class NewTotalsService extends TransactionBaseService { /** * Calculate and return the items totals for either the legacy calculation or the new calculation - * @param items - * @param param1 + * @param items + * @param param1 */ async getLineItemTotals( items: LineItem | LineItem[], @@ -136,9 +136,9 @@ export default class NewTotalsService extends TransactionBaseService { /** * Calculate and return the totals for an item - * @param item - * @param param1 - * @returns + * @param item + * @param param1 + * @returns */ protected async getLineItemTotals_( item: LineItem, diff --git a/packages/payment/src/models/capture.ts b/packages/payment/src/models/capture.ts index f0d5be2a25cf8..643cc01acf918 100644 --- a/packages/payment/src/models/capture.ts +++ b/packages/payment/src/models/capture.ts @@ -1,3 +1,4 @@ +import { generateEntityId } from "@medusajs/utils" import { BeforeCreate, Entity, @@ -7,8 +8,6 @@ import { PrimaryKey, Property, } from "@mikro-orm/core" - -import { generateEntityId } from "@medusajs/utils" import Payment from "./payment" type OptionalCaptureProps = "created_at" diff --git a/packages/types/package.json b/packages/types/package.json index 10d22d6f62395..3e342990f9486 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -22,6 +22,7 @@ "license": "MIT", "devDependencies": { "awilix": "^8.0.0", + "bignumber.js": "^9.1.2", "cross-env": "^5.2.1", "ioredis": "^5.2.5", "rimraf": "^5.0.1", diff --git a/packages/types/src/cart/common.ts b/packages/types/src/cart/common.ts index b83c2b7d6d514..2fb47b3fe2417 100644 --- a/packages/types/src/cart/common.ts +++ b/packages/types/src/cart/common.ts @@ -261,7 +261,23 @@ export interface CartShippingMethodDTO { discount_tax_total: number } -export interface CartLineItemDTO { +export interface CartLineItemTotalsDTO { + original_total: number + original_subtotal: number + original_tax_total: number + + item_total: number + item_subtotal: number + item_tax_total: number + + total: number + subtotal: number + tax_total: number + discount_total: number + discount_tax_total: number +} + +export interface CartLineItemDTO extends CartLineItemTotalsDTO { /** * The ID of the line item. */ @@ -384,20 +400,6 @@ export interface CartLineItemDTO { * When the line item was updated. */ updated_at?: Date - - original_total: number - original_subtotal: number - original_tax_total: number - - item_total: number - item_subtotal: number - item_tax_total: number - - total: number - subtotal: number - tax_total: number - discount_total: number - discount_tax_total: number } export interface CartDTO { @@ -478,8 +480,12 @@ export interface CartDTO { subtotal: number tax_total: number discount_total: number + raw_discount_total: any discount_tax_total: number + gift_card_total: number + gift_card_tax_total: number + shipping_total: number shipping_subtotal: number shipping_tax_total: number diff --git a/packages/types/src/cart/mutations.ts b/packages/types/src/cart/mutations.ts index 25d0dc71728d1..dc4ea41016531 100644 --- a/packages/types/src/cart/mutations.ts +++ b/packages/types/src/cart/mutations.ts @@ -156,7 +156,7 @@ export interface CreateLineItemDTO { is_tax_inclusive?: boolean compare_at_unit_price?: number - unit_price: number + unit_price: number | string tax_lines?: CreateTaxLineDTO[] adjustments?: CreateAdjustmentDTO[] diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 5d7d823b6e9e7..ab086b109dab3 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -26,6 +26,7 @@ export * from "./sales-channel" export * from "./search" export * from "./shared-context" export * from "./stock-location" +export * from "./totals" export * from "./transaction-base" export * from "./user" export * from "./workflow" diff --git a/packages/types/src/totals/big-number.ts b/packages/types/src/totals/big-number.ts new file mode 100644 index 0000000000000..1a0740d154577 --- /dev/null +++ b/packages/types/src/totals/big-number.ts @@ -0,0 +1,12 @@ +import BigNumber from "bignumber.js" + +export type BigNumberRawValue = { + value: string | number + [key: string]: unknown +} + +export type BigNumberRawPriceInput = + | BigNumberRawValue + | number + | string + | BigNumber diff --git a/packages/types/src/totals/index.ts b/packages/types/src/totals/index.ts new file mode 100644 index 0000000000000..0ed2463b75015 --- /dev/null +++ b/packages/types/src/totals/index.ts @@ -0,0 +1,2 @@ +export * from "./big-number"; + diff --git a/packages/utils/package.json b/packages/utils/package.json index 8ffbf885ea8a6..9d9fcbdbe8532 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -35,6 +35,7 @@ "@mikro-orm/migrations": "5.9.7", "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.1", + "bignumber.js": "^9.1.2", "knex": "2.4.2", "ulid": "^2.3.0" }, diff --git a/packages/utils/src/common/index.ts b/packages/utils/src/common/index.ts index 2c8a76c74eac8..85e47879eefa7 100644 --- a/packages/utils/src/common/index.ts +++ b/packages/utils/src/common/index.ts @@ -14,6 +14,7 @@ export * from "./get-iso-string-from-date" export * from "./get-selects-and-relations-from-object-array" export * from "./group-by" export * from "./handle-postgres-database-error" +export * from "./is-big-number" export * from "./is-date" export * from "./is-defined" export * from "./is-email" diff --git a/packages/utils/src/common/is-big-number.ts b/packages/utils/src/common/is-big-number.ts new file mode 100644 index 0000000000000..8335b649ed93c --- /dev/null +++ b/packages/utils/src/common/is-big-number.ts @@ -0,0 +1,6 @@ +import { BigNumberRawValue } from "@medusajs/types" +import { isObject } from "./is-object" + +export function isBigNumber(obj: any): obj is BigNumberRawValue { + return isObject(obj) && "value" in obj +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 814ab3a098775..bf8953ef387d6 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -16,5 +16,7 @@ export * from "./product" export * from "./promotion" export * from "./search" export * from "./shipping" +export * from "./totals" +export * from "./totals/big-number" export const MedusaModuleType = Symbol.for("MedusaModule") diff --git a/packages/utils/src/totals/__tests__/big-number.spec.ts b/packages/utils/src/totals/__tests__/big-number.spec.ts new file mode 100644 index 0000000000000..7fd7702f4d040 --- /dev/null +++ b/packages/utils/src/totals/__tests__/big-number.spec.ts @@ -0,0 +1,36 @@ +import { BigNumber as BN } from "bignumber.js" +import { BigNumber } from "../big-number" + +describe("BigNumber", function () { + describe("constructor", function () { + it("should set and return number", function () { + const number = new BigNumber(42) + expect(JSON.stringify(number)).toEqual(JSON.stringify(42)) + }) + + it("should set BigNumber and return number", function () { + const number = new BigNumber({ + value: "42", + }) + expect(JSON.stringify(number)).toEqual(JSON.stringify(42)) + }) + + it("should set string and return number", function () { + const number = new BigNumber("42") + expect(JSON.stringify(number)).toEqual(JSON.stringify(42)) + }) + + it("should set bignumber.js and return number", function () { + const bn = new BN("42") + const number = new BigNumber(bn) + expect(JSON.stringify(number)).toEqual(JSON.stringify(42)) + }) + + it("should throw if not correct type", function () { + // @ts-ignore + expect(() => new BigNumber([])).toThrow( + "Invalid BigNumber value. Should be one of: string, number, BigNumber (bignumber.js), BigNumberRawValue" + ) + }) + }) +}) diff --git a/packages/utils/src/totals/big-number.ts b/packages/utils/src/totals/big-number.ts new file mode 100644 index 0000000000000..623d967eff1cd --- /dev/null +++ b/packages/utils/src/totals/big-number.ts @@ -0,0 +1,97 @@ +import { BigNumberRawPriceInput, BigNumberRawValue } from "@medusajs/types" +import { BigNumber as BigNumberJS } from "bignumber.js" +import { isBigNumber, isString } from "../common" + +export class BigNumber { + static DEFAULT_PRECISION = 20 + + private numeric_: number + private raw_?: BigNumberRawValue + + constructor(rawPrice: BigNumberRawPriceInput) { + this.setRawPriceOrThrow(rawPrice) + } + + setRawPriceOrThrow(rawPrice: BigNumberRawPriceInput) { + if (BigNumberJS.isBigNumber(rawPrice)) { + /** + * Example: + * const bnUnitPrice = new BigNumberJS("10.99") + * const unitPrice = new BigNumber(bnUnitPrice) + */ + this.numeric_ = rawPrice.toNumber() + this.raw_ = { + value: rawPrice.toPrecision(BigNumber.DEFAULT_PRECISION), + } + } else if (isString(rawPrice)) { + /** + * Example: const unitPrice = "1234.1234" + */ + const bigNum = new BigNumberJS(rawPrice) + + this.numeric_ = bigNum.toNumber() + this.raw_ = this.raw_ = { + value: bigNum.toPrecision(BigNumber.DEFAULT_PRECISION), + } + } else if (isBigNumber(rawPrice)) { + /** + * Example: const unitPrice = { value: "1234.1234" } + */ + this.numeric_ = BigNumberJS(rawPrice.value).toNumber() + + this.raw_ = { + ...rawPrice, + } + } else if (typeof rawPrice === `number` && !Number.isNaN(rawPrice)) { + /** + * Example: const unitPrice = 1234 + */ + this.numeric_ = rawPrice as number + + this.raw_ = { + value: BigNumberJS(rawPrice as number).toString(), + } + } else { + throw new Error( + "Invalid BigNumber value. Should be one of: string, number, BigNumber (bignumber.js), BigNumberRawValue" + ) + } + } + + get numeric(): number { + let raw = this.raw_ as BigNumberRawValue + if (raw) { + return new BigNumberJS(raw.value).toNumber() + } else { + return this.numeric_ + } + } + + set numeric(value: BigNumberRawPriceInput) { + const newValue = new BigNumber(value) + this.numeric_ = newValue.numeric_ + this.raw_ = newValue.raw_ + } + + get raw(): BigNumberRawValue | undefined { + return this.raw_ + } + + set raw(rawValue: BigNumberRawPriceInput) { + const newValue = new BigNumber(rawValue) + this.numeric_ = newValue.numeric_ + this.raw_ = newValue.raw_ + } + + toJSON() { + return this.raw_ + ? new BigNumberJS(this.raw_.value).toNumber() + : this.numeric_ + } + + valueOf() { + return this.raw_ + ? new BigNumberJS(this.raw_.value).toNumber() + : this.numeric_ + } +} diff --git a/packages/utils/src/totals/index.ts b/packages/utils/src/totals/index.ts new file mode 100644 index 0000000000000..260851b0b2aa5 --- /dev/null +++ b/packages/utils/src/totals/index.ts @@ -0,0 +1,235 @@ +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" + +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 +} diff --git a/packages/utils/src/totals/to-big-number-js.ts b/packages/utils/src/totals/to-big-number-js.ts new file mode 100644 index 0000000000000..15c2721383e2f --- /dev/null +++ b/packages/utils/src/totals/to-big-number-js.ts @@ -0,0 +1,31 @@ +import { BigNumberRawPriceInput } from "@medusajs/types" +import { BigNumber as BigNumberJs } from "bignumber.js" +import { isDefined, toCamelCase } from "../common" +import { BigNumber } from "./big-number" + +type InputEntity = { [key in V]?: InputEntityField } +type InputEntityField = number | string | BigNumber + +type Camelize = V extends `${infer A}_${infer B}` + ? `${A}${Camelize>}` + : V + +type Output = { [key in Camelize]: BigNumberJs } + +export function toBigNumberJs( + entity: InputEntity, + fields: V[] +): Output { + return fields.reduce((acc, field: string) => { + const camelCased = toCamelCase(field) + let val: BigNumberRawPriceInput = 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) +} diff --git a/yarn.lock b/yarn.lock index 2203412df7888..698f1efd27f3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8707,6 +8707,7 @@ __metadata: resolution: "@medusajs/types@workspace:packages/types" dependencies: awilix: ^8.0.0 + bignumber.js: ^9.1.2 cross-env: ^5.2.1 ioredis: ^5.2.5 rimraf: ^5.0.1 @@ -8842,6 +8843,7 @@ __metadata: "@mikro-orm/postgresql": 5.9.7 "@types/express": ^4.17.17 awilix: ^8.0.1 + bignumber.js: ^9.1.2 cross-env: ^5.2.1 express: ^4.18.2 jest: ^29.6.3 @@ -21419,6 +21421,13 @@ __metadata: languageName: node linkType: hard +"bignumber.js@npm:^9.1.2": + version: 9.1.2 + resolution: "bignumber.js@npm:9.1.2" + checksum: e17786545433f3110b868725c449fa9625366a6e675cd70eb39b60938d6adbd0158cb4b3ad4f306ce817165d37e63f4aa3098ba4110db1d9a3b9f66abfbaf10d + languageName: node + linkType: hard + "binary-extensions@npm:^1.0.0": version: 1.13.1 resolution: "binary-extensions@npm:1.13.1"