From b6879f017a117c984256d0ea479279777023c84f Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Sun, 7 Apr 2024 16:50:40 +0200 Subject: [PATCH] feat: Totals calculation (#6980) --- .../cart/src/types/line-item-adjustment.ts | 4 +- packages/cart/src/types/line-item.ts | 8 +- .../src/types/shipping-method-adjustment.ts | 6 +- packages/cart/src/types/shipping-method.ts | 6 +- .../cart/steps/create-payment-collection.ts | 4 +- .../steps/create-payment-session.ts | 8 +- .../create-product-variants-prepare-data.ts | 12 +- .../product/create-product-variants.ts | 8 +- .../product/create-products-prepare-data.ts | 15 +- .../update-product-variants-prepare-data.ts | 13 +- .../product/update-products-prepare-data.ts | 13 +- .../handlers/product/upsert-variant-prices.ts | 8 +- .../src/payment/steps/capture-payment.ts | 4 +- .../src/payment/steps/refund-payment.ts | 4 +- .../src/payment/workflows/capture-payment.ts | 4 +- .../src/payment/workflows/refund-payment.ts | 3 +- .../__tests__/create-order.ts | 9 +- .../integration-tests/__tests__/order-edit.ts | 21 +- .../__tests__/order-items-shipping.spec.ts | 59 +- .../src/services/order-module-service.ts | 2 +- packages/order/src/utils/transform-order.ts | 4 +- .../payment-stripe/src/core/stripe-base.ts | 9 +- .../payment/src/services/payment-module.ts | 31 +- packages/types/src/cart/common.ts | 115 ++-- packages/types/src/cart/mutations.ts | 27 +- packages/types/src/cart/workflows.ts | 11 +- packages/types/src/order/common.ts | 104 ++- packages/types/src/order/mutations.ts | 10 +- packages/types/src/payment/common.ts | 27 +- packages/types/src/payment/mutations.ts | 17 +- packages/types/src/payment/provider.ts | 11 +- .../types/src/pricing/common/money-amount.ts | 19 +- .../types/src/pricing/common/price-set.ts | 17 +- packages/types/src/pricing/common/price.ts | 19 +- packages/types/src/totals/big-number.ts | 2 + packages/types/src/totals/index.ts | 27 +- packages/utils/src/totals/__tests__/totals.ts | 637 ++++++++++++++++++ packages/utils/src/totals/adjustment/index.ts | 40 ++ packages/utils/src/totals/big-number.ts | 2 +- packages/utils/src/totals/cart/index.ts | 227 +++++++ packages/utils/src/totals/index.ts | 240 +------ packages/utils/src/totals/line-item/index.ts | 126 ++++ packages/utils/src/totals/math.ts | 2 +- packages/utils/src/totals/promotion/index.ts | 59 +- packages/utils/src/totals/promotion/totals.ts | 58 -- .../utils/src/totals/shipping-method/index.ts | 132 ++++ packages/utils/src/totals/tax/index.ts | 33 + packages/utils/src/totals/to-big-number-js.ts | 28 - 48 files changed, 1674 insertions(+), 571 deletions(-) create mode 100644 packages/utils/src/totals/__tests__/totals.ts create mode 100644 packages/utils/src/totals/adjustment/index.ts create mode 100644 packages/utils/src/totals/cart/index.ts create mode 100644 packages/utils/src/totals/line-item/index.ts delete mode 100644 packages/utils/src/totals/promotion/totals.ts create mode 100644 packages/utils/src/totals/shipping-method/index.ts create mode 100644 packages/utils/src/totals/tax/index.ts delete mode 100644 packages/utils/src/totals/to-big-number-js.ts diff --git a/packages/cart/src/types/line-item-adjustment.ts b/packages/cart/src/types/line-item-adjustment.ts index bb67befede..076ad8ea03 100644 --- a/packages/cart/src/types/line-item-adjustment.ts +++ b/packages/cart/src/types/line-item-adjustment.ts @@ -1,6 +1,8 @@ +import { BigNumberInput } from "@medusajs/types" + export interface CreateLineItemAdjustmentDTO { item_id: string - amount: number + amount: BigNumberInput code?: string description?: string promotion_id?: string diff --git a/packages/cart/src/types/line-item.ts b/packages/cart/src/types/line-item.ts index caf4072510..82b7f5ebc0 100644 --- a/packages/cart/src/types/line-item.ts +++ b/packages/cart/src/types/line-item.ts @@ -1,3 +1,5 @@ +import { BigNumberInput } from "@medusajs/types" + interface PartialUpsertLineItemDTO { subtitle?: string thumbnail?: string @@ -20,13 +22,13 @@ interface PartialUpsertLineItemDTO { is_discountable?: boolean is_tax_inclusive?: boolean - compare_at_unit_price?: number + compare_at_unit_price?: BigNumberInput } export interface CreateLineItemDTO extends PartialUpsertLineItemDTO { title: string - quantity: number - unit_price: number | string + quantity: BigNumberInput + unit_price: BigNumberInput cart_id: string } diff --git a/packages/cart/src/types/shipping-method-adjustment.ts b/packages/cart/src/types/shipping-method-adjustment.ts index e6e5c945db..bb8ec468dc 100644 --- a/packages/cart/src/types/shipping-method-adjustment.ts +++ b/packages/cart/src/types/shipping-method-adjustment.ts @@ -1,7 +1,9 @@ +import { BigNumberInput } from "@medusajs/types" + export interface CreateShippingMethodAdjustmentDTO { shipping_method_id: string code: string - amount: number + amount: BigNumberInput description?: string promotion_id?: string provider_id?: string @@ -10,7 +12,7 @@ export interface CreateShippingMethodAdjustmentDTO { export interface UpdateShippingMethodAdjustmentDTO { id: string code?: string - amount?: number + amount?: BigNumberInput description?: string promotion_id?: string provider_id?: string diff --git a/packages/cart/src/types/shipping-method.ts b/packages/cart/src/types/shipping-method.ts index 6d15473f1b..e693467ab6 100644 --- a/packages/cart/src/types/shipping-method.ts +++ b/packages/cart/src/types/shipping-method.ts @@ -1,13 +1,15 @@ +import { BigNumberInput } from "@medusajs/types" + export interface CreateShippingMethodDTO { name: string shippingMethod_id: string - amount: number + amount: BigNumberInput data?: Record } export interface UpdateShippingMethodDTO { id: string name?: string - amount?: number + amount?: BigNumberInput data?: Record } diff --git a/packages/core-flows/src/definition/cart/steps/create-payment-collection.ts b/packages/core-flows/src/definition/cart/steps/create-payment-collection.ts index dd26222ddc..4495859fb8 100644 --- a/packages/core-flows/src/definition/cart/steps/create-payment-collection.ts +++ b/packages/core-flows/src/definition/cart/steps/create-payment-collection.ts @@ -1,11 +1,11 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { IPaymentModuleService } from "@medusajs/types" +import { BigNumberInput, IPaymentModuleService } from "@medusajs/types" import { StepResponse, createStep } from "@medusajs/workflows-sdk" type StepInput = { region_id: string currency_code: string - amount: number + amount: BigNumberInput metadata?: Record } diff --git a/packages/core-flows/src/definition/payment-collection/steps/create-payment-session.ts b/packages/core-flows/src/definition/payment-collection/steps/create-payment-session.ts index 471e59d416..f2bcf6efda 100644 --- a/packages/core-flows/src/definition/payment-collection/steps/create-payment-session.ts +++ b/packages/core-flows/src/definition/payment-collection/steps/create-payment-session.ts @@ -1,11 +1,15 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { IPaymentModuleService, PaymentProviderContext } from "@medusajs/types" +import { + BigNumberInput, + IPaymentModuleService, + PaymentProviderContext, +} from "@medusajs/types" import { StepResponse, createStep } from "@medusajs/workflows-sdk" interface StepInput { payment_collection_id: string provider_id: string - amount: number + amount: BigNumberInput currency_code: string context?: PaymentProviderContext data?: Record diff --git a/packages/core-flows/src/handlers/product/create-product-variants-prepare-data.ts b/packages/core-flows/src/handlers/product/create-product-variants-prepare-data.ts index 7bae20b7d8..08c4d37788 100644 --- a/packages/core-flows/src/handlers/product/create-product-variants-prepare-data.ts +++ b/packages/core-flows/src/handlers/product/create-product-variants-prepare-data.ts @@ -1,13 +1,12 @@ -import { ProductWorkflow, WorkflowTypes } from "@medusajs/types" - +import { BigNumberInput, ProductWorkflow, WorkflowTypes } from "@medusajs/types" import { WorkflowArguments } from "@medusajs/workflows-sdk" type VariantPrice = { region_id?: string currency_code?: string - amount: number - min_quantity?: number - max_quantity?: number + amount: BigNumberInput + min_quantity?: BigNumberInput + max_quantity?: BigNumberInput } export type CreateProductVariantsPreparedData = { @@ -22,7 +21,8 @@ export type CreateProductVariantsPreparedData = { export async function createProductVariantsPrepareData({ container, data, -}: WorkflowArguments): Promise { +}: // eslint-disable-next-line max-len +WorkflowArguments): Promise { const productVariants: ProductWorkflow.CreateProductVariantsInputDTO[] = data.productVariants || [] diff --git a/packages/core-flows/src/handlers/product/create-product-variants.ts b/packages/core-flows/src/handlers/product/create-product-variants.ts index 40e0bd0d94..11ebae9030 100644 --- a/packages/core-flows/src/handlers/product/create-product-variants.ts +++ b/packages/core-flows/src/handlers/product/create-product-variants.ts @@ -1,13 +1,13 @@ import { Modules, ModulesDefinition } from "@medusajs/modules-sdk" -import { ProductTypes } from "@medusajs/types" +import { BigNumberInput, ProductTypes } from "@medusajs/types" import { WorkflowArguments } from "@medusajs/workflows-sdk" type VariantPrice = { region_id?: string currency_code?: string - amount: number - min_quantity?: number - max_quantity?: number + amount: BigNumberInput + min_quantity?: BigNumberInput + max_quantity?: BigNumberInput } type HandlerInput = { diff --git a/packages/core-flows/src/handlers/product/create-products-prepare-data.ts b/packages/core-flows/src/handlers/product/create-products-prepare-data.ts index 6cb312668f..1ce68e6506 100644 --- a/packages/core-flows/src/handlers/product/create-products-prepare-data.ts +++ b/packages/core-flows/src/handlers/product/create-products-prepare-data.ts @@ -1,8 +1,13 @@ -import { ProductTypes, SalesChannelTypes, WorkflowTypes } from "@medusajs/types" +import { + BigNumberInput, + ProductTypes, + SalesChannelTypes, + WorkflowTypes, +} from "@medusajs/types" import { FeatureFlagUtils, - kebabCase, ShippingProfileUtils, + kebabCase, } from "@medusajs/utils" import { WorkflowArguments } from "@medusajs/workflows-sdk" @@ -14,9 +19,9 @@ type VariantIndexAndPrices = { prices: { region_id?: string currency_code?: string - amount: number - min_quantity?: number - max_quantity?: number + amount: BigNumberInput + min_quantity?: BigNumberInput + max_quantity?: BigNumberInput }[] } diff --git a/packages/core-flows/src/handlers/product/update-product-variants-prepare-data.ts b/packages/core-flows/src/handlers/product/update-product-variants-prepare-data.ts index c724d9788d..e0697d94e4 100644 --- a/packages/core-flows/src/handlers/product/update-product-variants-prepare-data.ts +++ b/packages/core-flows/src/handlers/product/update-product-variants-prepare-data.ts @@ -1,14 +1,19 @@ import { Modules, ModulesDefinition } from "@medusajs/modules-sdk" -import { ProductTypes, ProductWorkflow, WorkflowTypes } from "@medusajs/types" +import { + BigNumberInput, + ProductTypes, + ProductWorkflow, + WorkflowTypes, +} from "@medusajs/types" import { MedusaV2Flag } from "@medusajs/utils" import { WorkflowArguments } from "@medusajs/workflows-sdk" type VariantPrice = { region_id?: string currency_code?: string - amount: number - min_quantity?: number - max_quantity?: number + amount: BigNumberInput + min_quantity?: BigNumberInput + max_quantity?: BigNumberInput } export type UpdateProductVariantsPreparedData = { diff --git a/packages/core-flows/src/handlers/product/update-products-prepare-data.ts b/packages/core-flows/src/handlers/product/update-products-prepare-data.ts index 87c4457f5b..e1a963e5b3 100644 --- a/packages/core-flows/src/handlers/product/update-products-prepare-data.ts +++ b/packages/core-flows/src/handlers/product/update-products-prepare-data.ts @@ -1,4 +1,9 @@ -import { ProductDTO, SalesChannelDTO, WorkflowTypes } from "@medusajs/types" +import { + BigNumberInput, + ProductDTO, + SalesChannelDTO, + WorkflowTypes, +} from "@medusajs/types" import { MedusaV2Flag } from "@medusajs/utils" import { WorkflowArguments } from "@medusajs/workflows-sdk" @@ -9,9 +14,9 @@ type ProductWithSalesChannelsDTO = ProductDTO & { type VariantPrice = { region_id?: string currency_code?: string - amount: number - min_quantity?: number - max_quantity?: number + amount: BigNumberInput + min_quantity?: BigNumberInput + max_quantity?: BigNumberInput } export type UpdateProductsPreparedData = { diff --git a/packages/core-flows/src/handlers/product/upsert-variant-prices.ts b/packages/core-flows/src/handlers/product/upsert-variant-prices.ts index ac54cf7f5c..c681c60031 100644 --- a/packages/core-flows/src/handlers/product/upsert-variant-prices.ts +++ b/packages/core-flows/src/handlers/product/upsert-variant-prices.ts @@ -1,4 +1,4 @@ -import { PricingTypes } from "@medusajs/types" +import { BigNumberInput, PricingTypes } from "@medusajs/types" import { MedusaV2Flag } from "@medusajs/utils" import { WorkflowArguments } from "@medusajs/workflows-sdk" @@ -6,9 +6,9 @@ type VariantPrice = { id?: string region_id?: string currency_code: string - amount: number - min_quantity?: number - max_quantity?: number + amount: BigNumberInput + min_quantity?: BigNumberInput + max_quantity?: BigNumberInput rules: Record } diff --git a/packages/core-flows/src/payment/steps/capture-payment.ts b/packages/core-flows/src/payment/steps/capture-payment.ts index 479dd5d990..fc1ebbb6d6 100644 --- a/packages/core-flows/src/payment/steps/capture-payment.ts +++ b/packages/core-flows/src/payment/steps/capture-payment.ts @@ -1,11 +1,11 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { IPaymentModuleService } from "@medusajs/types" +import { BigNumberInput, IPaymentModuleService } from "@medusajs/types" import { StepResponse, createStep } from "@medusajs/workflows-sdk" type StepInput = { payment_id: string captured_by?: string - amount?: number + amount?: BigNumberInput } export const capturePaymentStepId = "capture-payment-step" diff --git a/packages/core-flows/src/payment/steps/refund-payment.ts b/packages/core-flows/src/payment/steps/refund-payment.ts index 9c1ff1a3af..7b01f75eac 100644 --- a/packages/core-flows/src/payment/steps/refund-payment.ts +++ b/packages/core-flows/src/payment/steps/refund-payment.ts @@ -1,11 +1,11 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { IPaymentModuleService } from "@medusajs/types" +import { BigNumberInput, IPaymentModuleService } from "@medusajs/types" import { StepResponse, createStep } from "@medusajs/workflows-sdk" type StepInput = { payment_id: string created_by?: string - amount?: number + amount?: BigNumberInput } export const refundPaymentStepId = "refund-payment-step" diff --git a/packages/core-flows/src/payment/workflows/capture-payment.ts b/packages/core-flows/src/payment/workflows/capture-payment.ts index c303e93b4e..c36859ce76 100644 --- a/packages/core-flows/src/payment/workflows/capture-payment.ts +++ b/packages/core-flows/src/payment/workflows/capture-payment.ts @@ -1,4 +1,4 @@ -import { PaymentDTO } from "@medusajs/types" +import { BigNumberInput, PaymentDTO } from "@medusajs/types" import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" import { capturePaymentStep } from "../steps/capture-payment" @@ -9,7 +9,7 @@ export const capturePaymentWorkflow = createWorkflow( input: WorkflowData<{ payment_id: string captured_by?: string - amount?: number + amount?: BigNumberInput }> ): WorkflowData => { const payment = capturePaymentStep(input) diff --git a/packages/core-flows/src/payment/workflows/refund-payment.ts b/packages/core-flows/src/payment/workflows/refund-payment.ts index cfe108051c..081b696066 100644 --- a/packages/core-flows/src/payment/workflows/refund-payment.ts +++ b/packages/core-flows/src/payment/workflows/refund-payment.ts @@ -1,3 +1,4 @@ +import { BigNumberInput } from "@medusajs/types" import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" import { refundPaymentStep } from "../steps/refund-payment" @@ -8,7 +9,7 @@ export const refundPaymentWorkflow = createWorkflow( input: WorkflowData<{ payment_id: string created_by?: string - amount?: number + amount?: BigNumberInput }> ) => { const payment = refundPaymentStep(input) diff --git a/packages/order/integration-tests/__tests__/create-order.ts b/packages/order/integration-tests/__tests__/create-order.ts index ebd7ea9f61..3cb6bbd163 100644 --- a/packages/order/integration-tests/__tests__/create-order.ts +++ b/packages/order/integration-tests/__tests__/create-order.ts @@ -194,7 +194,8 @@ moduleIntegrationTestRunner({ it("should create an order, shipping method and items. Including taxes and adjustments associated with them", async function () { const createdOrder = await service.create(input) - expect(createdOrder).toEqual(expectation) + const serializedOrder = JSON.parse(JSON.stringify(createdOrder)) + expect(serializedOrder).toEqual(expectation) }) it("should transform requested fields and relations to match the db schema and return the order", async function () { @@ -232,7 +233,8 @@ moduleIntegrationTestRunner({ ], }) - expect(getOrder).toEqual(expectation) + const serializedOrder = JSON.parse(JSON.stringify(getOrder)) + expect(serializedOrder).toEqual(expectation) }) it("should return order transactions", async function () { @@ -247,7 +249,8 @@ moduleIntegrationTestRunner({ relations: ["transactions"], }) - expect(getOrder).toEqual( + const serializedOrder = JSON.parse(JSON.stringify(getOrder)) + expect(serializedOrder).toEqual( expect.objectContaining({ id: createdOrder.id, transactions: [ diff --git a/packages/order/integration-tests/__tests__/order-edit.ts b/packages/order/integration-tests/__tests__/order-edit.ts index 5bab0d9dec..aaec411141 100644 --- a/packages/order/integration-tests/__tests__/order-edit.ts +++ b/packages/order/integration-tests/__tests__/order-edit.ts @@ -1,8 +1,8 @@ import { Modules } from "@medusajs/modules-sdk" import { CreateOrderDTO, IOrderModuleService } from "@medusajs/types" -import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" -import { ChangeActionType } from "../../src/utils" import { BigNumber } from "@medusajs/utils" +import { SuiteOptions, moduleIntegrationTestRunner } from "medusa-test-utils" +import { ChangeActionType } from "../../src/utils" jest.setTimeout(100000) @@ -212,8 +212,10 @@ moduleIntegrationTestRunner({ ], relations: ["items", "shipping_methods", "transactions"], }) + const serializedFinalOrder = JSON.parse(JSON.stringify(finalOrder)) - expect(createdOrder.items).toEqual([ + const serializedCreatedOrder = JSON.parse(JSON.stringify(createdOrder)) + expect(serializedCreatedOrder.items).toEqual([ expect.objectContaining({ title: "Item 1", unit_price: 8, @@ -252,12 +254,12 @@ moduleIntegrationTestRunner({ }), ]) - expect(finalOrder).toEqual( + expect(serializedFinalOrder).toEqual( expect.objectContaining({ version: 1, }) ) - expect(finalOrder.items).toEqual([ + expect(serializedFinalOrder.items).toEqual([ expect.objectContaining({ title: "Item 1", subtitle: "Subtitle 1", @@ -396,7 +398,8 @@ moduleIntegrationTestRunner({ ], }) - await service.confirmOrderChange(orderChange.id, { + await service.confirmOrderChange({ + id: orderChange.id, confirmed_by: "cx_agent_123", }) @@ -416,13 +419,15 @@ moduleIntegrationTestRunner({ relations: ["items", "shipping_methods", "transactions"], }) - expect(modified).toEqual( + const serializedModifiedOrder = JSON.parse(JSON.stringify(modified)) + + expect(serializedModifiedOrder).toEqual( expect.objectContaining({ version: 2, }) ) - expect(modified.items).toEqual([ + expect(serializedModifiedOrder.items).toEqual([ expect.objectContaining({ quantity: 2, detail: expect.objectContaining({ diff --git a/packages/order/integration-tests/__tests__/order-items-shipping.spec.ts b/packages/order/integration-tests/__tests__/order-items-shipping.spec.ts index 45f5eb05a4..dd66ee13ec 100644 --- a/packages/order/integration-tests/__tests__/order-items-shipping.spec.ts +++ b/packages/order/integration-tests/__tests__/order-items-shipping.spec.ts @@ -1,5 +1,8 @@ import { Modules } from "@medusajs/modules-sdk" -import { IOrderModuleService } from "@medusajs/types" +import { + CreateOrderLineItemTaxLineDTO, + IOrderModuleService, +} from "@medusajs/types" import { OrderStatus } from "@medusajs/utils" import { SuiteOptions, moduleIntegrationTestRunner } from "medusa-test-utils" @@ -134,7 +137,8 @@ moduleIntegrationTestRunner({ relations: ["items.item"], }) - expect(order).toEqual( + const serialized = JSON.parse(JSON.stringify(order)) + expect(serialized).toEqual( expect.objectContaining({ id: createdOrder.id, currency_code: "eur", @@ -179,7 +183,8 @@ moduleIntegrationTestRunner({ } ) - expect(orders).toEqual( + const serialized = JSON.parse(JSON.stringify(orders)) + expect(serialized).toEqual( expect.arrayContaining([ expect.objectContaining({ currency_code: "eur", @@ -398,7 +403,8 @@ moduleIntegrationTestRunner({ relations: ["items.item"], }) - expect(order.items[0]).toEqual( + const serialized = JSON.parse(JSON.stringify(order)) + expect(serialized.items[0]).toEqual( expect.objectContaining({ quantity: 1, title: "test", @@ -436,7 +442,8 @@ moduleIntegrationTestRunner({ relations: ["items.item"], }) - expect(order.items).toEqual( + const serialized = JSON.parse(JSON.stringify(order)) + expect(serialized.items).toEqual( expect.arrayContaining([ expect.objectContaining({ title: "test", @@ -669,7 +676,8 @@ moduleIntegrationTestRunner({ relations: ["items.item"], }) - expect(order.items).toEqual( + const serialized = JSON.parse(JSON.stringify(order)) + expect(serialized.items).toEqual( expect.arrayContaining([ expect.objectContaining({ title: "changed-test", @@ -948,7 +956,8 @@ moduleIntegrationTestRunner({ relations: ["items.item.adjustments"], }) - expect(order.items).toEqual( + const serialized = JSON.parse(JSON.stringify(order)) + expect(serialized.items).toEqual( expect.arrayContaining([ expect.objectContaining({ id: itemOne.id, @@ -1071,7 +1080,8 @@ moduleIntegrationTestRunner({ relations: ["items.item.adjustments"], }) - expect(order.items).toEqual( + const serialized = JSON.parse(JSON.stringify(order)) + expect(serialized.items).toEqual( expect.arrayContaining([ expect.objectContaining({ id: itemOne.id, @@ -1224,9 +1234,10 @@ moduleIntegrationTestRunner({ }, ]) - const [checkOrderOne, checkOrderTwo] = await service.list( - {}, - { relations: ["items.item.adjustments"] } + const [checkOrderOne, checkOrderTwo] = JSON.parse( + JSON.stringify( + await service.list({}, { relations: ["items.item.adjustments"] }) + ) ) expect(checkOrderOne.items).toEqual( @@ -1445,7 +1456,8 @@ moduleIntegrationTestRunner({ relations: ["shipping_methods.adjustments"], }) - expect(order.shipping_methods).toEqual( + const serialized = JSON.parse(JSON.stringify(order)) + expect(serialized.shipping_methods).toEqual( expect.arrayContaining([ expect.objectContaining({ id: shippingMethodOne.id, @@ -1572,7 +1584,8 @@ moduleIntegrationTestRunner({ relations: ["shipping_methods.adjustments"], }) - expect(order.shipping_methods).toEqual( + const serialized = JSON.parse(JSON.stringify(order)) + expect(serialized.shipping_methods).toEqual( expect.arrayContaining([ expect.objectContaining({ id: shippingMethodOne.id, @@ -1994,7 +2007,8 @@ moduleIntegrationTestRunner({ relations: ["items.item.tax_lines"], }) - expect(order.items).toEqual( + const serialized = JSON.parse(JSON.stringify(order)) + expect(serialized.items).toEqual( expect.arrayContaining([ expect.objectContaining({ id: itemOne.id, @@ -2111,7 +2125,8 @@ moduleIntegrationTestRunner({ relations: ["items.item.tax_lines"], }) - expect(order.items).toEqual( + const serialized = JSON.parse(JSON.stringify(order)) + expect(serialized.items).toEqual( expect.arrayContaining([ expect.objectContaining({ id: itemOne.id, @@ -2174,7 +2189,7 @@ moduleIntegrationTestRunner({ ]) ) - const taxLine = taxLines.find((tx) => tx.item_id === itemOne.id) + const taxLine = taxLines.find((tx) => tx.item_id === itemOne.id)! await service.setLineItemTaxLines(createdOrder.id, [ // update @@ -2188,7 +2203,7 @@ moduleIntegrationTestRunner({ item_id: itemOne.id, rate: 25, code: "TX-2", - }, + } as CreateOrderLineItemTaxLineDTO, // delete: should delete the initial tax line for itemOne ]) @@ -2196,7 +2211,8 @@ moduleIntegrationTestRunner({ relations: ["items.item.tax_lines"], }) - expect(order.items).toEqual( + const serialized = JSON.parse(JSON.stringify(order)) + expect(serialized.items).toEqual( expect.arrayContaining([ expect.objectContaining({ id: itemOne.id, @@ -2356,9 +2372,10 @@ moduleIntegrationTestRunner({ }, ]) - const [checkOrderOne, checkOrderTwo] = await service.list( - {}, - { relations: ["items.item.tax_lines"] } + const [checkOrderOne, checkOrderTwo] = JSON.parse( + JSON.stringify( + await service.list({}, { relations: ["items.item.tax_lines"] }) + ) ) expect(checkOrderOne.items).toEqual( diff --git a/packages/order/src/services/order-module-service.ts b/packages/order/src/services/order-module-service.ts index 65d90218a4..2272cb6155 100644 --- a/packages/order/src/services/order-module-service.ts +++ b/packages/order/src/services/order-module-service.ts @@ -1670,7 +1670,7 @@ export default class OrderModuleService< } return data.order_change_id }) - .filter(Boolean) + .filter(Boolean) as string[] if (orderChangeIds.length) { const ordChanges = await this.getAndValidateOrderChange_( diff --git a/packages/order/src/utils/transform-order.ts b/packages/order/src/utils/transform-order.ts index 6d1d11af94..cd70f56db1 100644 --- a/packages/order/src/utils/transform-order.ts +++ b/packages/order/src/utils/transform-order.ts @@ -1,5 +1,5 @@ import { OrderTypes } from "@medusajs/types" -import { deduplicate, isDefined } from "@medusajs/utils" +import { decorateCartTotals, deduplicate, isDefined } from "@medusajs/utils" export function formatOrder( order @@ -23,7 +23,7 @@ export function formatOrder( order.summary = order.summary?.[0]?.totals - return order + return decorateCartTotals(order) }) return isArray ? orders : orders[0] diff --git a/packages/payment-stripe/src/core/stripe-base.ts b/packages/payment-stripe/src/core/stripe-base.ts index 137691878d..6c85fc869c 100644 --- a/packages/payment-stripe/src/core/stripe-base.ts +++ b/packages/payment-stripe/src/core/stripe-base.ts @@ -13,6 +13,7 @@ import { } from "@medusajs/types" import { AbstractPaymentProvider, + BigNumber, MedusaError, PaymentActions, isPaymentProviderError, @@ -115,7 +116,7 @@ abstract class StripeBase extends AbstractPaymentProvider { const intentRequest: Stripe.PaymentIntentCreateParams = { description, - amount: Math.round(amount), + amount: Math.round(new BigNumber(amount).numeric), currency: currency_code, metadata: { resource_id: resource_id ?? "Medusa Payment" }, capture_method: this.options_.capture ? "automatic" : "manual", @@ -259,6 +260,8 @@ abstract class StripeBase extends AbstractPaymentProvider { ): Promise { const { context, data, amount } = input + const amountNumeric = Math.round(new BigNumber(amount).numeric) + const stripeId = context.customer?.metadata?.stripe_id if (stripeId !== data.customer) { @@ -272,14 +275,14 @@ abstract class StripeBase extends AbstractPaymentProvider { return result } else { - if (amount && data.amount === Math.round(amount)) { + if (amount && data.amount === amountNumeric) { return { data } } try { const id = data.id as string const sessionData = (await this.stripe_.paymentIntents.update(id, { - amount: Math.round(amount), + amount: amountNumeric, })) as unknown as PaymentProviderSessionResponse["data"] return { data: sessionData } diff --git a/packages/payment/src/services/payment-module.ts b/packages/payment/src/services/payment-module.ts index 136b05dbf9..ce9a0a8067 100644 --- a/packages/payment/src/services/payment-module.ts +++ b/packages/payment/src/services/payment-module.ts @@ -27,9 +27,11 @@ import { UpsertPaymentCollectionDTO, } from "@medusajs/types" import { + BigNumber, InjectManager, InjectTransactionManager, isString, + MathBN, MedusaContext, MedusaError, ModulesSdkUtils, @@ -43,7 +45,6 @@ import { PaymentSession, Refund, } from "@models" -import BigNumber from "bignumber.js" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import PaymentProviderService from "./payment-provider" @@ -532,15 +533,14 @@ export default class PaymentModuleService< } const capturedAmount = payment.captures.reduce((captureAmount, next) => { - const amountAsBigNumber = new BigNumber(next.raw_amount.value) - return captureAmount.plus(amountAsBigNumber) - }, BigNumber(0)) + return MathBN.add(captureAmount, next.raw_amount) + }, MathBN.convert(0)) - const authorizedAmount = BigNumber(payment.raw_amount.value) - const newCaptureAmount = BigNumber(data.amount) - const remainingToCapture = authorizedAmount.minus(capturedAmount) + const authorizedAmount = new BigNumber(payment.raw_amount) + const newCaptureAmount = new BigNumber(data.amount) + const remainingToCapture = MathBN.sub(authorizedAmount, capturedAmount) - if (newCaptureAmount.gt(remainingToCapture)) { + if (MathBN.gt(newCaptureAmount, remainingToCapture)) { throw new MedusaError( MedusaError.Types.INVALID_DATA, `You cannot capture more than the authorized amount substracted by what is already captured.` @@ -567,7 +567,10 @@ export default class PaymentModuleService< ) // When the entire authorized amount has been captured, we mark it fully capture by setting the captured_at field - if (capturedAmount.plus(newCaptureAmount).eq(authorizedAmount)) { + const totalCaptured = MathBN.convert( + MathBN.add(capturedAmount, newCaptureAmount) + ) + if (MathBN.gte(totalCaptured, authorizedAmount)) { await this.paymentService_.update( { id: payment.id, captured_at: new Date() }, sharedContext @@ -600,12 +603,12 @@ export default class PaymentModuleService< } const capturedAmount = payment.captures.reduce((captureAmount, next) => { - const amountAsBigNumber = new BigNumber(next.raw_amount.value) - return captureAmount.plus(amountAsBigNumber) - }, BigNumber(0)) - const refundAmount = BigNumber(data.amount) + const amountAsBigNumber = new BigNumber(next.raw_amount) + return MathBN.add(captureAmount, amountAsBigNumber) + }, MathBN.convert(0)) + const refundAmount = new BigNumber(data.amount) - if (capturedAmount.lt(refundAmount)) { + if (MathBN.lt(capturedAmount, refundAmount)) { throw new MedusaError( MedusaError.Types.INVALID_DATA, `You cannot refund more than what is captured on the payment.` diff --git a/packages/types/src/cart/common.ts b/packages/types/src/cart/common.ts index 100a266ac3..b27f73adc9 100644 --- a/packages/types/src/cart/common.ts +++ b/packages/types/src/cart/common.ts @@ -1,5 +1,6 @@ import { BaseFilterable } from "../dal" import { OperatorMap } from "../dal/utils" +import { BigNumberValue } from "../totals" /** * The adjustment line details. @@ -19,7 +20,7 @@ export interface AdjustmentLineDTO { /** * The amount to adjust the original amount with. */ - amount: number + amount: BigNumberValue /** * The ID of the associated cart. @@ -142,6 +143,16 @@ export interface ShippingMethodTaxLineDTO extends TaxLineDTO { * The ID of the associated shipping method. */ shipping_method_id: string + + /** + * The total tax relative to the shipping method. + */ + total: BigNumberValue + + /** + * The subtotal tax relative to the shipping method. + */ + subtotal: BigNumberValue } /** @@ -157,6 +168,16 @@ export interface LineItemTaxLineDTO extends TaxLineDTO { * The ID of the associated line item. */ item_id: string + + /** + * The total tax relative to the item. + */ + total: BigNumberValue + + /** + * The subtotal tax relative to the item. + */ + subtotal: BigNumberValue } /** @@ -266,7 +287,7 @@ export interface CartShippingMethodDTO { /** * The price of the shipping method. */ - amount: number + amount: BigNumberValue /** * Whether the shipping method price is tax inclusive. @@ -315,42 +336,42 @@ export interface CartShippingMethodDTO { /** * The original total of the cart shipping method. */ - original_total: number + original_total: BigNumberValue /** * The original subtotal of the cart shipping method. */ - original_subtotal: number + original_subtotal: BigNumberValue /** * The original tax total of the cart shipping method. */ - original_tax_total: number + original_tax_total: BigNumberValue /** * The total of the cart shipping method. */ - total: number + total: BigNumberValue /** * The subtotal of the cart shipping method. */ - subtotal: number + subtotal: BigNumberValue /** * The tax total of the cart shipping method. */ - tax_total: number + tax_total: BigNumberValue /** * The discount total of the cart shipping method. */ - discount_total: number + discount_total: BigNumberValue /** * The discount tax total of the cart shipping method. */ - discount_tax_total: number + discount_tax_total: BigNumberValue } /** @@ -360,57 +381,57 @@ export interface CartLineItemTotalsDTO { /** * The original total of the cart line item. */ - original_total: number + original_total: BigNumberValue /** * The original subtotal of the cart line item. */ - original_subtotal: number + original_subtotal: BigNumberValue /** * The original tax total of the cart line item. */ - original_tax_total: number + original_tax_total: BigNumberValue /** * The item total of the cart line item. */ - item_total: number + item_total: BigNumberValue /** * The item subtotal of the cart line item. */ - item_subtotal: number + item_subtotal: BigNumberValue /** * The item tax total of the cart line item. */ - item_tax_total: number + item_tax_total: BigNumberValue /** * The total of the cart line item. */ - total: number + total: BigNumberValue /** * The subtotal of the cart line item. */ - subtotal: number + subtotal: BigNumberValue /** * The tax total of the cart line item. */ - tax_total: number + tax_total: BigNumberValue /** * The discount total of the cart line item. */ - discount_total: number + discount_total: BigNumberValue /** * The discount tax total of the cart line item. */ - discount_tax_total: number + discount_tax_total: BigNumberValue } /** @@ -440,7 +461,7 @@ export interface CartLineItemDTO extends CartLineItemTotalsDTO { /** * The line item's quantity in the cart. */ - quantity: number + quantity: BigNumberValue /** * The ID of the associated product. @@ -520,12 +541,12 @@ export interface CartLineItemDTO extends CartLineItemTotalsDTO { /** * The calculated price of the line item. */ - compare_at_unit_price?: number + compare_at_unit_price?: BigNumberValue /** * The unit price of the item. */ - unit_price: number + unit_price: BigNumberValue /** * The associated tax lines. @@ -654,67 +675,67 @@ export interface CartDTO { /** * The original item total of the cart. */ - original_item_total: number + original_item_total: BigNumberValue /** * The original item subtotal of the cart. */ - original_item_subtotal: number + original_item_subtotal: BigNumberValue /** * The original item tax total of the cart. */ - original_item_tax_total: number + original_item_tax_total: BigNumberValue /** * The item total of the cart. */ - item_total: number + item_total: BigNumberValue /** * The item subtotal of the cart. */ - item_subtotal: number + item_subtotal: BigNumberValue /** * The item tax total of the cart. */ - item_tax_total: number + item_tax_total: BigNumberValue /** * The original total of the cart. */ - original_total: number + original_total: BigNumberValue /** * The original subtotal of the cart. */ - original_subtotal: number + original_subtotal: BigNumberValue /** * The original tax total of the cart. */ - original_tax_total: number + original_tax_total: BigNumberValue /** * The total of the cart. */ - total: number + total: BigNumberValue /** - * The subtotal of the cart. + * The subtotal of the cart. (Excluding taxes) */ - subtotal: number + subtotal: BigNumberValue /** * The tax total of the cart. */ - tax_total: number + tax_total: BigNumberValue /** * The discount total of the cart. */ - discount_total: number + discount_total: BigNumberValue /** * The raw discount total of the cart. @@ -724,47 +745,47 @@ export interface CartDTO { /** * The discount tax total of the cart. */ - discount_tax_total: number + discount_tax_total: BigNumberValue /** * The gift card total of the cart. */ - gift_card_total: number + gift_card_total: BigNumberValue /** * The gift card tax total of the cart. */ - gift_card_tax_total: number + gift_card_tax_total: BigNumberValue /** * The shipping total of the cart. */ - shipping_total: number + shipping_total: BigNumberValue /** * The shipping subtotal of the cart. */ - shipping_subtotal: number + shipping_subtotal: BigNumberValue /** * The shipping tax total of the cart. */ - shipping_tax_total: number + shipping_tax_total: BigNumberValue /** * The original shipping total of the cart. */ - original_shipping_total: number + original_shipping_total: BigNumberValue /** * The original shipping subtotal of the cart. */ - original_shipping_subtotal: number + original_shipping_subtotal: BigNumberValue /** * The original shipping tax total of the cart. */ - original_shipping_tax_total: number + original_shipping_tax_total: BigNumberValue } /** diff --git a/packages/types/src/cart/mutations.ts b/packages/types/src/cart/mutations.ts index 7e1fd40c0d..56f0acd82e 100644 --- a/packages/types/src/cart/mutations.ts +++ b/packages/types/src/cart/mutations.ts @@ -1,3 +1,4 @@ +import { BigNumberInput } from "../totals" import { CartDTO, CartLineItemDTO } from "./common" /** ADDRESS START */ @@ -228,7 +229,7 @@ export interface CreateAdjustmentDTO { /** * The amount to adjust the original amount with. */ - amount: number + amount: BigNumberInput /** * The description of the adjustment. @@ -264,7 +265,7 @@ export interface UpdateAdjustmentDTO { /** * The amount to adjust the original amount with. */ - amount: number + amount: BigNumberInput /** * The description of the adjustment. @@ -325,7 +326,7 @@ export interface UpsertLineItemAdjustmentDTO { /** * The amount to adjust the original amount with. */ - amount?: number + amount?: BigNumberInput /** * The description of the line item adjustment. @@ -463,7 +464,7 @@ export interface CreateLineItemDTO { /** * The quantity of the line item in the cart. */ - quantity: number + quantity: BigNumberInput /** * The associated product's ID. @@ -543,12 +544,12 @@ export interface CreateLineItemDTO { /** * The calculated price of the line item after applying promotions. */ - compare_at_unit_price?: number + compare_at_unit_price?: BigNumberInput /** * The unit price of the line item. */ - unit_price: number | string + unit_price: BigNumberInput /** * The tax lines of the line item. @@ -626,12 +627,12 @@ export interface UpdateLineItemDTO /** * The quantity of the line item in the cart. */ - quantity?: number + quantity?: BigNumberInput /** * The unit price of the line item. */ - unit_price?: number + unit_price?: BigNumberInput /** * Holds custom data in key-value pairs. @@ -670,7 +671,7 @@ export interface CreateShippingMethodDTO { /** * The amount of the shipping method. */ - amount: number + amount: BigNumberInput /** * The data of the shipping method. @@ -700,7 +701,7 @@ export interface CreateShippingMethodForSingleCartDTO { /** * The amount of the shipping method. */ - amount: number + amount: BigNumberInput /** * The data of the shipping method. @@ -735,7 +736,7 @@ export interface UpdateShippingMethodDTO { /** * The amount of the shipping method. */ - amount?: number + amount?: BigNumberInput /** * The data of the shipping method. @@ -771,7 +772,7 @@ export interface CreateShippingMethodAdjustmentDTO { /** * The amount to adjust the original amount with. */ - amount: number + amount: BigNumberInput /** * The description of the shipping method adjustment. @@ -807,7 +808,7 @@ export interface UpdateShippingMethodAdjustmentDTO { /** * The amount to adjust the original amount with. */ - amount?: number + amount?: BigNumberInput /** * The description of the shipping method adjustment. diff --git a/packages/types/src/cart/workflows.ts b/packages/types/src/cart/workflows.ts index be9e50c167..9635720d77 100644 --- a/packages/types/src/cart/workflows.ts +++ b/packages/types/src/cart/workflows.ts @@ -2,11 +2,12 @@ import { CustomerDTO } from "../customer" import { ShippingOptionDTO } from "../fulfillment" import { ProductDTO } from "../product" import { RegionDTO } from "../region" +import { BigNumberInput } from "../totals" import { CartDTO, CartLineItemDTO } from "./common" import { UpdateLineItemDTO } from "./mutations" export interface CreateCartCreateLineItemDTO { - quantity: number + quantity: BigNumberInput variant_id: string title?: string @@ -31,8 +32,8 @@ export interface CreateCartCreateLineItemDTO { is_tax_inclusive?: boolean is_giftcard?: boolean - compare_at_unit_price?: number - unit_price?: number | string + compare_at_unit_price?: BigNumberInput + unit_price?: BigNumberInput metadata?: Record } @@ -93,7 +94,7 @@ export interface CreatePaymentCollectionForCartWorkflowInputDTO { cart_id: string region_id: string currency_code: string - amount: number + amount: BigNumberInput metadata?: Record } @@ -115,5 +116,5 @@ export interface ListShippingOptionsForCartWorkflowInputDTO { } export interface PricedShippingOptionDTO extends ShippingOptionDTO { - amount: number + amount: BigNumberInput } diff --git a/packages/types/src/order/common.ts b/packages/types/src/order/common.ts index 321f901d9c..08fb89a914 100644 --- a/packages/types/src/order/common.ts +++ b/packages/types/src/order/common.ts @@ -1,30 +1,30 @@ import { BaseFilterable } from "../dal" import { OperatorMap } from "../dal/utils" -import { BigNumberRawValue } from "../totals" +import { BigNumberRawValue, BigNumberValue } from "../totals" export type OrderSummaryDTO = { - total: number - subtotal: number - total_tax: number + total: BigNumberValue + subtotal: BigNumberValue + total_tax: BigNumberValue - ordered_total: number - fulfilled_total: number - returned_total: number - return_request_total: number - write_off_total: number - projected_total: number + ordered_total: BigNumberValue + fulfilled_total: BigNumberValue + returned_total: BigNumberValue + return_request_total: BigNumberValue + write_off_total: BigNumberValue + projected_total: BigNumberValue - net_total: number - net_subtotal: number - net_total_tax: number + net_total: BigNumberValue + net_subtotal: BigNumberValue + net_total_tax: BigNumberValue - future_total: number - future_subtotal: number - future_total_tax: number - future_projected_total: number + future_total: BigNumberValue + future_subtotal: BigNumberValue + future_total_tax: BigNumberValue + future_projected_total: BigNumberValue - balance: number - future_balance: number + balance: BigNumberValue + future_balance: BigNumberValue } export interface OrderAdjustmentLineDTO { @@ -39,7 +39,7 @@ export interface OrderAdjustmentLineDTO { /** * The amount of the adjustment line */ - amount: number + amount: BigNumberValue /** * The ID of the associated order */ @@ -134,6 +134,16 @@ export interface OrderShippingMethodTaxLineDTO extends OrderTaxLineDTO { * The ID of the associated shipping method */ shipping_method_id: string + + /** + * The total tax relative to the shipping method. + */ + total: BigNumberValue + + /** + * The subtotal tax relative to the shipping method. + */ + subtotal: BigNumberValue } export interface OrderLineItemTaxLineDTO extends OrderTaxLineDTO { @@ -145,6 +155,16 @@ export interface OrderLineItemTaxLineDTO extends OrderTaxLineDTO { * The ID of the associated line item */ item_id: string + + /** + * The total tax relative to the item. + */ + total: BigNumberValue + + /** + * The subtotal tax relative to the item. + */ + subtotal: BigNumberValue } export interface OrderAddressDTO { @@ -276,11 +296,45 @@ export interface OrderShippingMethodDTO { */ updated_at: Date | string - total: number - subtotal: number - tax_total: number - discount_total: number - discount_tax_total: number + /** + * The original total of the order shipping method. + */ + original_total: BigNumberValue + + /** + * The original subtotal of the order shipping method. + */ + original_subtotal: BigNumberValue + + /** + * The original tax total of the order shipping method. + */ + original_tax_total: BigNumberValue + + /** + * The total of the order shipping method. + */ + total: BigNumberValue + + /** + * The subtotal of the order shipping method. + */ + subtotal: BigNumberValue + + /** + * The tax total of the order shipping method. + */ + tax_total: BigNumberValue + + /** + * The discount total of the order shipping method. + */ + discount_total: BigNumberValue + + /** + * The discount tax total of the order shipping method. + */ + discount_tax_total: BigNumberValue } export interface OrderLineItemTotalsDTO { diff --git a/packages/types/src/order/mutations.ts b/packages/types/src/order/mutations.ts index 6e1725789c..054956b66b 100644 --- a/packages/types/src/order/mutations.ts +++ b/packages/types/src/order/mutations.ts @@ -40,7 +40,7 @@ export interface CreateOrderDTO { no_notification?: boolean items?: CreateOrderLineItemDTO[] shipping_methods?: Omit[] - transactions?: CreateOrderTransactionDTO[] + transactions?: Omit[] metadata?: Record promo_codes?: string[] @@ -244,7 +244,9 @@ export interface CreateOrderChangeDTO { internal_note?: string requested_by?: string requested_at?: Date + created_by?: string metadata?: Record + actions?: CreateOrderChangeActionDTO[] } export interface UpdateOrderChangeDTO { @@ -286,9 +288,9 @@ export interface ConfirmOrderChangeDTO { /** ORDER CHANGE ACTION START */ export interface CreateOrderChangeActionDTO { - order_change_id: string - reference: string - reference_id: string + order_change_id?: string + reference?: string + reference_id?: string action: string internal_note?: string details?: Record diff --git a/packages/types/src/payment/common.ts b/packages/types/src/payment/common.ts index faa9213632..e9b22ed1d2 100644 --- a/packages/types/src/payment/common.ts +++ b/packages/types/src/payment/common.ts @@ -1,5 +1,6 @@ import { BaseFilterable } from "../dal" import { OperatorMap } from "../dal/utils" +import { BigNumberValue } from "../totals" /* ********** PAYMENT COLLECTION ********** */ /** * @enum @@ -87,17 +88,17 @@ export interface PaymentCollectionDTO { /** * The total amount to be authorized and captured. */ - amount: number + amount: BigNumberValue /** * The amount authorized within the associated payment sessions. */ - authorized_amount?: number + authorized_amount?: BigNumberValue /** * The amount refunded within the associated payments. */ - refunded_amount?: number + refunded_amount?: BigNumberValue /** * When the payment collection was completed. @@ -190,7 +191,7 @@ export interface FilterablePaymentSessionProps /** * Filter the payment sessions by their amount. */ - amount?: number | OperatorMap + amount?: BigNumberValue | OperatorMap /** * Filter the payment sessions by the ID of their associated payment provider. @@ -240,7 +241,7 @@ export interface FilterableCaptureProps extends BaseFilterable { /** * Filter the captures by their amounts. */ - amount?: number | OperatorMap + amount?: BigNumberValue | OperatorMap /** * Filter the captures by the ID of their associated payment. @@ -280,7 +281,7 @@ export interface FilterableRefundProps extends BaseFilterable { /** * Filter the refunds by their amount. */ - amount?: number | OperatorMap + amount?: BigNumberValue | OperatorMap /** * Filter the refunds by the ID of their associated payment. @@ -312,12 +313,12 @@ export interface PaymentDTO { /** * The payment's total amount. */ - amount: number + amount: BigNumberValue /** * The authorized amount of the payment. */ - authorized_amount?: number + authorized_amount?: BigNumberValue /** * The ISO 3 character currency code of the payment. @@ -377,12 +378,12 @@ export interface PaymentDTO { /** * The sum of the associated captures' amounts. */ - captured_amount?: number + captured_amount?: BigNumberValue /** * The sum of the associated refunds' amounts. */ - refunded_amount?: number + refunded_amount?: BigNumberValue /** * The associated captures. @@ -481,7 +482,7 @@ export interface CaptureDTO { /** * The captured amount. */ - amount: number + amount: BigNumberValue /** * The creation date of the capture. @@ -512,7 +513,7 @@ export interface RefundDTO { /** * The refunded amount. */ - amount: number + amount: BigNumberValue /** * The creation date of the refund. @@ -543,7 +544,7 @@ export interface PaymentSessionDTO { /** * The amount to authorize. */ - amount: number + amount: BigNumberValue /** * The 3 character currency code of the payment session. diff --git a/packages/types/src/payment/mutations.ts b/packages/types/src/payment/mutations.ts index 7748c59c76..bfc0e7a1e2 100644 --- a/packages/types/src/payment/mutations.ts +++ b/packages/types/src/payment/mutations.ts @@ -1,3 +1,4 @@ +import { BigNumberInput } from "../totals" import { PaymentProviderContext } from "./provider" /** @@ -17,7 +18,7 @@ export interface CreatePaymentCollectionDTO { /** * The amount of the payment collection. */ - amount: number + amount: BigNumberInput /** * Holds custom data in key-value pairs. @@ -58,7 +59,7 @@ export interface UpsertPaymentCollectionDTO { /** * The amount of the payment collection. */ - amount?: number + amount?: BigNumberInput /** * Holds custom data in key-value pairs. @@ -83,7 +84,7 @@ export interface PaymentCollectionUpdatableFields { /** * The amount of the payment collection. */ - amount?: number + amount?: BigNumberInput /** * Holds custom data in key-value pairs. @@ -98,7 +99,7 @@ export interface CreatePaymentDTO { /** * The amount of the payment. */ - amount: number + amount: BigNumberInput /** * The ISO 3 character currency code of the payment. @@ -183,7 +184,7 @@ export interface CreateCaptureDTO { /** * The amount of the capture. */ - amount?: number + amount?: BigNumberInput /** * The associated payment's ID. @@ -204,7 +205,7 @@ export interface CreateRefundDTO { /** * The amount of the refund. */ - amount?: number + amount?: BigNumberInput /** * The associated payment's ID. @@ -235,7 +236,7 @@ export interface CreatePaymentSessionDTO { /** * The amount to be authorized. */ - amount: number + amount: BigNumberInput /** * Necessary data for the associated payment provider to process the payment. @@ -270,7 +271,7 @@ export interface UpdatePaymentSessionDTO { /** * The amount to be authorized. */ - amount: number + amount: BigNumberInput /** * Necessary context data for the associated payment provider. diff --git a/packages/types/src/payment/provider.ts b/packages/types/src/payment/provider.ts index 290c2d1f9a..2628e32536 100644 --- a/packages/types/src/payment/provider.ts +++ b/packages/types/src/payment/provider.ts @@ -1,5 +1,6 @@ import { AddressDTO } from "../address" import { CustomerDTO } from "../customer" +import { BigNumberInput, BigNumberValue } from "../totals" import { PaymentSessionStatus } from "./common" import { ProviderWebhookPayload } from "./mutations" @@ -85,7 +86,7 @@ export type CreatePaymentProviderSession = { /** * The amount to be authorized. */ - amount: number + amount: BigNumberInput /** * The ISO 3 character currency code. @@ -112,7 +113,7 @@ export type UpdatePaymentProviderSession = { /** * The payment session's amount. */ - amount: number + amount: BigNumberInput /** * The ISO 3 character code of the payment session. @@ -203,7 +204,7 @@ export type WebhookActionData = { /** * The amount to be captured or authorized (based on the action's type.) */ - amount: number + amount: BigNumberValue } /** @@ -310,12 +311,12 @@ export interface IPaymentProvider { * In this method, you can interact with the third-party provider and perform any actions necessary to refund the payment. * * @param {Record} paymentSessionData - The `data` field of a Payment. - * @param {number} refundAmount - The amount to refund. + * @param {BigNumberInput} refundAmount - The amount to refund. * @returns {Promise} Either an error object or an object that's stored in the `data` field of the payment. */ refundPayment( paymentSessionData: Record, - refundAmount: number + refundAmount: BigNumberInput ): Promise /** diff --git a/packages/types/src/pricing/common/money-amount.ts b/packages/types/src/pricing/common/money-amount.ts index 6a72d2e508..5d771b884f 100644 --- a/packages/types/src/pricing/common/money-amount.ts +++ b/packages/types/src/pricing/common/money-amount.ts @@ -1,4 +1,5 @@ import { BaseFilterable } from "../../dal" +import { BigNumberInput, BigNumberValue } from "../../totals" /** * @interface @@ -17,15 +18,15 @@ export interface MoneyAmountDTO { /** * The price of this money amount. */ - amount?: number + amount?: BigNumberValue /** * The minimum quantity required to be purchased for this price to be applied. */ - min_quantity?: number + min_quantity?: BigNumberValue /** * The maximum quantity required to be purchased for this price to be applied. */ - max_quantity?: number + max_quantity?: BigNumberValue /** * When the money_amount was created. */ @@ -57,15 +58,15 @@ export interface CreateMoneyAmountDTO { /** * The amount of this money amount. */ - amount: number + amount: BigNumberInput /** * The minimum quantity required to be purchased for this money amount to be applied. */ - min_quantity?: number | null + min_quantity?: BigNumberInput | null /** * The maximum quantity required to be purchased for this money amount to be applied. */ - max_quantity?: number | null + max_quantity?: BigNumberInput | null } /** @@ -85,15 +86,15 @@ export interface UpdateMoneyAmountDTO { /** * The price of this money amount. */ - amount?: number + amount?: BigNumberInput /** * The minimum quantity required to be purchased for this money amount to be applied. */ - min_quantity?: number + min_quantity?: BigNumberInput /** * The maximum quantity required to be purchased for this money amount to be applied. */ - max_quantity?: number + max_quantity?: BigNumberInput } /** diff --git a/packages/types/src/pricing/common/price-set.ts b/packages/types/src/pricing/common/price-set.ts index 26579be235..f93ebe8b76 100644 --- a/packages/types/src/pricing/common/price-set.ts +++ b/packages/types/src/pricing/common/price-set.ts @@ -1,5 +1,6 @@ import { BaseFilterable } from "../../dal" import { Context } from "../../shared-context" +import { BigNumberInput, BigNumberValue } from "../../totals" import { CreateMoneyAmountDTO, FilterableMoneyAmountProps, @@ -24,10 +25,10 @@ export interface PricingRepositoryService { */ export interface PricingContext { /** - * an object whose keys are the name of the context attribute. Its value can be a string or a number. For example, you can pass the `currency_code` property with its value being the currency code to calculate the price in. + * an object whose keys are the name of the context attribute. Its value can be a string or a BigNumberInput. For example, you can pass the `currency_code` property with its value being the currency code to calculate the price in. * Another example is passing the `quantity` property to calculate the price for that specified quantity, which finds a price set whose `min_quantity` and `max_quantity` conditions match the specified quantity. */ - context?: Record + context?: Record } /** @@ -124,7 +125,7 @@ export interface CalculatedPriceSet { /** * The amount of the calculated price, or `null` if there isn't a calculated price. */ - calculated_amount: number | null + calculated_amount: BigNumberValue | null /** * Whether the original price is associated with a price list. During the calculation process, if the price list of the calculated price is of type override, @@ -134,7 +135,7 @@ export interface CalculatedPriceSet { /** * The amount of the original price, or `null` if there isn't a calculated price. */ - original_amount: number | null + original_amount: BigNumberValue | null /** * The currency code of the calculated price, or null if there isn't a calculated price. @@ -160,11 +161,11 @@ export interface CalculatedPriceSet { /** * The `min_quantity` field defined on a price. */ - min_quantity: number | null + min_quantity: BigNumberValue | null /** * The `max_quantity` field defined on a price. */ - max_quantity: number | null + max_quantity: BigNumberValue | null } /** @@ -186,11 +187,11 @@ export interface CalculatedPriceSet { /** * The `min_quantity` field defined on a price. */ - min_quantity: number | null + min_quantity: BigNumberValue | null /** * The `max_quantity` field defined on a price. */ - max_quantity: number | null + max_quantity: BigNumberValue | null } } diff --git a/packages/types/src/pricing/common/price.ts b/packages/types/src/pricing/common/price.ts index 52d22ca19f..81d6fc7a88 100644 --- a/packages/types/src/pricing/common/price.ts +++ b/packages/types/src/pricing/common/price.ts @@ -1,4 +1,5 @@ import { BaseFilterable } from "../../dal" +import { BigNumberInput, BigNumberValue } from "../../totals" import { PriceListDTO } from "./price-list" import { PriceRuleDTO } from "./price-rule" import { PriceSetDTO } from "./price-set" @@ -24,15 +25,15 @@ export interface PriceDTO { /** * The price of this price. */ - amount?: number + amount?: BigNumberValue /** * The minimum quantity required to be purchased for this price to be applied. */ - min_quantity?: number + min_quantity?: BigNumberValue /** * The maximum quantity required to be purchased for this price to be applied. */ - max_quantity?: number + max_quantity?: BigNumberValue /** * The price set associated with the price. * @@ -80,15 +81,15 @@ export interface UpdatePriceDTO { /** * The amount of this price. */ - amount?: number + amount?: BigNumberInput /** * The minimum quantity required to be purchased for this price to be applied. */ - min_quantity?: number + min_quantity?: BigNumberInput /** * The maximum quantity required to be purchased for this price to be applied. */ - max_quantity?: number + max_quantity?: BigNumberInput } export interface CreatePriceDTO { @@ -104,15 +105,15 @@ export interface CreatePriceDTO { /** * The amount of this price. */ - amount: number + amount: BigNumberInput /** * The minimum quantity required to be purchased for this price to be applied. */ - min_quantity?: number | null + min_quantity?: BigNumberInput | null /** * The maximum quantity required to be purchased for this price to be applied. */ - max_quantity?: number | null + max_quantity?: BigNumberInput | null } /** diff --git a/packages/types/src/totals/big-number.ts b/packages/types/src/totals/big-number.ts index fc93c79ed9..972aac8c1f 100644 --- a/packages/types/src/totals/big-number.ts +++ b/packages/types/src/totals/big-number.ts @@ -6,3 +6,5 @@ export type BigNumberRawValue = { } export type BigNumberInput = BigNumberRawValue | number | string | BigNumberJS + +export type BigNumberValue = BigNumberJS | number | string diff --git a/packages/types/src/totals/index.ts b/packages/types/src/totals/index.ts index 0ed2463b75..ac1cb57e56 100644 --- a/packages/types/src/totals/index.ts +++ b/packages/types/src/totals/index.ts @@ -1,2 +1,27 @@ -export * from "./big-number"; +import { BigNumberValue } from "./big-number" +export * from "./big-number" + +export type CartLikeWithTotals = { + original_item_subtotal: BigNumberValue + original_item_tax_total: BigNumberValue + item_total: BigNumberValue + item_subtotal: BigNumberValue + item_tax_total: BigNumberValue + original_total: BigNumberValue + original_subtotal: BigNumberValue + original_tax_total: BigNumberValue + total: BigNumberValue + subtotal: BigNumberValue + tax_total: BigNumberValue + discount_total: BigNumberValue + discount_tax_total: BigNumberValue + gift_card_total: BigNumberValue + gift_card_tax_total: BigNumberValue + shipping_total: BigNumberValue + shipping_subtotal: BigNumberValue + shipping_tax_total: BigNumberValue + original_shipping_total: BigNumberValue + original_shipping_subtotal: BigNumberValue + original_shipping_tax_total: BigNumberValue +} diff --git a/packages/utils/src/totals/__tests__/totals.ts b/packages/utils/src/totals/__tests__/totals.ts new file mode 100644 index 0000000000..6925f9631b --- /dev/null +++ b/packages/utils/src/totals/__tests__/totals.ts @@ -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, + }) + }) +}) diff --git a/packages/utils/src/totals/adjustment/index.ts b/packages/utils/src/totals/adjustment/index.ts new file mode 100644 index 0000000000..940b3314ea --- /dev/null +++ b/packages/utils/src/totals/adjustment/index.ts @@ -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[] + 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 +} diff --git a/packages/utils/src/totals/big-number.ts b/packages/utils/src/totals/big-number.ts index d2afdcae64..87346c32b4 100644 --- a/packages/utils/src/totals/big-number.ts +++ b/packages/utils/src/totals/big-number.ts @@ -119,6 +119,6 @@ export class BigNumber { } valueOf() { - return this.bignumber_ + return this.numeric_ } } diff --git a/packages/utils/src/totals/cart/index.ts b/packages/utils/src/totals/cart/index.ts new file mode 100644 index 0000000000..f9f98d026b --- /dev/null +++ b/packages/utils/src/totals/cart/index.ts @@ -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 +} diff --git a/packages/utils/src/totals/index.ts b/packages/utils/src/totals/index.ts index 73f19e75ac..5668567ad0 100644 --- a/packages/utils/src/totals/index.ts +++ b/packages/utils/src/totals/index.ts @@ -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 -} diff --git a/packages/utils/src/totals/line-item/index.ts b/packages/utils/src/totals/line-item/index.ts new file mode 100644 index 0000000000..79aabe6c0d --- /dev/null +++ b/packages/utils/src/totals/line-item/index.ts @@ -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[] + adjustments?: Pick[] +} + +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 +} diff --git a/packages/utils/src/totals/math.ts b/packages/utils/src/totals/math.ts index 77a434fdd5..35eafcc5a8 100644 --- a/packages/utils/src/totals/math.ts +++ b/packages/utils/src/totals/math.ts @@ -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 { diff --git a/packages/utils/src/totals/promotion/index.ts b/packages/utils/src/totals/promotion/index.ts index 38116e9378..df24544819 100644 --- a/packages/utils/src/totals/promotion/index.ts +++ b/packages/utils/src/totals/promotion/index.ts @@ -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) +} diff --git a/packages/utils/src/totals/promotion/totals.ts b/packages/utils/src/totals/promotion/totals.ts deleted file mode 100644 index df24544819..0000000000 --- a/packages/utils/src/totals/promotion/totals.ts +++ /dev/null @@ -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) -} diff --git a/packages/utils/src/totals/shipping-method/index.ts b/packages/utils/src/totals/shipping-method/index.ts new file mode 100644 index 0000000000..f6c4ccc9b6 --- /dev/null +++ b/packages/utils/src/totals/shipping-method/index.ts @@ -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[] +} + +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 +} diff --git a/packages/utils/src/totals/tax/index.ts b/packages/utils/src/totals/tax/index.ts new file mode 100644 index 0000000000..b477005ede --- /dev/null +++ b/packages/utils/src/totals/tax/index.ts @@ -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[] + 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 +} diff --git a/packages/utils/src/totals/to-big-number-js.ts b/packages/utils/src/totals/to-big-number-js.ts deleted file mode 100644 index 5fa4904009..0000000000 --- a/packages/utils/src/totals/to-big-number-js.ts +++ /dev/null @@ -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 = { [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: 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) -}