feat(medusa): Decorate OrderEdit LineItems with totals (#3108)

This commit is contained in:
Frane Polić
2023-02-06 17:32:26 +01:00
committed by GitHub
parent e22a383f47
commit 4d6e63d68f
10 changed files with 291 additions and 132 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
feat(medusa): decorate order edit line items with totals

View File

@@ -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,
})
)
})
})
})

View File

@@ -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,
})
)

View File

@@ -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", () => {

View File

@@ -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

View File

@@ -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,
})
}

View File

@@ -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()
}),

View File

@@ -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)

View File

@@ -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<OrderEdit> {
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
}

View File

@@ -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