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:
Philip Korsholm
2022-11-07 10:47:26 +00:00
committed by GitHub
parent 434b11af46
commit 15d6f92964
4 changed files with 552 additions and 302 deletions
+518 -285
View File
@@ -18,335 +18,568 @@ const {
const {
simpleCustomerFactory,
} = require("../../factories/simple-customer-factory")
const {
default: startServerWithEnvironment,
} = require("../../../helpers/start-server-with-environment")
jest.setTimeout(30000)
describe("/admin/swaps", () => {
let medusaProcess
let dbConnection
describe("tax exclusive", () => {
let medusaProcess
let dbConnection
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", ".."))
dbConnection = await initDb({ cwd })
medusaProcess = await setupServer({ cwd })
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
medusaProcess.kill()
})
describe("GET /admin/swaps/:id", () => {
beforeEach(async () => {
await adminSeeder(dbConnection)
await orderSeeder(dbConnection)
await swapSeeder(dbConnection)
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", ".."))
dbConnection = await initDb({ cwd })
medusaProcess = await setupServer({ cwd })
})
afterEach(async () => {
afterAll(async () => {
const db = useDb()
await db.teardown()
await db.shutdown()
medusaProcess.kill()
})
it("gets a swap with cart and totals", async () => {
const api = useApi()
const response = await api
.get("/admin/swaps/test-swap", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
expect(response.data.swap).toEqual(
expect.objectContaining({
id: "test-swap",
})
)
expect(response.data.swap.cart).toEqual(
expect.objectContaining({
id: "test-cart-w-swap",
shipping_total: 1000,
subtotal: 1000,
total: 2000,
})
)
expect(response.data.swap.cart).toHaveProperty("discount_total")
expect(response.data.swap.cart).toHaveProperty("gift_card_total")
})
it("gets a swap with a discount", async () => {
const api = useApi()
const response = await api
.get("/admin/swaps/disc-swap", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
expect(response.data.swap).toEqual(
expect.objectContaining({
id: "disc-swap",
})
)
expect(response.data.swap.cart).toEqual(
expect.objectContaining({
id: "disc-swap-cart",
discount_total: -800,
shipping_total: 1000,
subtotal: -8000,
total: -6200,
})
)
})
})
describe("GET /admin/swaps/", () => {
beforeEach(async () => {
await adminSeeder(dbConnection)
await orderSeeder(dbConnection)
await swapSeeder(dbConnection)
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("lists all swaps", async () => {
const api = useApi()
const response = await api
.get("/admin/swaps/", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
expect(response.data).toHaveProperty("count")
expect(response.data.offset).toBe(0)
expect(response.data.limit).toBe(50)
expect(response.data.swaps).toContainEqual(
expect.objectContaining({
id: "test-swap",
})
)
})
})
describe("Complete swap flow", () => {
beforeEach(async () => {
try {
describe("GET /admin/swaps/:id", () => {
beforeEach(async () => {
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 *********
const prodA = await simpleProductFactory(dbConnection, {
id: "prod-a",
variants: [
{ id: "prod-a-var", prices: [{ amount: 1000, currency: "dkk" }] },
],
await orderSeeder(dbConnection)
await swapSeeder(dbConnection)
})
await simpleProductFactory(dbConnection, {
id: "prod-b",
variants: [
{ id: "prod-b-var", prices: [{ amount: 1000, currency: "dkk" }] },
],
afterEach(async () => {
const db = useDb()
await db.teardown()
})
await simpleRegionFactory(dbConnection, {
id: "test-region",
currency_code: "dkk",
})
it("gets a swap with cart and totals", async () => {
const api = useApi()
await simpleDiscountFactory(dbConnection, {
id: "test-discount",
regions: ["test-region"],
code: "TEST",
rule: {
type: "percentage",
value: "10",
allocation: "total",
conditions: [
{
type: "products",
operator: "in",
products: [prodA.id],
const response = await api
.get("/admin/swaps/test-swap", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
expect(response.data.swap).toEqual(
expect.objectContaining({
id: "test-swap",
})
)
expect(response.data.swap.cart).toEqual(
expect.objectContaining({
id: "test-cart-w-swap",
shipping_total: 1000,
subtotal: 1000,
total: 2000,
})
)
expect(response.data.swap.cart).toHaveProperty("discount_total")
expect(response.data.swap.cart).toHaveProperty("gift_card_total")
})
it("gets a swap with a discount", async () => {
const api = useApi()
const response = await api
.get("/admin/swaps/disc-swap", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
expect(response.data.swap).toEqual(
expect.objectContaining({
id: "disc-swap",
})
)
expect(response.data.swap.cart).toEqual(
expect.objectContaining({
id: "disc-swap-cart",
discount_total: -800,
shipping_total: 1000,
subtotal: -8000,
total: -6200,
})
)
})
})
describe("GET /admin/swaps/", () => {
beforeEach(async () => {
await adminSeeder(dbConnection)
await orderSeeder(dbConnection)
await swapSeeder(dbConnection)
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("lists all swaps", async () => {
const api = useApi()
const response = await api
.get("/admin/swaps/", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
expect(response.data).toHaveProperty("count")
expect(response.data.offset).toBe(0)
expect(response.data.limit).toBe(50)
expect(response.data.swaps).toContainEqual(
expect.objectContaining({
id: "test-swap",
})
)
})
})
describe("Complete swap flow", () => {
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 *********
const prodA = await simpleProductFactory(dbConnection, {
id: "prod-a",
variants: [
{ id: "prod-a-var", prices: [{ amount: 1000, currency: "dkk" }] },
],
},
})
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",
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",
})
await simpleProductFactory(dbConnection, {
id: "prod-b",
variants: [
{ id: "prod-b-var", prices: [{ amount: 1000, currency: "dkk" }] },
],
})
// ********* COMPLETE CART *********
const completedOrder = await api.post("/store/carts/cart-test/complete")
await simpleRegionFactory(dbConnection, {
id: "test-region",
currency_code: "dkk",
})
// ********* 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",
await simpleDiscountFactory(dbConnection, {
id: "test-discount",
regions: ["test-region"],
code: "TEST",
rule: {
type: "percentage",
value: "10",
allocation: "total",
conditions: [
{
type: "products",
operator: "in",
products: [prodA.id],
},
],
},
}
)
})
const fulfillmentId = fulfilledOrder.data.order.fulfillments[0].id
await simpleCustomerFactory(dbConnection, {
id: "test-customer",
email: "test@customer.com",
})
await api.post(
`/admin/orders/${orderId}/shipment`,
{
fulfillment_id: fulfillmentId,
},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
const so = await simpleShippingOptionFactory(dbConnection, {
region_id: "test-region",
})
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: [
await simpleCartFactory(dbConnection, {
customer: "test-customer",
id: "cart-test",
line_items: [
{
item_id: "line-item",
id: "line-item",
variant_id: "prod-a-var",
cart_id: "cart-test",
unit_price: 1000,
quantity: 1,
},
],
additional_items: [{ variant_id: "prod-b-var", quantity: 1 }],
},
{
headers: {
authorization: "Bearer test_token",
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)
}
)
let swap = createSwap.data.order.swaps[0]
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",
})
// ********* PREPARE SWAP CART *********
await api.post(`/store/carts/${swap.cart_id}/shipping-methods`, {
option_id: so.id,
data: {},
// ********* 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)
})
})
})
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
}
})
await api.post(`/store/carts/${swap.cart_id}/payment-sessions`)
await api.post(`/store/carts/${swap.cart_id}/payment-session`, {
provider_id: "test-pay",
afterEach(async () => {
const db = useDb()
await db.teardown()
})
// ********* COMPLETE SWAP CART *********
await api.post(`/store/carts/${swap.cart_id}/complete`)
it("completes swap and ensures difference due", async () => {
// ********* FACTORIES *********
await simpleRegionFactory(dbConnection, {
id: "test-region",
currency_code: "dkk",
includes_tax: true,
})
swap = await api
.get(`/admin/swaps/${swap.id}`, {
headers: {
Authorization: "Bearer test_token",
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",
},
})
.catch((err) => {
console.log(err)
await simpleCustomerFactory(dbConnection, {
id: "test-customer",
email: "test@customer.com",
})
const swapCart = await api.get(
`/store/carts/${swap.data.swap.cart_id}`,
{}
)
const so = await simpleShippingOptionFactory(dbConnection, {
region_id: "test-region",
})
// ********* VALIDATE *********
expect(swap.data.swap.difference_due).toBe(swapCart.data.cart.total)
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
title?: string
options?: { option_id: string; value: string }[]
prices?: { currency: string; amount: number }[]
prices?: { currency: string; amount: number, region_id?: string }[]
}
export const simpleProductVariantFactory = async (
@@ -32,7 +32,7 @@ export const simpleProductVariantFactory = async (
const toSave = manager.create(ProductVariant, {
id,
product_id: data.product_id,
sku: data.sku ?? null,
sku: data.sku ,
inventory_quantity:
typeof data.inventory_quantity !== "undefined"
? data.inventory_quantity
@@ -59,6 +59,7 @@ export const simpleProductVariantFactory = async (
variant_id: id,
currency_code: p.currency,
amount: p.amount,
region_id: p.region_id ,
})
}
@@ -204,7 +204,7 @@ describe("SwapService", () => {
refund_amount: 11,
items: [{ item_id: IdMap.getId("line"), quantity: 1 }],
},
additional_items: [{ data: "lines", id: "test" }],
additional_items: [{ id: "test" }],
other: "data",
}
@@ -316,10 +316,11 @@ describe("SwapService", () => {
expect(
LineItemAdjustmentServiceMock.createAdjustmentForLineItem
).toHaveBeenCalledWith(
{ id: "cart" },
{ id: "cart", items: [{
id: "test-item",
}] },
{
id: "test",
data: "lines",
id: "test-item",
}
)
+26 -11
View File
@@ -1,5 +1,5 @@
import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import { EntityManager, In } from "typeorm"
import { buildQuery, isDefined, setMetadata, validateId } from "../utils"
import { TransactionBaseService } from "../interfaces"
@@ -600,7 +600,7 @@ class SwapService extends TransactionBaseService {
order?.discounts?.filter(({ rule }) => rule.type !== "free_shipping") ||
undefined
const cart = await this.cartService_.withTransaction(manager).create({
let cart = await this.cartService_.withTransaction(manager).create({
discounts,
email: order.email,
billing_address_id: order.billing_address_id,
@@ -627,16 +627,31 @@ class SwapService extends TransactionBaseService {
const lineItemServiceTx = this.lineItemService_.withTransaction(manager)
const lineItemAdjustmentServiceTx =
this.lineItemAdjustmentService_.withTransaction(manager)
for (const item of swap.additional_items) {
await lineItemServiceTx.update(item.id, {
cart_id: cart.id,
})
// we generate adjustments in case the cart has any discounts that should be applied to the additional items
await lineItemAdjustmentServiceTx.createAdjustmentForLineItem(
cart,
item
await Promise.all(
swap.additional_items.map(
async (item) =>
await lineItemServiceTx.update(item.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
await lineItemAdjustmentServiceTx.createAdjustmentForLineItem(
cart,
item
)
})
)
// If the swap has a return shipping method the price has to be added to
// the cart.