fix(medusa): Double tax issue on return refund amount (#4899)
Closes #4686 Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
5
.changeset/bright-apples-prove.md
Normal file
5
.changeset/bright-apples-prove.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
fix(medusa): Double tax issue on return refund amount
|
||||
@@ -8,6 +8,7 @@ const { useDb } = require("../../../environment-helpers/use-db")
|
||||
const {
|
||||
simpleProductFactory,
|
||||
simpleOrderFactory,
|
||||
simpleShippingOptionFactory,
|
||||
} = require("../../../factories")
|
||||
const adminSeeder = require("../../../helpers/admin-seeder")
|
||||
|
||||
@@ -275,4 +276,62 @@ describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /store/carts", () => {
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("creates a store return with tax inclusive shipping option", async () => {
|
||||
await adminSeeder(dbConnection)
|
||||
|
||||
const order = await createReturnableOrder(dbConnection, {
|
||||
includes_tax: true,
|
||||
})
|
||||
const returnOption = await simpleShippingOptionFactory(dbConnection, {
|
||||
name: "Return method",
|
||||
region_id: "test-region",
|
||||
is_return: true,
|
||||
price: 1000,
|
||||
includes_tax: true,
|
||||
})
|
||||
|
||||
const api = useApi()
|
||||
|
||||
const response = await api.post(
|
||||
`/store/returns`,
|
||||
{
|
||||
order_id: order.id,
|
||||
return_shipping: {
|
||||
option_id: returnOption.id,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
item_id: "test-item",
|
||||
quantity: 1,
|
||||
note: "TOO SMALL",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: "Bearer test_token",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
|
||||
/*
|
||||
* Region has default tax rate 12.5 but line item has tax rate 20
|
||||
* therefore refund amount should be 1000 * 1.2 = 1200
|
||||
* shipping method will have tax inclusive price of 1000
|
||||
*/
|
||||
expect(response.data.return.refund_amount).toEqual(200)
|
||||
expect(response.data.return.items).toHaveLength(1)
|
||||
expect(response.data.return.items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
item_id: "test-item",
|
||||
quantity: 1,
|
||||
note: "TOO SMALL",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -189,6 +189,56 @@ describe("/admin/orders", () => {
|
||||
)
|
||||
})
|
||||
|
||||
test("creates a store return with tax exclusive shipping option", async () => {
|
||||
await adminSeeder(dbConnection)
|
||||
const order = await createReturnableOrder(dbConnection, { oldTaxes: false })
|
||||
const returnOption = await simpleShippingOptionFactory(dbConnection, {
|
||||
name: "Return method",
|
||||
region_id: "test-region",
|
||||
is_return: true,
|
||||
price: 1000,
|
||||
})
|
||||
|
||||
const api = useApi()
|
||||
|
||||
const response = await api.post(
|
||||
`/store/returns`,
|
||||
{
|
||||
order_id: order.id,
|
||||
return_shipping: {
|
||||
option_id: returnOption.id,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
item_id: "test-item",
|
||||
quantity: 1,
|
||||
note: "TOO SMALL",
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
|
||||
/*
|
||||
* Region has default tax rate 12.5 but line item has tax rate 20
|
||||
* therefore refund amount should be 1000 * 1.2 = 1200
|
||||
* shipping method will have 12.5 rate 1000 * 1.125 = 1125
|
||||
*/
|
||||
expect(response.data.return.refund_amount).toEqual(75)
|
||||
expect(response.data.return.items).toHaveLength(1)
|
||||
expect(response.data.return.items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
item_id: "test-item",
|
||||
quantity: 1,
|
||||
note: "TOO SMALL",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
test("creates a return w. discount", async () => {
|
||||
await adminSeeder(dbConnection)
|
||||
const order = await createReturnableOrder(dbConnection, {
|
||||
|
||||
@@ -147,6 +147,14 @@ export const ShippingOptionServiceMock = {
|
||||
return Promise.resolve({ _id: methodId })
|
||||
}),
|
||||
delete: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
createShippingMethod: jest.fn().mockImplementation((optionId, data, config) => {
|
||||
return Promise.resolve({
|
||||
...config,
|
||||
id: "test-shipping-method",
|
||||
shipping_option_id: optionId,
|
||||
data,
|
||||
})
|
||||
}),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import { FlagRouter } from "@medusajs/utils"
|
||||
|
||||
import idMap from "medusa-test-utils/dist/id-map"
|
||||
import ReturnService from "../return"
|
||||
import { ProductVariantInventoryServiceMock } from "../__mocks__/product-variant-inventory"
|
||||
import { ShippingOptionServiceMock } from "../__mocks__/shipping-option"
|
||||
|
||||
import TaxInclusivePricingFeatureFlag from "../../loaders/feature-flags/tax-inclusive-pricing"
|
||||
|
||||
describe("ReturnService", () => {
|
||||
describe("receive", () => {
|
||||
const returnRepository = MockRepository({
|
||||
@@ -334,4 +340,161 @@ describe("ReturnService", () => {
|
||||
).rejects.toThrow("Cannot update a canceled return")
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
const returnRepository = MockRepository({
|
||||
findOne: (query) => {
|
||||
switch (query.where.id) {
|
||||
case IdMap.getId("test-return"):
|
||||
return Promise.resolve({
|
||||
status: "canceled",
|
||||
})
|
||||
default:
|
||||
return Promise.resolve({})
|
||||
}
|
||||
},
|
||||
create: (data) => data,
|
||||
save: (data) => data,
|
||||
})
|
||||
|
||||
const returnItemRepository = MockRepository({
|
||||
create: (data) => data,
|
||||
})
|
||||
|
||||
const totalsService = {
|
||||
getTotal: jest.fn().mockImplementation((cart) => {
|
||||
return 1000
|
||||
}),
|
||||
getRefundTotal: jest.fn().mockImplementation((order, lineItems) => {
|
||||
return 100
|
||||
}),
|
||||
getCalculationContext: jest
|
||||
.fn()
|
||||
.mockImplementation((order, lineItems) => {
|
||||
return Promise.resolve({})
|
||||
}),
|
||||
}
|
||||
|
||||
const orderService = {
|
||||
retrieve: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
items: [
|
||||
{
|
||||
id: IdMap.getId("test-line"),
|
||||
quantity: 10,
|
||||
returned_quantity: 0,
|
||||
variant_id: "test-variant",
|
||||
},
|
||||
{
|
||||
id: IdMap.getId("test-line-2"),
|
||||
quantity: 10,
|
||||
returned_quantity: 0,
|
||||
variant_id: "test-variant-2",
|
||||
},
|
||||
],
|
||||
payments: [{ id: "payment_test" }],
|
||||
})
|
||||
}),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const lineItemService = {
|
||||
retrieve: jest.fn().mockImplementation((data) => {
|
||||
return Promise.resolve({ ...data, returned_quantity: 0 })
|
||||
}),
|
||||
update: jest.fn(),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const returnReasonService = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
list: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve([
|
||||
{
|
||||
id: IdMap.getId("test-return-reason"),
|
||||
value: "test-return-reason",
|
||||
label: "Test Return Reason",
|
||||
description: null,
|
||||
parent_return_reason_id: null,
|
||||
return_reason_children: [],
|
||||
metadata: {},
|
||||
},
|
||||
])
|
||||
}, {}),
|
||||
}
|
||||
|
||||
const shippingOptionService = ShippingOptionServiceMock
|
||||
const taxProviderService = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
createShippingTaxLines: jest.fn().mockImplementation((shippingMethod) => {
|
||||
return Promise.resolve([
|
||||
{
|
||||
rate: 25,
|
||||
},
|
||||
])
|
||||
}),
|
||||
}
|
||||
|
||||
const featureFlagRouter = new FlagRouter({
|
||||
[TaxInclusivePricingFeatureFlag.key]: false,
|
||||
})
|
||||
|
||||
const returnService = new ReturnService({
|
||||
manager: MockManager,
|
||||
lineItemService,
|
||||
orderService,
|
||||
totalsService,
|
||||
returnReasonService,
|
||||
returnRepository,
|
||||
returnItemRepository,
|
||||
shippingOptionService,
|
||||
taxProviderService,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully creates a return", async () => {
|
||||
await returnService.create({
|
||||
order_id: IdMap.getId("test-order"),
|
||||
items: [
|
||||
{
|
||||
item_id: IdMap.getId("test-line"),
|
||||
quantity: 10,
|
||||
},
|
||||
],
|
||||
shipping_method: {
|
||||
option_id: "taxincl-option",
|
||||
price: 80,
|
||||
},
|
||||
})
|
||||
|
||||
expect(returnRepository.save).toHaveBeenCalledTimes(2)
|
||||
expect(returnRepository.save).toHaveBeenCalledWith({
|
||||
order_id: IdMap.getId("test-order"),
|
||||
items: [
|
||||
{
|
||||
item_id: IdMap.getId("test-line"),
|
||||
quantity: 10,
|
||||
metadata: undefined,
|
||||
note: undefined,
|
||||
reason_id: undefined,
|
||||
requested_quantity: 10,
|
||||
},
|
||||
],
|
||||
status: "requested",
|
||||
refund_amount: 0,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,7 +21,9 @@ import {
|
||||
ReturnStatus,
|
||||
} from "../models"
|
||||
import { MedusaError, isDefined } from "medusa-core-utils"
|
||||
import { buildQuery, setMetadata } from "../utils"
|
||||
import { FlagRouter } from "@medusajs/utils"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import { buildQuery, setMetadata, calculatePriceTaxAmount } from "../utils"
|
||||
|
||||
import { OrdersReturnItem } from "../types/orders"
|
||||
import { ReturnItemRepository } from "../repositories/return-item"
|
||||
@@ -40,6 +42,7 @@ type InjectedDependencies = {
|
||||
fulfillmentProviderService: FulfillmentProviderService
|
||||
orderService: OrderService
|
||||
productVariantInventoryService: ProductVariantInventoryService
|
||||
featureFlagRouter: FlagRouter
|
||||
}
|
||||
|
||||
type Transformer = (
|
||||
@@ -60,6 +63,7 @@ class ReturnService extends TransactionBaseService {
|
||||
protected readonly orderService_: OrderService
|
||||
// eslint-disable-next-line
|
||||
protected readonly productVariantInventoryService_: ProductVariantInventoryService
|
||||
protected readonly featureFlagRouter_: FlagRouter
|
||||
|
||||
constructor({
|
||||
totalsService,
|
||||
@@ -72,6 +76,7 @@ class ReturnService extends TransactionBaseService {
|
||||
fulfillmentProviderService,
|
||||
orderService,
|
||||
productVariantInventoryService,
|
||||
featureFlagRouter,
|
||||
}: InjectedDependencies) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
@@ -86,6 +91,7 @@ class ReturnService extends TransactionBaseService {
|
||||
this.returnReasonService_ = returnReasonService
|
||||
this.orderService_ = orderService
|
||||
this.productVariantInventoryService_ = productVariantInventoryService
|
||||
this.featureFlagRouter_ = featureFlagRouter
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -486,11 +492,33 @@ class ReturnService extends TransactionBaseService {
|
||||
.withTransaction(manager)
|
||||
.createShippingTaxLines(shippingMethod, calculationContext)
|
||||
|
||||
const includesTax =
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
) && shippingMethod.includes_tax
|
||||
|
||||
const taxRate = taxLines.reduce((acc, curr) => {
|
||||
return acc + curr.rate / 100
|
||||
}, 0)
|
||||
|
||||
const taxAmountIncludedInPrice = !includesTax
|
||||
? 0
|
||||
: Math.round(
|
||||
calculatePriceTaxAmount({
|
||||
price: shippingMethod.price,
|
||||
taxRate,
|
||||
includesTax,
|
||||
})
|
||||
)
|
||||
|
||||
const shippingPriceWithoutTax =
|
||||
shippingMethod.price - taxAmountIncludedInPrice
|
||||
|
||||
const shippingTotal =
|
||||
shippingMethod.price +
|
||||
shippingPriceWithoutTax +
|
||||
taxLines.reduce(
|
||||
(acc, tl) =>
|
||||
acc + Math.round(shippingMethod.price * (tl.rate / 100)),
|
||||
acc + Math.round(shippingPriceWithoutTax * (tl.rate / 100)),
|
||||
0
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user