Fix(medusa): Swaps in tax inclusive regions (#2557)
**What** - Ensure that swaps can be created for orders with discounts in tax inclusive regions **How** - Retrieve cart with discounts and region before creating adjustments for line items in cart. - In `discountService.calculateDiscountForLineItem` we used the method `totalsService.getLineItemTotals` if a line-item is tax inclusive. This method uses some fields from the cart that aren't populated on cart creation (such a `items` which caused the original error). **Testing** - add integration test `swaps > tax inclusive > "Complete swap flow with discount"` that creates a swap in the following environment - tax inclusive region - tax inclusive line item to be swapped - fixed type discount with allocation: total Fixes CORE-748
This commit is contained in:
1 parent
434b11af46
commit
15d6f92964
4 files changed
+260
-10
No files matched your search
@@ -18,10 +18,14 @@ const {
|
|||||||
const {
|
const {
|
||||||
simpleCustomerFactory,
|
simpleCustomerFactory,
|
||||||
} = require("../../factories/simple-customer-factory")
|
} = require("../../factories/simple-customer-factory")
|
||||||
|
const {
|
||||||
|
default: startServerWithEnvironment,
|
||||||
|
} = require("../../../helpers/start-server-with-environment")
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
describe("/admin/swaps", () => {
|
describe("/admin/swaps", () => {
|
||||||
|
describe("tax exclusive", () => {
|
||||||
let medusaProcess
|
let medusaProcess
|
||||||
let dbConnection
|
let dbConnection
|
||||||
|
|
||||||
@@ -349,4 +353,233 @@ describe("/admin/swaps", () => {
|
|||||||
expect(swap.data.swap.difference_due).toBe(swapCart.data.cart.total)
|
expect(swap.data.swap.difference_due).toBe(swapCart.data.cart.total)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
describe("tax inclusive", () => {
|
||||||
|
let medusaProcess
|
||||||
|
let dbConnection
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||||
|
const [process, connection] = await startServerWithEnvironment({
|
||||||
|
cwd,
|
||||||
|
env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true },
|
||||||
|
verbose: false,
|
||||||
|
})
|
||||||
|
dbConnection = connection
|
||||||
|
medusaProcess = process
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
medusaProcess.kill()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Complete swap flow with discount", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
try {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("completes swap and ensures difference due", async () => {
|
||||||
|
// ********* FACTORIES *********
|
||||||
|
await simpleRegionFactory(dbConnection, {
|
||||||
|
id: "test-region",
|
||||||
|
currency_code: "dkk",
|
||||||
|
includes_tax: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const prodA = await simpleProductFactory(dbConnection, {
|
||||||
|
id: "prod-a",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: "prod-a-var",
|
||||||
|
prices: [
|
||||||
|
{ amount: 1000, currency: "dkk", region_id: "test-region" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
await simpleProductFactory(dbConnection, {
|
||||||
|
id: "prod-b",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: "prod-b-var",
|
||||||
|
prices: [
|
||||||
|
{ amount: 1000, currency: "dkk", region_id: "test-region" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
await simpleDiscountFactory(dbConnection, {
|
||||||
|
id: "test-discount",
|
||||||
|
regions: ["test-region"],
|
||||||
|
code: "TEST",
|
||||||
|
rule: {
|
||||||
|
type: "fixed",
|
||||||
|
value: "10",
|
||||||
|
allocation: "total",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await simpleCustomerFactory(dbConnection, {
|
||||||
|
id: "test-customer",
|
||||||
|
email: "test@customer.com",
|
||||||
|
})
|
||||||
|
|
||||||
|
const so = await simpleShippingOptionFactory(dbConnection, {
|
||||||
|
region_id: "test-region",
|
||||||
|
})
|
||||||
|
|
||||||
|
await simpleCartFactory(dbConnection, {
|
||||||
|
customer: "test-customer",
|
||||||
|
id: "cart-test",
|
||||||
|
line_items: [
|
||||||
|
{
|
||||||
|
id: "line-item",
|
||||||
|
includes_tax: true,
|
||||||
|
variant_id: "prod-a-var",
|
||||||
|
cart_id: "cart-test",
|
||||||
|
unit_price: 1000,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
region: "test-region",
|
||||||
|
shipping_address: {
|
||||||
|
address_1: "test",
|
||||||
|
country_code: "us",
|
||||||
|
first_name: "chris",
|
||||||
|
last_name: "rock",
|
||||||
|
postal_code: "101",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
// ********* PREPARE CART *********
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.post("/store/carts/cart-test", {
|
||||||
|
discounts: [{ code: "TEST" }],
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.post("/store/carts/cart-test/shipping-methods", {
|
||||||
|
option_id: so.id,
|
||||||
|
data: {},
|
||||||
|
})
|
||||||
|
await api.post("/store/carts/cart-test/payment-sessions")
|
||||||
|
await api.post("/store/carts/cart-test/payment-session", {
|
||||||
|
provider_id: "test-pay",
|
||||||
|
})
|
||||||
|
|
||||||
|
// ********* COMPLETE CART *********
|
||||||
|
const completedOrder = await api.post("/store/carts/cart-test/complete")
|
||||||
|
|
||||||
|
// ********* PREPARE ORDER *********
|
||||||
|
const orderId = completedOrder.data.data.id
|
||||||
|
const fulfilledOrder = await api.post(
|
||||||
|
`/admin/orders/${orderId}/fulfillment`,
|
||||||
|
{
|
||||||
|
items: [{ item_id: "line-item", quantity: 1 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const fulfillmentId = fulfilledOrder.data.order.fulfillments[0].id
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/orders/${orderId}/shipment`,
|
||||||
|
{
|
||||||
|
fulfillment_id: fulfillmentId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/orders/${orderId}/capture`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ********* CREATE SWAP *********
|
||||||
|
const createSwap = await api.post(
|
||||||
|
`/admin/orders/${completedOrder.data.data.id}/swaps`,
|
||||||
|
{
|
||||||
|
return_items: [
|
||||||
|
{
|
||||||
|
item_id: "line-item",
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
additional_items: [{ variant_id: "prod-b-var", quantity: 1 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
authorization: "Bearer test_token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
let swap = createSwap.data.order.swaps[0]
|
||||||
|
|
||||||
|
// ********* PREPARE SWAP CART *********
|
||||||
|
await api.post(`/store/carts/${swap.cart_id}/shipping-methods`, {
|
||||||
|
option_id: so.id,
|
||||||
|
data: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
await api.post(`/store/carts/${swap.cart_id}/payment-sessions`)
|
||||||
|
await api.post(`/store/carts/${swap.cart_id}/payment-session`, {
|
||||||
|
provider_id: "test-pay",
|
||||||
|
})
|
||||||
|
|
||||||
|
// ********* COMPLETE SWAP CART *********
|
||||||
|
await api.post(`/store/carts/${swap.cart_id}/complete`)
|
||||||
|
|
||||||
|
swap = await api
|
||||||
|
.get(`/admin/swaps/${swap.id}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
const swapCart = await api.get(
|
||||||
|
`/store/carts/${swap.data.swap.cart_id}`,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ********* VALIDATE *********
|
||||||
|
expect(swap.data.swap.difference_due).toBe(swapCart.data.cart.total)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -14,7 +14,7 @@ export type ProductVariantFactoryData = {
|
|||||||
inventory_quantity?: number
|
inventory_quantity?: number
|
||||||
title?: string
|
title?: string
|
||||||
options?: { option_id: string; value: string }[]
|
options?: { option_id: string; value: string }[]
|
||||||
prices?: { currency: string; amount: number }[]
|
prices?: { currency: string; amount: number, region_id?: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const simpleProductVariantFactory = async (
|
export const simpleProductVariantFactory = async (
|
||||||
@@ -32,7 +32,7 @@ export const simpleProductVariantFactory = async (
|
|||||||
const toSave = manager.create(ProductVariant, {
|
const toSave = manager.create(ProductVariant, {
|
||||||
id,
|
id,
|
||||||
product_id: data.product_id,
|
product_id: data.product_id,
|
||||||
sku: data.sku ?? null,
|
sku: data.sku ,
|
||||||
inventory_quantity:
|
inventory_quantity:
|
||||||
typeof data.inventory_quantity !== "undefined"
|
typeof data.inventory_quantity !== "undefined"
|
||||||
? data.inventory_quantity
|
? data.inventory_quantity
|
||||||
@@ -59,6 +59,7 @@ export const simpleProductVariantFactory = async (
|
|||||||
variant_id: id,
|
variant_id: id,
|
||||||
currency_code: p.currency,
|
currency_code: p.currency,
|
||||||
amount: p.amount,
|
amount: p.amount,
|
||||||
|
region_id: p.region_id ,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ describe("SwapService", () => {
|
|||||||
refund_amount: 11,
|
refund_amount: 11,
|
||||||
items: [{ item_id: IdMap.getId("line"), quantity: 1 }],
|
items: [{ item_id: IdMap.getId("line"), quantity: 1 }],
|
||||||
},
|
},
|
||||||
additional_items: [{ data: "lines", id: "test" }],
|
additional_items: [{ id: "test" }],
|
||||||
other: "data",
|
other: "data",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,10 +316,11 @@ describe("SwapService", () => {
|
|||||||
expect(
|
expect(
|
||||||
LineItemAdjustmentServiceMock.createAdjustmentForLineItem
|
LineItemAdjustmentServiceMock.createAdjustmentForLineItem
|
||||||
).toHaveBeenCalledWith(
|
).toHaveBeenCalledWith(
|
||||||
{ id: "cart" },
|
{ id: "cart", items: [{
|
||||||
|
id: "test-item",
|
||||||
|
}] },
|
||||||
{
|
{
|
||||||
id: "test",
|
id: "test-item",
|
||||||
data: "lines",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MedusaError } from "medusa-core-utils"
|
import { MedusaError } from "medusa-core-utils"
|
||||||
import { EntityManager } from "typeorm"
|
import { EntityManager, In } from "typeorm"
|
||||||
|
|
||||||
import { buildQuery, isDefined, setMetadata, validateId } from "../utils"
|
import { buildQuery, isDefined, setMetadata, validateId } from "../utils"
|
||||||
import { TransactionBaseService } from "../interfaces"
|
import { TransactionBaseService } from "../interfaces"
|
||||||
@@ -600,7 +600,7 @@ class SwapService extends TransactionBaseService {
|
|||||||
order?.discounts?.filter(({ rule }) => rule.type !== "free_shipping") ||
|
order?.discounts?.filter(({ rule }) => rule.type !== "free_shipping") ||
|
||||||
undefined
|
undefined
|
||||||
|
|
||||||
const cart = await this.cartService_.withTransaction(manager).create({
|
let cart = await this.cartService_.withTransaction(manager).create({
|
||||||
discounts,
|
discounts,
|
||||||
email: order.email,
|
email: order.email,
|
||||||
billing_address_id: order.billing_address_id,
|
billing_address_id: order.billing_address_id,
|
||||||
@@ -627,16 +627,31 @@ class SwapService extends TransactionBaseService {
|
|||||||
const lineItemServiceTx = this.lineItemService_.withTransaction(manager)
|
const lineItemServiceTx = this.lineItemService_.withTransaction(manager)
|
||||||
const lineItemAdjustmentServiceTx =
|
const lineItemAdjustmentServiceTx =
|
||||||
this.lineItemAdjustmentService_.withTransaction(manager)
|
this.lineItemAdjustmentService_.withTransaction(manager)
|
||||||
for (const item of swap.additional_items) {
|
|
||||||
|
await Promise.all(
|
||||||
|
swap.additional_items.map(
|
||||||
|
async (item) =>
|
||||||
await lineItemServiceTx.update(item.id, {
|
await lineItemServiceTx.update(item.id, {
|
||||||
cart_id: cart.id,
|
cart_id: cart.id,
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
cart = await this.cartService_
|
||||||
|
.withTransaction(manager)
|
||||||
|
.retrieve(cart.id, {
|
||||||
|
relations: ["items", "region", "discounts", "discounts.rule"],
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
cart.items.map(async (item) => {
|
||||||
// we generate adjustments in case the cart has any discounts that should be applied to the additional items
|
// we generate adjustments in case the cart has any discounts that should be applied to the additional items
|
||||||
await lineItemAdjustmentServiceTx.createAdjustmentForLineItem(
|
await lineItemAdjustmentServiceTx.createAdjustmentForLineItem(
|
||||||
cart,
|
cart,
|
||||||
item
|
item
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
)
|
||||||
|
|
||||||
// If the swap has a return shipping method the price has to be added to
|
// If the swap has a return shipping method the price has to be added to
|
||||||
// the cart.
|
// the cart.
|
||||||
|
|||||||
Reference in new issue
Block a user