fix(medusa): totals calculation with gift card (#5075)

**What**
Resolve potential discrepency between what is calculated in the `createFromCart` and the cart/order totals calculation.
When the giftCard is taxable then the following calculation is applied 
```ts
cart.subtotal + cart.shipping_total - cart.discount_total
```
otherwise
```ts
 cart.subtotal +
          cart.shipping_total +
          cart.tax_total -
          cart.discount_total
```

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Adrien de PerettiandOli Juhl authored and GitHub committed 2023-10-25 10:29:30 +00:00
1 parent 8bb8eb530b
commit e64823d1b9
6 files changed
+84 -29

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
fix(medusa): totals calculation with gift card
@@ -1,3 +1,5 @@
const NewTotalsService = require("../new-totals")
export const newTotalsServiceMock = { export const newTotalsServiceMock = {
withTransaction: function () { withTransaction: function () {
return this return this
@@ -16,6 +18,23 @@ export const newTotalsServiceMock = {
getShippingMethodTotals: jest.fn().mockImplementation((order, lineItems) => { getShippingMethodTotals: jest.fn().mockImplementation((order, lineItems) => {
return Promise.resolve({}) return Promise.resolve({})
}), }),
getGiftCardableAmount: jest
.fn()
.mockImplementation(
({
gift_cards_taxable,
subtotal,
shipping_total,
discount_total,
tax_total,
}) => {
return Promise.resolve(
(gift_cards_taxable
? subtotal + shipping_total - discount_total
: subtotal + shipping_total + tax_total - discount_total) || 0
)
}
),
} }
const mock = jest.fn().mockImplementation(() => { const mock = jest.fn().mockImplementation(() => {
@@ -364,6 +364,7 @@ describe("OrderService", () => {
subtotal: 100, subtotal: 100,
total: 100, total: 100,
discount_total: 0, discount_total: 0,
gift_card_total: 100,
} }
orderService.cartService_.retrieveWithTotals = () => { orderService.cartService_.retrieveWithTotals = () => {
+22 -16
View File
@@ -1,8 +1,11 @@
import { FlagRouter, isDefined, MedusaError } from "@medusajs/utils"
import { isEmpty, isEqual } from "lodash"
import { DeepPartial, EntityManager, In, IsNull, Not } from "typeorm"
import { import {
Address, Address,
Cart, Cart,
CustomShippingOption,
Customer, Customer,
CustomShippingOption,
Discount, Discount,
DiscountRule, DiscountRule,
DiscountRuleType, DiscountRuleType,
@@ -22,13 +25,13 @@ import {
CartCreateProps, CartCreateProps,
CartUpdateProps, CartUpdateProps,
FilterableCartProps, FilterableCartProps,
isCart,
LineItemUpdate, LineItemUpdate,
LineItemValidateData, LineItemValidateData,
isCart,
} from "../types/cart" } from "../types/cart"
import { import {
CustomShippingOptionService,
CustomerService, CustomerService,
CustomShippingOptionService,
DiscountService, DiscountService,
EventBusService, EventBusService,
GiftCardService, GiftCardService,
@@ -48,21 +51,16 @@ import {
TaxProviderService, TaxProviderService,
TotalsService, TotalsService,
} from "." } from "."
import { DeepPartial, EntityManager, In, IsNull, Not } from "typeorm"
import { IPriceSelectionStrategy, TransactionBaseService } from "../interfaces" import { IPriceSelectionStrategy, TransactionBaseService } from "../interfaces"
import { MedusaError, isDefined } from "medusa-core-utils" import IsolateProductDomainFeatureFlag from "../loaders/feature-flags/isolate-product-domain"
import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels"
import { buildQuery, isString, setMetadata } from "../utils" import { buildQuery, isString, setMetadata } from "../utils"
import { isEmpty, isEqual } from "lodash"
import { AddressRepository } from "../repositories/address" import { AddressRepository } from "../repositories/address"
import { CartRepository } from "../repositories/cart" import { CartRepository } from "../repositories/cart"
import { FlagRouter } from "@medusajs/utils"
import { IsNumber } from "class-validator"
import IsolateProductDomainFeatureFlag from "../loaders/feature-flags/isolate-product-domain"
import { LineItemRepository } from "../repositories/line-item" import { LineItemRepository } from "../repositories/line-item"
import { PaymentSessionInput } from "../types/payment" import { PaymentSessionInput } from "../types/payment"
import { PaymentSessionRepository } from "../repositories/payment-session" import { PaymentSessionRepository } from "../repositories/payment-session"
import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels"
import { ShippingMethodRepository } from "../repositories/shipping-method" import { ShippingMethodRepository } from "../repositories/shipping-method"
import { validateEmail } from "../utils/is-email" import { validateEmail } from "../utils/is-email"
@@ -2753,8 +2751,21 @@ class CartService extends TransactionBaseService {
} }
) )
cart.tax_total = cart.item_tax_total + cart.shipping_tax_total
cart.raw_discount_total = cart.discount_total
cart.discount_total = Math.round(cart.discount_total)
const giftCardableAmount = this.newTotalsService_.getGiftCardableAmount({
gift_cards_taxable: cart.region?.gift_cards_taxable,
subtotal: cart.subtotal,
discount_total: cart.discount_total,
shipping_total: cart.shipping_total,
tax_total: cart.tax_total,
})
const giftCardTotal = await this.newTotalsService_.getGiftCardTotals( const giftCardTotal = await this.newTotalsService_.getGiftCardTotals(
cart.subtotal - cart.discount_total, giftCardableAmount,
{ {
region: cart.region, region: cart.region,
giftCards: cart.gift_cards, giftCards: cart.gift_cards,
@@ -2764,11 +2775,6 @@ class CartService extends TransactionBaseService {
cart.gift_card_total = giftCardTotal.total || 0 cart.gift_card_total = giftCardTotal.total || 0
cart.gift_card_tax_total = giftCardTotal.tax_total || 0 cart.gift_card_tax_total = giftCardTotal.tax_total || 0
cart.tax_total = cart.item_tax_total + cart.shipping_tax_total
cart.raw_discount_total = cart.discount_total
cart.discount_total = Math.round(cart.discount_total)
cart.total = cart.total =
cart.subtotal + cart.subtotal +
cart.shipping_total + cart.shipping_total +
+21 -1
View File
@@ -1,5 +1,5 @@
import { FlagRouter } from "@medusajs/utils" import { FlagRouter } from "@medusajs/utils"
import { MedusaError, isDefined } from "medusa-core-utils" import { isDefined, MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import { import {
ITaxCalculationStrategy, ITaxCalculationStrategy,
@@ -635,6 +635,26 @@ export default class NewTotalsService extends TransactionBaseService {
return shippingMethodsTotals return shippingMethodsTotals
} }
getGiftCardableAmount({
gift_cards_taxable,
subtotal,
shipping_total,
discount_total,
tax_total,
}: {
gift_cards_taxable?: boolean
subtotal: number
shipping_total: number
discount_total: number
tax_total: number
}): number {
return (
(gift_cards_taxable
? subtotal + shipping_total - discount_total
: subtotal + shipping_total + tax_total - discount_total) || 0
)
}
/** /**
* Calculate and return the shipping method totals * Calculate and return the shipping method totals
* @param shippingMethod * @param shippingMethod
+16 -12
View File
@@ -725,12 +725,8 @@ class OrderService extends TransactionBaseService {
) )
} }
const giftCardableAmount = let giftCardableAmountBalance = cart.gift_card_total ?? 0
(cart.region?.gift_cards_taxable
? cart.subtotal! - cart.discount_total!
: cart.total! + cart.gift_card_total!) || 0 // we re add the gift card total to compensate the fact that the decorate total already removed this amount from the total
let giftCardableAmountBalance = giftCardableAmount
const giftCardService = this.giftCardService_.withTransaction(manager) const giftCardService = this.giftCardService_.withTransaction(manager)
// Order the gift cards by first ends_at date, then remaining amount. To ensure largest possible amount left, for longest possible time. // Order the gift cards by first ends_at date, then remaining amount. To ensure largest possible amount left, for longest possible time.
@@ -739,6 +735,7 @@ class OrderService extends TransactionBaseService {
const bEnd = b.ends_at ?? new Date(2100, 1, 1) const bEnd = b.ends_at ?? new Date(2100, 1, 1)
return aEnd.getTime() - bEnd.getTime() || a.balance - b.balance return aEnd.getTime() - bEnd.getTime() || a.balance - b.balance
}) })
for (const giftCard of orderedGiftCards) { for (const giftCard of orderedGiftCards) {
const newGiftCardBalance = Math.max( const newGiftCardBalance = Math.max(
0, 0,
@@ -1898,8 +1895,20 @@ class OrderService extends TransactionBaseService {
} }
) )
order.item_tax_total = item_tax_total
order.shipping_tax_total = shipping_tax_total
order.tax_total = item_tax_total + shipping_tax_total
const giftCardableAmount = this.newTotalsService_.getGiftCardableAmount({
gift_cards_taxable: order.region?.gift_cards_taxable,
subtotal: order.subtotal,
discount_total: order.discount_total,
shipping_total: order.shipping_total,
tax_total: order.tax_total,
})
const giftCardTotal = await this.newTotalsService_.getGiftCardTotals( const giftCardTotal = await this.newTotalsService_.getGiftCardTotals(
order.subtotal - order.discount_total, giftCardableAmount,
{ {
region: order.region, region: order.region,
giftCards: order.gift_cards, giftCards: order.gift_cards,
@@ -1909,12 +1918,7 @@ class OrderService extends TransactionBaseService {
order.gift_card_total = giftCardTotal.total || 0 order.gift_card_total = giftCardTotal.total || 0
order.gift_card_tax_total = giftCardTotal.tax_total || 0 order.gift_card_tax_total = giftCardTotal.tax_total || 0
order.item_tax_total = item_tax_total order.tax_total -= order.gift_card_tax_total
order.shipping_tax_total = shipping_tax_total
order.tax_total =
order.item_tax_total +
order.shipping_tax_total -
order.gift_card_tax_total
for (const swap of order.swaps ?? []) { for (const swap of order.swaps ?? []) {
swap.additional_items = swap.additional_items.map((item) => { swap.additional_items = swap.additional_items.map((item) => {