diff --git a/integration-tests/api/__tests__/admin/swaps.js b/integration-tests/api/__tests__/admin/swaps.js index be1a8ec594..d3db302bbd 100644 --- a/integration-tests/api/__tests__/admin/swaps.js +++ b/integration-tests/api/__tests__/admin/swaps.js @@ -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) + }) }) }) }) diff --git a/integration-tests/api/factories/simple-product-variant-factory.ts b/integration-tests/api/factories/simple-product-variant-factory.ts index fe04c38adc..7c008519e3 100644 --- a/integration-tests/api/factories/simple-product-variant-factory.ts +++ b/integration-tests/api/factories/simple-product-variant-factory.ts @@ -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 , }) } diff --git a/packages/medusa/src/services/__tests__/swap.ts b/packages/medusa/src/services/__tests__/swap.ts index 9b100eac03..356899395b 100644 --- a/packages/medusa/src/services/__tests__/swap.ts +++ b/packages/medusa/src/services/__tests__/swap.ts @@ -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", } ) diff --git a/packages/medusa/src/services/swap.ts b/packages/medusa/src/services/swap.ts index 8a51d26497..ed7f0fe718 100644 --- a/packages/medusa/src/services/swap.ts +++ b/packages/medusa/src/services/swap.ts @@ -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.