From 4d6e63d68f4e64c365ecbba133876d95e6528763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Mon, 6 Feb 2023 17:32:26 +0100 Subject: [PATCH] feat(medusa): Decorate OrderEdit LineItems with totals (#3108) --- .changeset/tame-items-eat.md | 5 + .../order-edit/ff-tax-inclusive-pricing.js | 193 ++++++++++++++++++ .../admin/{ => order-edit}/order-edit.js | 33 ++- .../__tests__/request-confirmation.ts | 3 - .../admin/order-edits/request-confirmation.ts | 2 +- .../update-order-edit-line-item.ts | 27 ++- .../src/services/__mocks__/order-edit.js | 12 -- .../src/services/__tests__/order-edit.ts | 7 +- packages/medusa/src/services/order-edit.ts | 139 +++++-------- packages/medusa/src/services/totals.ts | 2 +- 10 files changed, 291 insertions(+), 132 deletions(-) create mode 100644 .changeset/tame-items-eat.md create mode 100644 integration-tests/api/__tests__/admin/order-edit/ff-tax-inclusive-pricing.js rename integration-tests/api/__tests__/admin/{ => order-edit}/order-edit.js (99%) diff --git a/.changeset/tame-items-eat.md b/.changeset/tame-items-eat.md new file mode 100644 index 0000000000..fefdbe7c56 --- /dev/null +++ b/.changeset/tame-items-eat.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): decorate order edit line items with totals diff --git a/integration-tests/api/__tests__/admin/order-edit/ff-tax-inclusive-pricing.js b/integration-tests/api/__tests__/admin/order-edit/ff-tax-inclusive-pricing.js new file mode 100644 index 0000000000..2bbb39b924 --- /dev/null +++ b/integration-tests/api/__tests__/admin/order-edit/ff-tax-inclusive-pricing.js @@ -0,0 +1,193 @@ +const path = require("path") +const { IdMap } = require("medusa-test-utils") + +const startServerWithEnvironment = + require("../../../../helpers/start-server-with-environment").default +const { useApi } = require("../../../../helpers/use-api") +const { useDb } = require("../../../../helpers/use-db") + +const adminSeeder = require("../../../helpers/admin-seeder") + +const { + simpleProductFactory, + simpleRegionFactory, + simpleCartFactory, +} = require("../../../factories") + +jest.setTimeout(30000) + +const adminReqConfig = { + headers: { + Authorization: "Bearer test_token", + }, +} + +describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/order-edits", () => { + 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 }, + }) + dbConnection = connection + medusaProcess = process + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + + medusaProcess.kill() + }) + + describe("Items totals", () => { + let product1 + const prodId1 = IdMap.getId("prodId1") + const lineItemId1 = IdMap.getId("line-item-1") + + beforeEach(async () => { + await adminSeeder(dbConnection) + + product1 = await simpleProductFactory(dbConnection, { + id: prodId1, + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("decorates items with (tax-inclusive) totals", async () => { + const taxInclusiveRegion = await simpleRegionFactory(dbConnection, { + tax_rate: 25, + includes_tax: true, + }) + + const taxInclusiveCart = await simpleCartFactory(dbConnection, { + email: "adrien@test.com", + region: taxInclusiveRegion.id, + line_items: [ + { + id: lineItemId1, + variant_id: product1.variants[0].id, + quantity: 2, + unit_price: 10000, + includes_tax: true, + }, + ], + }) + + const api = useApi() + + await api.post(`/store/carts/${taxInclusiveCart.id}/payment-sessions`) + + const completeRes = await api.post( + `/store/carts/${taxInclusiveCart.id}/complete` + ) + + const order = completeRes.data.data + + const response = await api.post( + `/admin/order-edits/`, + { + order_id: order.id, + internal_note: "This is an internal note", + }, + adminReqConfig + ) + + expect(response.status).toEqual(200) + expect(response.data.order_edit).toEqual( + expect.objectContaining({ + items: expect.arrayContaining([ + expect.objectContaining({ + // 2x items | unit_price: 10000 (tax incl.) | 25% tax + original_item_id: lineItemId1, + subtotal: 2 * 8000, + discount_total: 0, + total: 2 * 10000, + unit_price: 10000, + original_total: 2 * 10000, + original_tax_total: 2 * 2000, + tax_total: 2 * 2000, + }), + ]), + discount_total: 0, + gift_card_total: 0, + gift_card_tax_total: 0, + shipping_total: 0, + subtotal: 16000, + tax_total: 4000, + total: 20000, + difference_due: 0, + }) + ) + }) + + it("decorates items with (tax-exclusive) totals", async () => { + const taxInclusiveRegion = await simpleRegionFactory(dbConnection, { + tax_rate: 25, + }) + + const cart = await simpleCartFactory(dbConnection, { + email: "adrien@test.com", + region: taxInclusiveRegion.id, + line_items: [ + { + id: lineItemId1, + variant_id: product1.variants[0].id, + quantity: 2, + unit_price: 10000, + }, + ], + }) + + const api = useApi() + + await api.post(`/store/carts/${cart.id}/payment-sessions`) + + const completeRes = await api.post(`/store/carts/${cart.id}/complete`) + + const order = completeRes.data.data + + const response = await api.post( + `/admin/order-edits/`, + { + order_id: order.id, + internal_note: "This is an internal note", + }, + adminReqConfig + ) + + expect(response.status).toEqual(200) + expect(response.data.order_edit).toEqual( + expect.objectContaining({ + items: expect.arrayContaining([ + expect.objectContaining({ + original_item_id: lineItemId1, + subtotal: 2 * 10000, + discount_total: 0, + unit_price: 10000, + total: 2 * 10000 + 2 * 2500, + original_total: 2 * 10000 + 2 * 2500, + original_tax_total: 2 * 2500, + tax_total: 2 * 2500, + }), + ]), + discount_total: 0, + gift_card_total: 0, + gift_card_tax_total: 0, + shipping_total: 0, + subtotal: 20000, + tax_total: 5000, + total: 25000, + difference_due: 0, + }) + ) + }) + }) +}) diff --git a/integration-tests/api/__tests__/admin/order-edit.js b/integration-tests/api/__tests__/admin/order-edit/order-edit.js similarity index 99% rename from integration-tests/api/__tests__/admin/order-edit.js rename to integration-tests/api/__tests__/admin/order-edit/order-edit.js index 2f96138cba..9f8a232aba 100644 --- a/integration-tests/api/__tests__/admin/order-edit.js +++ b/integration-tests/api/__tests__/admin/order-edit/order-edit.js @@ -1,17 +1,16 @@ const path = require("path") +const { OrderEditItemChangeType, OrderEdit } = require("@medusajs/medusa") +const { IdMap } = require("medusa-test-utils") -const startServerWithEnvironment = - require("../../../helpers/start-server-with-environment").default -const { useApi } = require("../../../helpers/use-api") -const { useDb, initDb } = require("../../../helpers/use-db") -const adminSeeder = require("../../helpers/admin-seeder") +const { useApi } = require("../../../../helpers/use-api") +const { useDb, initDb } = require("../../../../helpers/use-db") +const adminSeeder = require("../../../helpers/admin-seeder") const { simpleOrderEditFactory, -} = require("../../factories/simple-order-edit-factory") -const { IdMap } = require("medusa-test-utils") +} = require("../../../factories/simple-order-edit-factory") const { simpleOrderItemChangeFactory, -} = require("../../factories/simple-order-item-change-factory") +} = require("../../../factories/simple-order-item-change-factory") const { simpleLineItemFactory, simpleProductFactory, @@ -19,9 +18,8 @@ const { simpleDiscountFactory, simpleCartFactory, simpleRegionFactory, -} = require("../../factories") -const { OrderEditItemChangeType, OrderEdit } = require("@medusajs/medusa") -const setupServer = require("../../../helpers/setup-server") +} = require("../../../factories") +const setupServer = require("../../../../helpers/setup-server") jest.setTimeout(30000) @@ -37,11 +35,9 @@ describe("/admin/order-edits", () => { const adminUserId = "admin_user" beforeAll(async () => { - const cwd = path.resolve(path.join(__dirname, "..", "..")) + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ - cwd, - }) + medusaProcess = await setupServer({ cwd }) }) afterAll(async () => { @@ -1167,6 +1163,7 @@ describe("/admin/order-edits", () => { id: orderId1, fulfillment_status: "fulfilled", payment_status: "captured", + tax_rate: null, region: { id: "test-region", name: "Test region", @@ -2572,13 +2569,15 @@ describe("/admin/order-edits", () => { ]), }), ]), - discount_total: 2000, + // rounding issue since we are allocating 1/3 of the discount to one item and 2/3 to the other item where both cost 10 + // resulting in adjustment amounts like: 1333... + discount_total: 2001, + total: 1099, gift_card_total: 0, gift_card_tax_total: 0, shipping_total: 0, subtotal: 3000, tax_total: 100, - total: 1100, }) ) diff --git a/packages/medusa/src/api/routes/admin/order-edits/__tests__/request-confirmation.ts b/packages/medusa/src/api/routes/admin/order-edits/__tests__/request-confirmation.ts index f3a1f84943..a1f3ffca48 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/__tests__/request-confirmation.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/__tests__/request-confirmation.ts @@ -2,7 +2,6 @@ import { IdMap } from "medusa-test-utils" import { request } from "../../../../../helpers/test-request" import { orderEditServiceMock } from "../../../../../services/__mocks__/order-edit" - describe("GET /admin/order-edits/:id", () => { describe("successfully requests an order edit confirmation", () => { const orderEditId = IdMap.getId("testRequestOrder") @@ -35,8 +34,6 @@ describe("GET /admin/order-edits/:id", () => { orderEditId, { requestedBy: IdMap.getId("admin_user") } ) - - expect(orderEditServiceMock.update).toHaveBeenCalledTimes(1) }) it("returns updated orderEdit", () => { diff --git a/packages/medusa/src/api/routes/admin/order-edits/request-confirmation.ts b/packages/medusa/src/api/routes/admin/order-edits/request-confirmation.ts index 497babbef5..b33901ec49 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/request-confirmation.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/request-confirmation.ts @@ -84,7 +84,7 @@ export default async (req, res) => { requestedBy: loggedInUser, }) - const total = await orderEditServiceTx.getTotals(orderEdit.id) + const total = await orderEditServiceTx.decorateTotals(orderEdit) if (total.difference_due > 0) { const order = await orderService diff --git a/packages/medusa/src/api/routes/admin/order-edits/update-order-edit-line-item.ts b/packages/medusa/src/api/routes/admin/order-edits/update-order-edit-line-item.ts index 0aa07fa9dc..9ba144f3d0 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/update-order-edit-line-item.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/update-order-edit-line-item.ts @@ -79,20 +79,25 @@ export default async (req: Request, res: Response) => { const manager: EntityManager = req.scope.resolve("manager") - await manager.transaction(async (transactionManager) => { - await orderEditService - .withTransaction(transactionManager) - .updateLineItem(id, item_id, validatedBody) - }) + const decoratedEdit = await manager.transaction( + async (transactionManager) => { + const orderEditTx = orderEditService.withTransaction(transactionManager) - let orderEdit = await orderEditService.retrieve(id, { - select: defaultOrderEditFields, - relations: defaultOrderEditRelations, - }) - orderEdit = await orderEditService.decorateTotals(orderEdit) + await orderEditTx.updateLineItem(id, item_id, validatedBody) + + const orderEdit = await orderEditTx.retrieve(id, { + select: defaultOrderEditFields, + relations: defaultOrderEditRelations, + }) + + await orderEditTx.decorateTotals(orderEdit) + + return orderEdit + } + ) res.status(200).send({ - order_edit: orderEdit, + order_edit: decoratedEdit, }) } diff --git a/packages/medusa/src/services/__mocks__/order-edit.js b/packages/medusa/src/services/__mocks__/order-edit.js index 0aa055bd41..fedf0787ce 100644 --- a/packages/medusa/src/services/__mocks__/order-edit.js +++ b/packages/medusa/src/services/__mocks__/order-edit.js @@ -117,18 +117,6 @@ export const orderEditServiceMock = { declined_at: new Date(), }) }), - getTotals: jest.fn().mockImplementation((id) => { - return Promise.resolve({ - shipping_total: 10, - gift_card_total: 0, - gift_card_tax_total: 0, - discount_total: 0, - tax_total: 1, - subtotal: 2000, - difference_due: 1000, - total: 1000, - }) - }), delete: jest.fn().mockImplementation((_) => { return Promise.resolve() }), diff --git a/packages/medusa/src/services/__tests__/order-edit.ts b/packages/medusa/src/services/__tests__/order-edit.ts index ea0d0ca6c3..8e3fcf0c64 100644 --- a/packages/medusa/src/services/__tests__/order-edit.ts +++ b/packages/medusa/src/services/__tests__/order-edit.ts @@ -3,11 +3,12 @@ import { OrderEditItemChangeType, OrderEditStatus } from "../../models" import { EventBusService, LineItemService, + NewTotalsService, OrderEditItemChangeService, OrderEditService, OrderService, TaxProviderService, - TotalsService + TotalsService, } from "../index" import LineItemAdjustmentService from "../line-item-adjustment" import { EventBusServiceMock } from "../__mocks__/event-bus" @@ -17,6 +18,7 @@ import { OrderServiceMock } from "../__mocks__/order" import { orderEditItemChangeServiceMock } from "../__mocks__/order-edit-item-change" import { taxProviderServiceMock } from "../__mocks__/tax-provider" import { TotalsServiceMock } from "../__mocks__/totals" +import NewTotalsServiceMock from "../__mocks__/new-totals" const orderEditToUpdate = { id: IdMap.getId("order-edit-to-update"), @@ -188,6 +190,7 @@ describe("OrderEditService", () => { orderService: OrderServiceMock as unknown as OrderService, eventBusService: EventBusServiceMock as unknown as EventBusService, totalsService: TotalsServiceMock as unknown as TotalsService, + newTotalsService: NewTotalsServiceMock as unknown as NewTotalsService, lineItemService: lineItemServiceMock as unknown as LineItemService, orderEditItemChangeService: orderEditItemChangeServiceMock as unknown as OrderEditItemChangeService, @@ -330,7 +333,7 @@ describe("OrderEditService", () => { let result beforeEach(async () => { - jest.spyOn(orderEditService, "getTotals").mockResolvedValue({ + jest.spyOn(orderEditService, "decorateTotals").mockResolvedValue({ difference_due: 1500, } as any) diff --git a/packages/medusa/src/services/order-edit.ts b/packages/medusa/src/services/order-edit.ts index 34da0fc6f6..25e60e24d7 100644 --- a/packages/medusa/src/services/order-edit.ts +++ b/packages/medusa/src/services/order-edit.ts @@ -7,23 +7,24 @@ import { Order, OrderEdit, OrderEditItemChangeType, - OrderEditStatus + OrderEditStatus, } from "../models" import { OrderEditRepository } from "../repositories/order-edit" import { FindConfig, Selector } from "../types/common" import { AddOrderEditLineItemInput, - CreateOrderEditInput + CreateOrderEditInput, } from "../types/order-edit" import { buildQuery, isString } from "../utils" import { EventBusService, LineItemAdjustmentService, LineItemService, + NewTotalsService, OrderEditItemChangeService, OrderService, TaxProviderService, - TotalsService + TotalsService, } from "./index" type InjectedDependencies = { @@ -32,6 +33,7 @@ type InjectedDependencies = { orderService: OrderService totalsService: TotalsService + newTotalsService: NewTotalsService lineItemService: LineItemService eventBusService: EventBusService taxProviderService: TaxProviderService @@ -56,6 +58,7 @@ export default class OrderEditService extends TransactionBaseService { protected readonly orderService_: OrderService protected readonly totalsService_: TotalsService + protected readonly newTotalsService_: NewTotalsService protected readonly lineItemService_: LineItemService protected readonly eventBusService_: EventBusService protected readonly taxProviderService_: TaxProviderService @@ -69,6 +72,7 @@ export default class OrderEditService extends TransactionBaseService { lineItemService, eventBusService, totalsService, + newTotalsService, orderEditItemChangeService, lineItemAdjustmentService, taxProviderService, @@ -82,6 +86,7 @@ export default class OrderEditService extends TransactionBaseService { this.lineItemService_ = lineItemService this.eventBusService_ = eventBusService this.totalsService_ = totalsService + this.newTotalsService_ = newTotalsService this.orderEditItemChangeService_ = orderEditItemChangeService this.lineItemAdjustmentService_ = lineItemAdjustmentService this.taxProviderService_ = taxProviderService @@ -148,69 +153,6 @@ export default class OrderEditService extends TransactionBaseService { return orderEdits } - /** - * Compute and return the different totals from the order edit id - * @param orderEditId - */ - async getTotals(orderEditId: string): Promise<{ - shipping_total: number - gift_card_total: number - gift_card_tax_total: number - discount_total: number - tax_total: number | null - subtotal: number - difference_due: number - total: number - }> { - const manager = this.transactionManager_ ?? this.manager_ - const { order_id, items } = await this.retrieve(orderEditId, { - select: ["id", "order_id", "items"], - relations: ["items", "items.tax_lines", "items.adjustments"], - }) - - const order = await this.orderService_ - .withTransaction(manager) - .retrieve(order_id, { - relations: [ - "discounts", - "discounts.rule", - "gift_cards", - "region", - "items", - "items.tax_lines", - "items.adjustments", - "region.tax_rates", - "shipping_methods", - "shipping_methods.tax_lines", - ], - }) - const computedOrder = { ...order, items } as Order - - const totalsServiceTx = this.totalsService_.withTransaction(manager) - - const shipping_total = await totalsServiceTx.getShippingTotal(computedOrder) - const { total: gift_card_total, tax_total: gift_card_tax_total } = - await totalsServiceTx.getGiftCardTotal(computedOrder) - const discount_total = await totalsServiceTx.getDiscountTotal(computedOrder) - const tax_total = await totalsServiceTx.getTaxTotal(computedOrder) - const subtotal = await totalsServiceTx.getSubtotal(computedOrder) - const total = await totalsServiceTx.getTotal(computedOrder) - - const orderTotal = await totalsServiceTx.getTotal(order) - const difference_due = total - orderTotal - - return { - shipping_total, - gift_card_total, - gift_card_tax_total, - discount_total, - tax_total, - subtotal, - total, - difference_due, - } - } - async create( data: CreateOrderEditInput, context: { createdBy: string } @@ -390,11 +332,11 @@ export default class OrderEditService extends TransactionBaseService { ) } - const lineItem = await this.lineItemService_ - .withTransaction(manager) - .retrieve(itemId, { - select: ["id", "order_edit_id", "original_item_id"], - }) + const lineItemServiceTx = this.lineItemService_.withTransaction(manager) + + const lineItem = await lineItemServiceTx.retrieve(itemId, { + select: ["id", "order_edit_id", "original_item_id"], + }) if (lineItem.order_edit_id !== orderEditId) { throw new MedusaError( @@ -427,11 +369,9 @@ export default class OrderEditService extends TransactionBaseService { }) } - await this.lineItemService_ - .withTransaction(manager) - .update(change.line_item_id!, { - quantity: data.quantity, - }) + await lineItemServiceTx.update(change.line_item_id!, { + quantity: data.quantity, + }) await this.refreshAdjustments(orderEditId) }) @@ -538,15 +478,44 @@ export default class OrderEditService extends TransactionBaseService { } async decorateTotals(orderEdit: OrderEdit): Promise { - const totals = await this.getTotals(orderEdit.id) - orderEdit.discount_total = totals.discount_total - orderEdit.gift_card_total = totals.gift_card_total - orderEdit.gift_card_tax_total = totals.gift_card_tax_total - orderEdit.shipping_total = totals.shipping_total - orderEdit.subtotal = totals.subtotal - orderEdit.tax_total = totals.tax_total - orderEdit.total = totals.total - orderEdit.difference_due = totals.difference_due + const manager = this.transactionManager_ ?? this.manager_ + const { order_id, items } = await this.retrieve(orderEdit.id, { + select: ["id", "order_id", "items"], + relations: ["items", "items.tax_lines", "items.adjustments"], + }) + + const orderServiceTx = this.orderService_.withTransaction(manager) + + const order = await orderServiceTx.retrieve(order_id, { + relations: [ + "discounts", + "discounts.rule", + "gift_cards", + "region", + "items", + "items.tax_lines", + "items.adjustments", + "region.tax_rates", + "shipping_methods", + "shipping_methods.tax_lines", + ], + }) + + const computedOrder = { ...order, items } as Order + await Promise.all([ + await orderServiceTx.decorateTotals(computedOrder), + await orderServiceTx.decorateTotals(order), + ]) + + orderEdit.items = computedOrder.items + orderEdit.discount_total = computedOrder.discount_total + orderEdit.gift_card_total = computedOrder.gift_card_total + orderEdit.gift_card_tax_total = computedOrder.gift_card_tax_total + orderEdit.shipping_total = computedOrder.shipping_total + orderEdit.subtotal = computedOrder.subtotal + orderEdit.tax_total = computedOrder.tax_total + orderEdit.total = computedOrder.total + orderEdit.difference_due = computedOrder.total - order.total return orderEdit } diff --git a/packages/medusa/src/services/totals.ts b/packages/medusa/src/services/totals.ts index 893c3dd508..62e89d6803 100644 --- a/packages/medusa/src/services/totals.ts +++ b/packages/medusa/src/services/totals.ts @@ -129,7 +129,7 @@ class TotalsService extends TransactionBaseService { } /** - * Calculates subtotal of a given cart or order. + * Calculates total of a given cart or order. * @param cartOrOrder - object to calculate total for * @param options - options to calculate by * @return the calculated subtotal