diff --git a/integration-tests/api/__tests__/admin/swaps.js b/integration-tests/api/__tests__/admin/swaps.js new file mode 100644 index 0000000000..7c3ae863f1 --- /dev/null +++ b/integration-tests/api/__tests__/admin/swaps.js @@ -0,0 +1,155 @@ +const { dropDatabase } = require("pg-god"); +const path = require("path"); + +const setupServer = require("../../../helpers/setup-server"); +const { useApi } = require("../../../helpers/use-api"); +const { initDb } = require("../../../helpers/use-db"); + +const orderSeeder = require("../../helpers/order-seeder"); +const swapSeeder = require("../../helpers/swap-seeder"); +const adminSeeder = require("../../helpers/admin-seeder"); + +jest.setTimeout(30000); + +describe("/admin/swaps", () => { + let medusaProcess; + let dbConnection; + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..")); + dbConnection = await initDb({ cwd }); + medusaProcess = await setupServer({ cwd }); + }); + + afterAll(async () => { + await dbConnection.close(); + await dropDatabase({ databaseName: "medusa-integration" }); + + medusaProcess.kill(); + }); + + describe("GET /admin/swaps/:id", () => { + beforeEach(async () => { + try { + await adminSeeder(dbConnection); + await orderSeeder(dbConnection); + await swapSeeder(dbConnection); + } catch (err) { + throw err; + } + }); + + afterEach(async () => { + const manager = dbConnection.manager; + await manager.query(`DELETE FROM "return_item"`); + await manager.query(`DELETE FROM "return"`); + await manager.query(`DELETE FROM "shipping_method"`); + await manager.query(`DELETE FROM "line_item"`); + await manager.query(`DELETE FROM "payment"`); + await manager.query(`DELETE FROM "swap"`); + await manager.query(`DELETE FROM "cart"`); + await manager.query(`DELETE FROM "money_amount"`); + await manager.query(`DELETE FROM "product_variant"`); + await manager.query(`DELETE FROM "product"`); + await manager.query(`DELETE FROM "shipping_option"`); + await manager.query(`DELETE FROM "discount"`); + await manager.query(`DELETE FROM "order"`); + await manager.query(`DELETE FROM "customer"`); + await manager.query( + `UPDATE "country" SET region_id=NULL WHERE iso_2 = 'us'` + ); + await manager.query(`DELETE FROM "region"`); + await manager.query(`DELETE FROM "user"`); + }); + + 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", + shipping_total: 1000, + subtotal: 1000, + total: 2000, + }) + ); + expect(response.data.swap.cart).toHaveProperty("discount_total"); + expect(response.data.swap.cart).toHaveProperty("gift_card_total"); + }); + }); + + describe("GET /admin/swaps/", () => { + beforeEach(async () => { + try { + await adminSeeder(dbConnection); + await orderSeeder(dbConnection); + await swapSeeder(dbConnection); + } catch (err) { + throw err; + } + }); + + afterEach(async () => { + const manager = dbConnection.manager; + await manager.query(`DELETE FROM "return_item"`); + await manager.query(`DELETE FROM "return"`); + await manager.query(`DELETE FROM "shipping_method"`); + await manager.query(`DELETE FROM "line_item"`); + await manager.query(`DELETE FROM "payment"`); + await manager.query(`DELETE FROM "swap"`); + await manager.query(`DELETE FROM "cart"`); + await manager.query(`DELETE FROM "money_amount"`); + await manager.query(`DELETE FROM "product_variant"`); + await manager.query(`DELETE FROM "product"`); + await manager.query(`DELETE FROM "shipping_option"`); + await manager.query(`DELETE FROM "discount"`); + await manager.query(`DELETE FROM "order"`); + await manager.query(`DELETE FROM "customer"`); + await manager.query( + `UPDATE "country" SET region_id=NULL WHERE iso_2 = 'us'` + ); + await manager.query(`DELETE FROM "region"`); + await manager.query(`DELETE FROM "user"`); + }); + + 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", + }) + ); + }); + }); +}); diff --git a/integration-tests/api/helpers/swap-seeder.js b/integration-tests/api/helpers/swap-seeder.js index b9cfe39012..6d744421ed 100644 --- a/integration-tests/api/helpers/swap-seeder.js +++ b/integration-tests/api/helpers/swap-seeder.js @@ -11,6 +11,7 @@ const { Region, Order, Swap, + Cart, Return, } = require("@medusajs/medusa"); @@ -52,16 +53,64 @@ module.exports = async (connection, data = {}) => { orderWithSwap = await manager.save(orderWithSwap); + const cart = manager.create(Cart, { + id: "test-cart", + customer_id: "test-customer", + email: "test-customer@email.com", + shipping_address_id: "test-shipping-address", + billing_address_id: "test-billing-address", + region_id: "test-region", + type: "swap", + metadata: { + swap_id: "test-swap", + parent_order_id: orderWithSwap.id, + }, + }); + + await manager.save(cart); + + const swap = manager.create(Swap, { + id: "test-swap", + order_id: "order-with-swap", + payment_status: "captured", + fulfillment_status: "fulfilled", + cart_id: "test-cart", + payment: { + id: "test-payment-swap", + amount: 10000, + currency_code: "usd", + amount_refunded: 0, + provider_id: "test", + data: {}, + }, + additional_items: [ + { + id: "test-item-swapped", + fulfilled_quantity: 1, + title: "Line Item", + description: "Line Item Desc", + thumbnail: "https://test.js/1234", + unit_price: 9000, + quantity: 1, + variant_id: "test-variant-2", + cart_id: "test-cart", + }, + ], + }); + + await manager.save(swap); + const li = manager.create(LineItem, { - id: "test-item-2", + id: "return-item-1", fulfilled_quantity: 1, - title: "Line Item", + title: "Return Line Item", description: "Line Item Desc", thumbnail: "https://test.js/1234", unit_price: 8000, quantity: 1, variant_id: "test-variant", order_id: orderWithSwap.id, + cart_id: cart.id, }); await manager.save(li); @@ -80,34 +129,30 @@ module.exports = async (connection, data = {}) => { await manager.save(li2); - const swap = manager.create(Swap, { - id: "test-swap", - order_id: "order-with-swap", - payment_status: "captured", - fulfillment_status: "fulfilled", - payment: { - id: "test-payment-swap", - amount: 10000, - currency_code: "usd", - amount_refunded: 0, - provider_id: "test", - data: {}, - }, - additional_items: [ - { - id: "test-item-swapped", - fulfilled_quantity: 1, - title: "Line Item", - description: "Line Item Desc", - thumbnail: "https://test.js/1234", - unit_price: 8000, - quantity: 1, - variant_id: "test-variant-2", - }, - ], + const swapReturn = await manager.create(Return, { + swap_id: swap.id, + order_id: orderWithSwap.id, + item_id: li.id, + refund_amount: li.quantity * li.unit_price, + // shipping_method_id: , }); - await manager.save(swap); + await manager.save(swapReturn); + + const return_item1 = manager.create(LineItem, { + ...li, + unit_price: -1 * li.unit_price, + }); + + await manager.save(return_item1); + + await manager.insert(ShippingMethod, { + id: "another-test-method", + shipping_option_id: "test-option", + cart_id: "test-cart", + price: 1000, + data: {}, + }); const swapOnSwap = manager.create(Swap, { id: "swap-on-swap", diff --git a/packages/medusa/src/api/routes/admin/swaps/__tests__/get-swap.js b/packages/medusa/src/api/routes/admin/swaps/__tests__/get-swap.js new file mode 100644 index 0000000000..34273ce7ac --- /dev/null +++ b/packages/medusa/src/api/routes/admin/swaps/__tests__/get-swap.js @@ -0,0 +1,78 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { SwapServiceMock } from "../../../../../services/__mocks__/swap" + +const defaultRelations = [ + "order", + "additional_items", + "return_order", + "fulfillments", + "payment", + "shipping_address", + "shipping_methods", + "cart", + "cart.items", + "cart.region", + "cart.shipping_methods", + "cart.gift_cards", + "cart.discounts", + "cart.payment", +] + +const defaultFields = [ + "id", + "fulfillment_status", + "payment_status", + "order_id", + "difference_due", + "cart_id", + "created_at", + "updated_at", + "metadata", + "cart.subtotal", + "cart.tax_total", + "cart.shipping_total", + "cart.discount_total", + "cart.gift_card_total", + "cart.total", +] + +describe("GET /admin/swaps/:id", () => { + describe("successfully gets a swap", () => { + let subject + + beforeAll(async () => { + subject = await request( + "GET", + `/admin/swaps/${IdMap.getId("test-swap")}`, + { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls swapService retrieve", () => { + expect(SwapServiceMock.retrieve).toHaveBeenCalledTimes(1) + expect(SwapServiceMock.retrieve).toHaveBeenCalledWith( + IdMap.getId("test-swap"), + { + select: defaultFields, + relations: defaultRelations, + } + ) + }) + + it("returns swap", () => { + expect(subject.status).toEqual(200) + expect(subject.body.swap.id).toEqual(IdMap.getId("test-swap")) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/swaps/__tests__/list-swaps.js b/packages/medusa/src/api/routes/admin/swaps/__tests__/list-swaps.js new file mode 100644 index 0000000000..200f800957 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/swaps/__tests__/list-swaps.js @@ -0,0 +1,48 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { SwapServiceMock } from "../../../../../services/__mocks__/swap" + +const defaultListOptions = { + take: 50, + skip: 0, + order: { created_at: "DESC" }, +} + +describe("GET /admin/swaps/", () => { + describe("successfully lists swaps", () => { + let subject + + beforeAll(async () => { + subject = await request("GET", `/admin/swaps/`, { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + }) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls swapService list with default pagination and sorting options", () => { + expect(SwapServiceMock.list).toHaveBeenCalledTimes(1) + expect(SwapServiceMock.list).toHaveBeenCalledWith( + {}, + { + ...defaultListOptions, + } + ) + }) + + it("returns swaps", () => { + expect(subject.status).toEqual(200) + expect(subject.body.count).toBe(2) + expect(subject.body.swaps).toEqual([ + { id: IdMap.getId("test-swap") }, + { id: IdMap.getId("test-swap-1") }, + ]) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/swaps/get-swap.js b/packages/medusa/src/api/routes/admin/swaps/get-swap.js index 114a641a69..1f1b3cefe2 100644 --- a/packages/medusa/src/api/routes/admin/swaps/get-swap.js +++ b/packages/medusa/src/api/routes/admin/swaps/get-swap.js @@ -1,23 +1,36 @@ +import { defaultFields, defaultRelations } from "./" + +/** + * @oas [get] /swaps/{id} + * operationId: "GetSwapsSwap" + * summary: "Retrieve a Swap" + * description: "Retrieves a Swap." + * parameters: + * - (path) id=* {string} The id of the Swap. + * tags: + * - Swap + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * swap: + * $ref: "#/components/schemas/swap" + */ export default async (req, res) => { const { id } = req.params try { - const orderService = req.scope.resolve("orderService") + const swapService = req.scope.resolve("swapService") - const order = await orderService.retrieve(id, { - relations: [ - "order", - "additional_items", - "return_order", - "fulfillments", - "payment", - "shipping_address", - "shipping_methods", - "cart", - ], + const swap = await swapService.retrieve(id, { + select: defaultFields, + relations: defaultRelations, }) - res.json({ order }) + res.json({ swap }) } catch (error) { throw error } diff --git a/packages/medusa/src/api/routes/admin/swaps/index.js b/packages/medusa/src/api/routes/admin/swaps/index.js index 541bff2f10..00bc25a0db 100644 --- a/packages/medusa/src/api/routes/admin/swaps/index.js +++ b/packages/medusa/src/api/routes/admin/swaps/index.js @@ -18,3 +18,38 @@ export default app => { return app } + +export const defaultRelations = [ + "order", + "additional_items", + "return_order", + "fulfillments", + "payment", + "shipping_address", + "shipping_methods", + "cart", + "cart.items", + "cart.region", + "cart.shipping_methods", + "cart.gift_cards", + "cart.discounts", + "cart.payment", +] + +export const defaultFields = [ + "id", + "fulfillment_status", + "payment_status", + "order_id", + "difference_due", + "cart_id", + "created_at", + "updated_at", + "metadata", + "cart.subtotal", + "cart.tax_total", + "cart.shipping_total", + "cart.discount_total", + "cart.gift_card_total", + "cart.total", +] diff --git a/packages/medusa/src/api/routes/store/carts/__tests__/complete-cart.js b/packages/medusa/src/api/routes/store/carts/__tests__/complete-cart.js index 489ecc6c4f..f973c3dbf5 100644 --- a/packages/medusa/src/api/routes/store/carts/__tests__/complete-cart.js +++ b/packages/medusa/src/api/routes/store/carts/__tests__/complete-cart.js @@ -78,7 +78,7 @@ describe("POST /store/carts/:id", () => { }) it("returns the created order", () => { - expect(subject.body.data.id).toEqual("test-swap") + expect(subject.body.data.id).toEqual(IdMap.getId("test-swap")) }) }) diff --git a/packages/medusa/src/services/__mocks__/swap.js b/packages/medusa/src/services/__mocks__/swap.js index 2ad66283d7..6234ddd04e 100644 --- a/packages/medusa/src/services/__mocks__/swap.js +++ b/packages/medusa/src/services/__mocks__/swap.js @@ -11,7 +11,13 @@ export const SwapServiceMock = { return Promise.resolve() }), retrieve: jest.fn().mockImplementation(data => { - return Promise.resolve({ id: "test-swap" }) + return Promise.resolve({ id: IdMap.getId("test-swap") }) + }), + list: jest.fn().mockImplementation((...args) => { + return Promise.resolve([ + { id: IdMap.getId("test-swap") }, + { id: IdMap.getId("test-swap-1") }, + ]) }), } diff --git a/packages/medusa/src/services/swap.js b/packages/medusa/src/services/swap.js index c1c8711c42..f105fba730 100644 --- a/packages/medusa/src/services/swap.js +++ b/packages/medusa/src/services/swap.js @@ -92,6 +92,69 @@ class SwapService extends BaseService { return cloned } + transformQueryForTotals_(config) { + let { select, relations } = config + + if (!select) { + return { + ...config, + totalsToSelect: [], + } + } + + const totalFields = [ + "cart.subtotal", + "cart.tax_total", + "cart.shipping_total", + "cart.discount_total", + "cart.gift_card_total", + "cart.total", + ] + + const totalsToSelect = select.filter(v => totalFields.includes(v)) + if (totalsToSelect.length > 0) { + const relationSet = new Set(relations) + relationSet.add("cart") + relationSet.add("cart.items") + relationSet.add("cart.gift_cards") + relationSet.add("cart.discounts") + relationSet.add("cart.shipping_methods") + relationSet.add("cart.region") + relations = [...relationSet] + + select = select.filter(v => !totalFields.includes(v)) + } + + return { + ...config, + relations, + select, + totalsToSelect, + } + } + + async decorateTotals_(cart, totalsFields = []) { + if (totalsFields.includes("cart.shipping_total")) { + cart.shipping_total = await this.totalsService_.getShippingTotal(cart) + } + if (totalsFields.includes("cart.discount_total")) { + cart.discount_total = await this.totalsService_.getDiscountTotal(cart) + } + if (totalsFields.includes("cart.tax_total")) { + cart.tax_total = await this.totalsService_.getTaxTotal(cart) + } + if (totalsFields.includes("cart.gift_card_total")) { + cart.gift_card_total = await this.totalsService_.getGiftCardTotal(cart) + } + if (totalsFields.includes("cart.subtotal")) { + cart.subtotal = await this.totalsService_.getSubtotal(cart) + } + if (totalsFields.includes("cart.total")) { + cart.total = await this.totalsService_.getTotal(cart) + } + return cart + } + /** * Retrieves a swap with the given id. * @param {string} id - the id of the swap to retrieve @@ -102,7 +165,11 @@ class SwapService extends BaseService { const validatedId = this.validateId_(id) - const query = this.buildQuery_({ id: validatedId }, config) + const { totalsToSelect, ...newConfig } = this.transformQueryForTotals_( + config + ) + + const query = this.buildQuery_({ id: validatedId }, newConfig) const rels = query.relations delete query.relations @@ -112,6 +179,11 @@ class SwapService extends BaseService { throw new MedusaError(MedusaError.Types.NOT_FOUND, "Swap was not found") } + if (rels && rels.includes("cart")) { + const cart = await this.decorateTotals_(swap.cart, totalsToSelect) + swap.cart = cart + } + return swap }