feat(medusa): refactor the way the order edit handle the items (#2255)
* feat(medusa): Reftor the way the order edit works
This commit is contained in:
committed by
GitHub
parent
7e56935e7a
commit
d138baf460
@@ -94,6 +94,13 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
|
||||
fulfilled_quantity: 1,
|
||||
shipped_quantity: 1,
|
||||
unit_price: 1000,
|
||||
tax_lines: [
|
||||
{
|
||||
rate: 10,
|
||||
code: "code1",
|
||||
name: "code1",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: lineItemId2,
|
||||
@@ -102,6 +109,13 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
|
||||
fulfilled_quantity: 1,
|
||||
shipped_quantity: 1,
|
||||
unit_price: 1000,
|
||||
tax_lines: [
|
||||
{
|
||||
rate: 10,
|
||||
code: "code2",
|
||||
name: "code2",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -170,20 +184,42 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
|
||||
canceled_by: null,
|
||||
confirmed_by: null,
|
||||
internal_note: "test internal note",
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({ id: lineItemCreateId, quantity: 2 }),
|
||||
expect.objectContaining({ id: lineItemId1, quantity: 2 }),
|
||||
]),
|
||||
removed_items: expect.arrayContaining([
|
||||
expect.objectContaining({ id: lineItemId2, quantity: 1 }),
|
||||
items: expect.arrayContaining([]),
|
||||
changes: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: "item_add",
|
||||
order_edit_id: orderEditId,
|
||||
original_line_item_id: null,
|
||||
line_item_id: lineItemCreateId,
|
||||
line_item: expect.any(Object),
|
||||
original_line_item: null,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: "item_update",
|
||||
order_edit_id: orderEditId,
|
||||
original_line_item_id: lineItemId1,
|
||||
line_item_id: lineItemUpdateId,
|
||||
line_item: expect.any(Object),
|
||||
original_line_item: expect.any(Object),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: "item_remove",
|
||||
order_edit_id: orderEditId,
|
||||
original_line_item_id: lineItemId2,
|
||||
line_item_id: null,
|
||||
line_item: null,
|
||||
original_line_item: expect.any(Object),
|
||||
}),
|
||||
]),
|
||||
// Items are cloned during the creation which explain why it is 0 for a fake order edit since it does
|
||||
// not use the logic of the service. Must be check in another test
|
||||
shipping_total: 0,
|
||||
gift_card_total: 0,
|
||||
gift_card_tax_total: 0,
|
||||
discount_total: 0,
|
||||
tax_total: 0,
|
||||
total: 2200,
|
||||
subtotal: 2200,
|
||||
total: 0,
|
||||
subtotal: 0,
|
||||
})
|
||||
)
|
||||
expect(response.status).toEqual(200)
|
||||
@@ -344,6 +380,14 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
|
||||
fulfilled_quantity: 1,
|
||||
shipped_quantity: 1,
|
||||
unit_price: 1000,
|
||||
tax_lines: [
|
||||
{
|
||||
item_id: lineItemId1,
|
||||
rate: 10,
|
||||
code: "default",
|
||||
name: "default",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: lineItemId2,
|
||||
@@ -352,6 +396,14 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
|
||||
fulfilled_quantity: 1,
|
||||
shipped_quantity: 1,
|
||||
unit_price: 1000,
|
||||
tax_lines: [
|
||||
{
|
||||
item_id: lineItemId2,
|
||||
rate: 10,
|
||||
code: "default",
|
||||
name: "default",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -363,7 +415,7 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
|
||||
return await db.teardown()
|
||||
})
|
||||
|
||||
it("creates and order edit", async () => {
|
||||
it("creates an order edit", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const response = await api.post(
|
||||
@@ -384,29 +436,46 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
|
||||
canceled_by: null,
|
||||
confirmed_by: null,
|
||||
internal_note: "This is an internal note",
|
||||
// The items are cloned from the items of the order
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: lineItemId1,
|
||||
id: expect.not.stringContaining(lineItemId1),
|
||||
order_id: null,
|
||||
order_edit_id: expect.any(String),
|
||||
original_item_id: lineItemId1,
|
||||
quantity: 1,
|
||||
fulfilled_quantity: 1,
|
||||
shipped_quantity: 1,
|
||||
unit_price: 1000,
|
||||
tax_lines: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
rate: 10,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: lineItemId2,
|
||||
id: expect.not.stringContaining(lineItemId2),
|
||||
order_id: null,
|
||||
order_edit_id: expect.any(String),
|
||||
original_item_id: lineItemId2,
|
||||
quantity: 1,
|
||||
fulfilled_quantity: 1,
|
||||
shipped_quantity: 1,
|
||||
unit_price: 1000,
|
||||
tax_lines: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
rate: 10,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
shipping_total: 0,
|
||||
gift_card_total: 0,
|
||||
gift_card_tax_total: 0,
|
||||
discount_total: 0,
|
||||
tax_total: 0,
|
||||
total: 2000,
|
||||
tax_total: 200,
|
||||
subtotal: 2000,
|
||||
total: 2200,
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -461,29 +530,46 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
|
||||
canceled_by: null,
|
||||
confirmed_by: null,
|
||||
internal_note: "This is an internal note",
|
||||
// The items are cloned from the items of the order
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: lineItemId1,
|
||||
id: expect.not.stringContaining(lineItemId1),
|
||||
order_id: null,
|
||||
order_edit_id: expect.any(String),
|
||||
original_item_id: lineItemId1,
|
||||
quantity: 1,
|
||||
fulfilled_quantity: 1,
|
||||
shipped_quantity: 1,
|
||||
unit_price: 1000,
|
||||
tax_lines: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
rate: 10,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: lineItemId2,
|
||||
id: expect.not.stringContaining(lineItemId2),
|
||||
order_id: null,
|
||||
order_edit_id: expect.any(String),
|
||||
original_item_id: lineItemId2,
|
||||
quantity: 1,
|
||||
fulfilled_quantity: 1,
|
||||
shipped_quantity: 1,
|
||||
unit_price: 1000,
|
||||
tax_lines: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
rate: 10,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
shipping_total: 0,
|
||||
gift_card_total: 0,
|
||||
gift_card_tax_total: 0,
|
||||
discount_total: 0,
|
||||
tax_total: 0,
|
||||
total: 2000,
|
||||
tax_total: 200,
|
||||
subtotal: 2000,
|
||||
total: 2200,
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -581,7 +667,7 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
|
||||
})
|
||||
|
||||
describe("POST /admin/order-edits/:id", () => {
|
||||
const orderEditId = IdMap.getId("order-edit-1")
|
||||
let orderEditId
|
||||
const prodId1 = IdMap.getId("prodId1")
|
||||
const lineItemId1 = IdMap.getId("line-item-1")
|
||||
const orderId1 = IdMap.getId("order-id-1")
|
||||
@@ -612,16 +698,27 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
|
||||
fulfilled_quantity: 1,
|
||||
shipped_quantity: 1,
|
||||
unit_price: 1000,
|
||||
tax_lines: [
|
||||
{
|
||||
rate: 10,
|
||||
code: "code1",
|
||||
name: "code1",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await simpleOrderEditFactory(dbConnection, {
|
||||
id: orderEditId,
|
||||
order_id: order.id,
|
||||
created_by: "admin_user",
|
||||
internal_note: "test internal note",
|
||||
})
|
||||
const api = useApi()
|
||||
const response = await api.post(
|
||||
`/admin/order-edits/`,
|
||||
{
|
||||
order_id: orderId1,
|
||||
internal_note: "This is an internal note",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
orderEditId = response.data.order_edit.id
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -647,25 +744,29 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
|
||||
canceled_by: null,
|
||||
confirmed_by: null,
|
||||
internal_note: "changed note",
|
||||
/*
|
||||
* Computed items are appended to the response
|
||||
*/
|
||||
items: [
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: lineItemId1,
|
||||
order_id: orderId1,
|
||||
order_id: null,
|
||||
order_edit_id: orderEditId,
|
||||
original_item_id: lineItemId1,
|
||||
quantity: 1,
|
||||
fulfilled_quantity: 1,
|
||||
shipped_quantity: 1,
|
||||
unit_price: 1000,
|
||||
tax_lines: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
rate: 10,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
],
|
||||
/*
|
||||
* Computed totals are appended to the response
|
||||
*/
|
||||
]),
|
||||
discount_total: 0,
|
||||
gift_card_total: 0,
|
||||
gift_card_tax_total: 0,
|
||||
shipping_total: 0,
|
||||
subtotal: 1000,
|
||||
tax_total: 0,
|
||||
total: 1000,
|
||||
tax_total: 100,
|
||||
total: 1100,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
@@ -21,12 +21,6 @@ const { OrderEditItemChangeType } = require("@medusajs/medusa")
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
const adminHeaders = {
|
||||
headers: {
|
||||
Authorization: "Bearer test_token",
|
||||
},
|
||||
}
|
||||
|
||||
describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => {
|
||||
let medusaProcess
|
||||
let dbConnection
|
||||
@@ -93,6 +87,13 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => {
|
||||
fulfilled_quantity: 1,
|
||||
shipped_quantity: 1,
|
||||
unit_price: 1000,
|
||||
tax_lines: [
|
||||
{
|
||||
rate: 10,
|
||||
code: "code1",
|
||||
name: "code1",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: lineItemId2,
|
||||
@@ -101,6 +102,13 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => {
|
||||
fulfilled_quantity: 1,
|
||||
shipped_quantity: 1,
|
||||
unit_price: 1000,
|
||||
tax_lines: [
|
||||
{
|
||||
rate: 10,
|
||||
code: "code2",
|
||||
name: "code2",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -162,24 +170,44 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => {
|
||||
expect(response.data.order_edit).toEqual(
|
||||
expect.objectContaining({
|
||||
id: orderEditId,
|
||||
requested_by: null,
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({ id: lineItemCreateId, quantity: 2 }),
|
||||
expect.objectContaining({ id: lineItemId1, quantity: 2 }),
|
||||
]),
|
||||
removed_items: expect.arrayContaining([
|
||||
expect.objectContaining({ id: lineItemId2, quantity: 1 }),
|
||||
items: expect.arrayContaining([]),
|
||||
changes: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: "item_add",
|
||||
order_edit_id: orderEditId,
|
||||
original_line_item_id: null,
|
||||
line_item_id: lineItemCreateId,
|
||||
line_item: expect.any(Object),
|
||||
original_line_item: null,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: "item_update",
|
||||
order_edit_id: orderEditId,
|
||||
original_line_item_id: lineItemId1,
|
||||
line_item_id: lineItemUpdateId,
|
||||
line_item: expect.any(Object),
|
||||
original_line_item: expect.any(Object),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: "item_remove",
|
||||
order_edit_id: orderEditId,
|
||||
original_line_item_id: lineItemId2,
|
||||
line_item_id: null,
|
||||
line_item: null,
|
||||
original_line_item: expect.any(Object),
|
||||
}),
|
||||
]),
|
||||
// Items are cloned during the creation which explain why it is 0 for a fake order edit since it does
|
||||
// not use the logic of the service. Must be check in another test
|
||||
shipping_total: 0,
|
||||
gift_card_total: 0,
|
||||
gift_card_tax_total: 0,
|
||||
discount_total: 0,
|
||||
tax_total: 0,
|
||||
total: 2200,
|
||||
subtotal: 2200,
|
||||
total: 0,
|
||||
subtotal: 0,
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.data.order_edit.internal_note).not.toBeDefined()
|
||||
expect(response.data.order_edit.created_by).not.toBeDefined()
|
||||
expect(response.data.order_edit.canceled_by).not.toBeDefined()
|
||||
|
||||
@@ -33,6 +33,7 @@ describe("POST /admin/order-edits", () => {
|
||||
})
|
||||
|
||||
it("calls order edit service create", () => {
|
||||
expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1)
|
||||
expect(orderEditServiceMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(orderEditServiceMock.create).toHaveBeenCalledWith(
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@ describe("GET /admin/order-edits/:id", () => {
|
||||
select: defaultOrderEditFields,
|
||||
relations: defaultOrderEditRelations,
|
||||
})
|
||||
expect(orderEditServiceMock.decorateLineItemsAndTotals).toHaveBeenCalledTimes(1)
|
||||
expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("returns order", () => {
|
||||
|
||||
@@ -9,14 +9,18 @@ describe("GET /admin/order-edits/:id", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request("POST", `/admin/order-edits/${orderEditId}/request`, {
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/admin/order-edits/${orderEditId}/request`,
|
||||
{
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
},
|
||||
flags: [OrderEditingFeatureFlag],
|
||||
})
|
||||
flags: [OrderEditingFeatureFlag],
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
@@ -25,15 +29,20 @@ describe("GET /admin/order-edits/:id", () => {
|
||||
|
||||
it("calls orderEditService requestConfirmation", () => {
|
||||
expect(orderEditServiceMock.requestConfirmation).toHaveBeenCalledTimes(1)
|
||||
expect(orderEditServiceMock.requestConfirmation).toHaveBeenCalledWith(orderEditId, {loggedInUser: IdMap.getId("admin_user")})
|
||||
expect(orderEditServiceMock.requestConfirmation).toHaveBeenCalledWith(
|
||||
orderEditId,
|
||||
{ loggedInUser: IdMap.getId("admin_user") }
|
||||
)
|
||||
})
|
||||
|
||||
it("returns updated orderEdit", () => {
|
||||
expect(subject.body.order_edit).toEqual(expect.objectContaining({
|
||||
id: orderEditId,
|
||||
requested_at: expect.any(String),
|
||||
requested_by: IdMap.getId("admin_user")
|
||||
}))
|
||||
expect(subject.body.order_edit).toEqual(
|
||||
expect.objectContaining({
|
||||
id: orderEditId,
|
||||
requested_at: expect.any(String),
|
||||
requested_by: IdMap.getId("admin_user"),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,6 +2,10 @@ import { Request, Response } from "express"
|
||||
import { OrderEditService } from "../../../../services"
|
||||
import { IsOptional, IsString } from "class-validator"
|
||||
import { EntityManager } from "typeorm"
|
||||
import {
|
||||
defaultOrderEditFields,
|
||||
defaultOrderEditRelations,
|
||||
} from "../../../../types/order-edit"
|
||||
|
||||
/**
|
||||
* @oas [post] /order-edits
|
||||
@@ -62,13 +66,19 @@ export default async (req: Request, res: Response) => {
|
||||
const data = req.validatedBody as AdminPostOrderEditsReq
|
||||
const loggedInUserId = (req.user?.id ?? req.user?.userId) as string
|
||||
|
||||
const orderEdit = await manager.transaction(async (transactionManager) => {
|
||||
const orderEditServiceTx =
|
||||
orderEditService.withTransaction(transactionManager)
|
||||
const orderEdit = await orderEditServiceTx.create(data, { loggedInUserId })
|
||||
const createdOrderEdit = await manager.transaction(
|
||||
async (transactionManager) => {
|
||||
return await orderEditService
|
||||
.withTransaction(transactionManager)
|
||||
.create(data, { loggedInUserId })
|
||||
}
|
||||
)
|
||||
|
||||
return await orderEditServiceTx.decorateLineItemsAndTotals(orderEdit)
|
||||
let orderEdit = await orderEditService.retrieve(createdOrderEdit.id, {
|
||||
select: defaultOrderEditFields,
|
||||
relations: defaultOrderEditRelations,
|
||||
})
|
||||
orderEdit = await orderEditService.decorateTotals(orderEdit)
|
||||
|
||||
return res.json({ order_edit: orderEdit })
|
||||
}
|
||||
|
||||
@@ -60,8 +60,7 @@ export default async (req: Request, res: Response) => {
|
||||
const retrieveConfig = req.retrieveConfig
|
||||
|
||||
let orderEdit = await orderEditService.retrieve(id, retrieveConfig)
|
||||
|
||||
orderEdit = await orderEditService.decorateLineItemsAndTotals(orderEdit)
|
||||
orderEdit = await orderEditService.decorateTotals(orderEdit)
|
||||
|
||||
return res.json({ order_edit: orderEdit })
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ import { Request, Response } from "express"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
import { OrderEditService } from "../../../../services"
|
||||
import {
|
||||
defaultOrderEditFields,
|
||||
defaultOrderEditRelations,
|
||||
} from "../../../../types/order-edit"
|
||||
|
||||
/**
|
||||
* @oas [post] /order-edits/{id}
|
||||
@@ -71,26 +75,19 @@ export default async (req: Request, res: Response) => {
|
||||
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
|
||||
const orderEdit = await manager.transaction(async (transactionManager) => {
|
||||
const orderEditServiceTx =
|
||||
orderEditService.withTransaction(transactionManager)
|
||||
const updatedOrderEdit = await manager.transaction(
|
||||
async (transactionManager) => {
|
||||
return await orderEditService
|
||||
.withTransaction(transactionManager)
|
||||
.update(id, validatedBody)
|
||||
}
|
||||
)
|
||||
|
||||
const _orderEdit = await orderEditServiceTx.update(id, validatedBody)
|
||||
|
||||
const { items } = await orderEditServiceTx.computeLineItems(_orderEdit.id)
|
||||
_orderEdit.items = items
|
||||
|
||||
const totals = await orderEditServiceTx.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
|
||||
|
||||
return _orderEdit
|
||||
let orderEdit = await orderEditService.retrieve(updatedOrderEdit.id, {
|
||||
select: defaultOrderEditFields,
|
||||
relations: defaultOrderEditRelations,
|
||||
})
|
||||
orderEdit = await orderEditService.decorateTotals(orderEdit)
|
||||
|
||||
res.status(200).json({ order_edit: orderEdit })
|
||||
}
|
||||
|
||||
@@ -2,11 +2,6 @@ import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { orderEditServiceMock } from "../../../../../services/__mocks__/order-edit"
|
||||
import OrderEditingFeatureFlag from "../../../../../loaders/feature-flags/order-editing"
|
||||
import {
|
||||
defaultOrderEditFields,
|
||||
defaultOrderEditRelations,
|
||||
} from "../../../../../types/order-edit"
|
||||
import { storeOrderEditNotAllowedFields } from "../index"
|
||||
|
||||
describe("GET /store/order-edits/:id", () => {
|
||||
describe("successfully gets an order edit", () => {
|
||||
@@ -18,10 +13,14 @@ describe("GET /store/order-edits/:id", () => {
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request("POST", `/store/order-edits/${orderEditId}/decline`, {
|
||||
payload,
|
||||
flags: [OrderEditingFeatureFlag],
|
||||
})
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/store/order-edits/${orderEditId}/decline`,
|
||||
{
|
||||
payload,
|
||||
flags: [OrderEditingFeatureFlag],
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
@@ -30,8 +29,11 @@ describe("GET /store/order-edits/:id", () => {
|
||||
|
||||
it("calls orderService decline", () => {
|
||||
expect(orderEditServiceMock.decline).toHaveBeenCalledTimes(1)
|
||||
expect(orderEditServiceMock.decline).toHaveBeenCalledWith(orderEditId, { declinedReason: "test", loggedInUser: undefined})
|
||||
expect(orderEditServiceMock.decorateLineItemsAndTotals).toHaveBeenCalledTimes(1)
|
||||
expect(orderEditServiceMock.decline).toHaveBeenCalledWith(orderEditId, {
|
||||
declinedReason: "test",
|
||||
loggedInUser: undefined,
|
||||
})
|
||||
expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("returns orderEdit", () => {
|
||||
|
||||
@@ -3,10 +3,9 @@ import { request } from "../../../../../helpers/test-request"
|
||||
import { orderEditServiceMock } from "../../../../../services/__mocks__/order-edit"
|
||||
import OrderEditingFeatureFlag from "../../../../../loaders/feature-flags/order-editing"
|
||||
import {
|
||||
defaultOrderEditFields,
|
||||
defaultOrderEditRelations,
|
||||
defaultStoreOrderEditFields,
|
||||
defaultStoreOrderEditRelations,
|
||||
} from "../../../../../types/order-edit"
|
||||
import { storeOrderEditNotAllowedFields } from "../index"
|
||||
|
||||
describe("GET /store/order-edits/:id", () => {
|
||||
describe("successfully gets an order edit", () => {
|
||||
@@ -26,14 +25,10 @@ describe("GET /store/order-edits/:id", () => {
|
||||
it("calls orderService retrieve", () => {
|
||||
expect(orderEditServiceMock.retrieve).toHaveBeenCalledTimes(1)
|
||||
expect(orderEditServiceMock.retrieve).toHaveBeenCalledWith(orderEditId, {
|
||||
select: defaultOrderEditFields.filter(
|
||||
(field) => !storeOrderEditNotAllowedFields.includes(field)
|
||||
),
|
||||
relations: defaultOrderEditRelations.filter(
|
||||
(field) => !storeOrderEditNotAllowedFields.includes(field)
|
||||
),
|
||||
select: defaultStoreOrderEditFields,
|
||||
relations: defaultStoreOrderEditRelations,
|
||||
})
|
||||
expect(orderEditServiceMock.decorateLineItemsAndTotals).toHaveBeenCalledTimes(1)
|
||||
expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("returns order", () => {
|
||||
|
||||
@@ -2,6 +2,10 @@ import { IsOptional, IsString } from "class-validator"
|
||||
import { Request, Response } from "express"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { OrderEditService } from "../../../../services"
|
||||
import {
|
||||
defaultStoreOrderEditFields,
|
||||
defaultStoreOrderEditRelations,
|
||||
} from "../../../../types/order-edit"
|
||||
|
||||
/**
|
||||
* @oas [post] /order-edits/{id}/decline
|
||||
@@ -71,9 +75,12 @@ export default async (req: Request, res: Response) => {
|
||||
loggedInUser: userId,
|
||||
})
|
||||
})
|
||||
let orderEdit = await orderEditService.retrieve(id)
|
||||
|
||||
orderEdit = await orderEditService.decorateLineItemsAndTotals(orderEdit)
|
||||
let orderEdit = await orderEditService.retrieve(id, {
|
||||
select: defaultStoreOrderEditFields,
|
||||
relations: defaultStoreOrderEditRelations,
|
||||
})
|
||||
orderEdit = await orderEditService.decorateTotals(orderEdit)
|
||||
|
||||
res.status(200).json({ order_edit: orderEdit })
|
||||
}
|
||||
|
||||
@@ -54,8 +54,7 @@ export default async (req: Request, res: Response) => {
|
||||
const retrieveConfig = req.retrieveConfig
|
||||
|
||||
let orderEdit = await orderEditService.retrieve(id, retrieveConfig)
|
||||
|
||||
orderEdit = await orderEditService.decorateLineItemsAndTotals(orderEdit)
|
||||
orderEdit = await orderEditService.decorateTotals(orderEdit)
|
||||
|
||||
return res.json({ order_edit: orderEdit })
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import { EmptyQueryParams } from "../../../../types/common"
|
||||
import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled"
|
||||
import OrderEditingFeatureFlag from "../../../../loaders/feature-flags/order-editing"
|
||||
import {
|
||||
defaultOrderEditFields,
|
||||
defaultOrderEditRelations,
|
||||
defaultStoreOrderEditFields,
|
||||
defaultStoreOrderEditRelations,
|
||||
} from "../../../../types/order-edit"
|
||||
import { OrderEdit } from "../../../../models"
|
||||
import { StorePostOrderEditsOrderEditDecline } from "./decline-order-edit"
|
||||
@@ -25,13 +25,9 @@ export default (app) => {
|
||||
route.get(
|
||||
"/:id",
|
||||
transformQuery(EmptyQueryParams, {
|
||||
defaultRelations: defaultOrderEditRelations.filter(
|
||||
(field) => !storeOrderEditNotAllowedFields.includes(field)
|
||||
),
|
||||
defaultFields: defaultOrderEditFields.filter(
|
||||
(field) => !storeOrderEditNotAllowedFields.includes(field)
|
||||
),
|
||||
allowedFields: defaultOrderEditFields,
|
||||
defaultRelations: defaultStoreOrderEditRelations,
|
||||
defaultFields: defaultStoreOrderEditFields,
|
||||
allowedFields: defaultStoreOrderEditFields,
|
||||
isList: false,
|
||||
}),
|
||||
middlewares.wrap(require("./get-order-edit").default)
|
||||
@@ -54,10 +50,3 @@ export type StoreOrderEditsRes = {
|
||||
}
|
||||
|
||||
export * from "./decline-order-edit"
|
||||
|
||||
export const storeOrderEditNotAllowedFields = [
|
||||
"internal_note",
|
||||
"created_by",
|
||||
"confirmed_by",
|
||||
"canceled_by",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
|
||||
import OrderEditingFeatureFlag from "../loaders/feature-flags/order-editing"
|
||||
|
||||
export const featureFlag = OrderEditingFeatureFlag.key
|
||||
|
||||
export class lineItemOriginalItemRelation1663059812400
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = "lineItemOriginalItemRelation1663059812400"
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "line_item"
|
||||
ADD COLUMN IF NOT EXISTS original_item_id character varying,
|
||||
ADD COLUMN IF NOT EXISTS order_edit_id character varying`
|
||||
)
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "line_item"
|
||||
ADD CONSTRAINT "line_item_original_item_fk" FOREIGN KEY ("original_item_id") REFERENCES "line_item" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "line_item"
|
||||
ADD CONSTRAINT "line_item_order_edit_fk" FOREIGN KEY ("order_edit_id") REFERENCES "order_edit" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "unique_li_original_item_id_order_edit_id" ON "line_item" ("order_edit_id", "original_item_id") WHERE original_item_id IS NOT NULL AND order_edit_id IS NOT NULL`
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP INDEX IF EXISTS "unique_li_original_item_id_order_edit_id"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "line_item" DROP CONSTRAINT "line_item_original_item_fk"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "line_item" DROP CONSTRAINT "line_item_order_edit_fk"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "line_item" DROP COLUMN "original_item_id"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "line_item" DROP COLUMN "order_edit_id"`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
OneToMany,
|
||||
} from "typeorm"
|
||||
|
||||
import { BaseEntity } from "../interfaces/models/base-entity"
|
||||
import { BaseEntity } from "../interfaces"
|
||||
import { Cart } from "./cart"
|
||||
import { ClaimOrder } from "./claim-order"
|
||||
import { DbAwareColumn } from "../utils/db-aware-column"
|
||||
@@ -18,14 +18,30 @@ import { LineItemTaxLine } from "./line-item-tax-line"
|
||||
import { Order } from "./order"
|
||||
import { ProductVariant } from "./product-variant"
|
||||
import { Swap } from "./swap"
|
||||
import { generateEntityId } from "../utils/generate-entity-id"
|
||||
import { FeatureFlagColumn } from "../utils/feature-flag-decorators"
|
||||
import { generateEntityId } from "../utils"
|
||||
import {
|
||||
FeatureFlagClassDecorators,
|
||||
FeatureFlagColumn,
|
||||
FeatureFlagDecorators,
|
||||
} from "../utils/feature-flag-decorators"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import OrderEditingFeatureFlag from "../loaders/feature-flags/order-editing"
|
||||
import { OrderEdit } from "./order-edit"
|
||||
|
||||
@Check(`"fulfilled_quantity" <= "quantity"`)
|
||||
@Check(`"shipped_quantity" <= "fulfilled_quantity"`)
|
||||
@Check(`"returned_quantity" <= "quantity"`)
|
||||
@Check(`"quantity" > 0`)
|
||||
@FeatureFlagClassDecorators(OrderEditingFeatureFlag.key, [
|
||||
Index(
|
||||
"unique_li_original_item_id_order_edit_id",
|
||||
["order_edit_id", "original_item_id"],
|
||||
{
|
||||
unique: true,
|
||||
where: "WHERE original_item_id IS NOT NULL AND order_edit_id IS NOT NULL",
|
||||
}
|
||||
),
|
||||
])
|
||||
@Entity()
|
||||
export class LineItem extends BaseEntity {
|
||||
@Index()
|
||||
@@ -68,6 +84,27 @@ export class LineItem extends BaseEntity {
|
||||
})
|
||||
adjustments: LineItemAdjustment[]
|
||||
|
||||
@FeatureFlagColumn(OrderEditingFeatureFlag.key, {
|
||||
nullable: true,
|
||||
type: "varchar",
|
||||
})
|
||||
original_item_id?: string | null
|
||||
|
||||
@FeatureFlagColumn(OrderEditingFeatureFlag.key, {
|
||||
nullable: true,
|
||||
type: "varchar",
|
||||
})
|
||||
order_edit_id?: string | null
|
||||
|
||||
@FeatureFlagDecorators(OrderEditingFeatureFlag.key, [
|
||||
ManyToOne(
|
||||
() => OrderEdit,
|
||||
(orderEdit) => orderEdit.items
|
||||
),
|
||||
JoinColumn({ name: "order_edit_id" }),
|
||||
])
|
||||
order_edit?: OrderEdit | null
|
||||
|
||||
@Column()
|
||||
title: string
|
||||
|
||||
@@ -283,6 +320,15 @@ export class LineItem extends BaseEntity {
|
||||
* includes_tax:
|
||||
* description: "[EXPERIMENTAL] Indicates if the line item unit_price include tax"
|
||||
* type: boolean
|
||||
* original_item_id:
|
||||
* description: "[EXPERIMENTAL] The id of the original line item"
|
||||
* type: string
|
||||
* order_edit_id:
|
||||
* description: "[EXPERIMENTAL] The ID of the order edit to which a cloned item belongs"
|
||||
* type: string
|
||||
* order_edit:
|
||||
* description: "[EXPERIMENTAL] The order edit joined"
|
||||
* type: object
|
||||
* created_at:
|
||||
* type: string
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
AfterLoad,
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
@@ -72,6 +71,9 @@ export class OrderEdit extends BaseEntity {
|
||||
@Column({ type: resolveDbType("timestamptz"), nullable: true })
|
||||
canceled_at?: Date
|
||||
|
||||
@OneToMany(() => LineItem, (lineItem) => lineItem.order_edit)
|
||||
items: LineItem[]
|
||||
|
||||
// Computed
|
||||
shipping_total: number
|
||||
discount_total: number
|
||||
@@ -85,9 +87,6 @@ export class OrderEdit extends BaseEntity {
|
||||
|
||||
status: OrderEditStatus
|
||||
|
||||
items: LineItem[]
|
||||
removed_items: LineItem[]
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert(): void {
|
||||
this.id = generateEntityId(this.id, "oe")
|
||||
@@ -207,9 +206,4 @@ export class OrderEdit extends BaseEntity {
|
||||
* description: Computed line items from the changes.
|
||||
* items:
|
||||
* $ref: "#/components/schemas/line_item"
|
||||
* removed_items:
|
||||
* type: array
|
||||
* description: Computed line items from the changes that have been marked as deleted.
|
||||
* items:
|
||||
* $ref: "#/components/schemas/line_item"
|
||||
*/
|
||||
|
||||
@@ -1,68 +1,5 @@
|
||||
import { EntityRepository, FindManyOptions, Repository } from "typeorm"
|
||||
|
||||
import { OrderEdit } from "../models/order-edit"
|
||||
import { flatten, groupBy, merge } from "lodash"
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { OrderEdit } from "../models"
|
||||
|
||||
@EntityRepository(OrderEdit)
|
||||
export class OrderEditRepository extends Repository<OrderEdit> {
|
||||
public async findWithRelations(
|
||||
relations: (keyof OrderEdit | string)[] = [],
|
||||
idsOrOptionsWithoutRelations:
|
||||
| Omit<FindManyOptions<OrderEdit>, "relations">
|
||||
| string[] = {}
|
||||
): Promise<[OrderEdit[], number]> {
|
||||
let entities: OrderEdit[] = []
|
||||
let count
|
||||
if (Array.isArray(idsOrOptionsWithoutRelations)) {
|
||||
entities = await this.findByIds(idsOrOptionsWithoutRelations)
|
||||
count = idsOrOptionsWithoutRelations.length
|
||||
} else {
|
||||
const [results, resultCount] = await this.findAndCount(
|
||||
idsOrOptionsWithoutRelations
|
||||
)
|
||||
entities = results
|
||||
count = resultCount
|
||||
}
|
||||
const entitiesIds = entities.map(({ id }) => id)
|
||||
|
||||
const groupedRelations = {}
|
||||
for (const rel of relations) {
|
||||
const [topLevel] = rel.split(".")
|
||||
if (groupedRelations[topLevel]) {
|
||||
groupedRelations[topLevel].push(rel)
|
||||
} else {
|
||||
groupedRelations[topLevel] = [rel]
|
||||
}
|
||||
}
|
||||
|
||||
const entitiesIdsWithRelations = await Promise.all(
|
||||
Object.entries(groupedRelations).map(async ([_, rels]) => {
|
||||
return this.findByIds(entitiesIds, {
|
||||
select: ["id"],
|
||||
relations: rels as string[],
|
||||
})
|
||||
})
|
||||
).then(flatten)
|
||||
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
|
||||
|
||||
const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id")
|
||||
return [
|
||||
Object.values(entitiesAndRelationsById).map((v) => merge({}, ...v)),
|
||||
count,
|
||||
]
|
||||
}
|
||||
|
||||
public async findOneWithRelations(
|
||||
relations: Array<keyof OrderEdit> = [],
|
||||
optionsWithoutRelations: Omit<FindManyOptions<OrderEdit>, "relations"> = {}
|
||||
): Promise<OrderEdit> {
|
||||
// Limit 1
|
||||
optionsWithoutRelations.take = 1
|
||||
|
||||
const [result] = await this.findWithRelations(
|
||||
relations,
|
||||
optionsWithoutRelations
|
||||
)
|
||||
return result[0]
|
||||
}
|
||||
}
|
||||
export class OrderEditRepository extends Repository<OrderEdit> {}
|
||||
|
||||
@@ -92,7 +92,7 @@ export const orderEditServiceMock = {
|
||||
delete: jest.fn().mockImplementation((_) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
decorateLineItemsAndTotals: jest.fn().mockImplementation((orderEdit) => {
|
||||
decorateTotals: jest.fn().mockImplementation((orderEdit) => {
|
||||
const withLineItems = computeLineItems(orderEdit)
|
||||
return Promise.resolve({
|
||||
...withLineItems,
|
||||
|
||||
17
packages/medusa/src/services/__mocks__/tax-provider.js
Normal file
17
packages/medusa/src/services/__mocks__/tax-provider.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export const taxProviderServiceMock = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
createTaxLines: jest.fn().mockImplementation((order, calculationContext) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
clearLineItemsTaxLines: jest.fn().mockImplementation((_) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
return taxProviderServiceMock
|
||||
})
|
||||
|
||||
export default mock
|
||||
@@ -1,6 +1,10 @@
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import { FlagRouter } from "../../utils/flag-router"
|
||||
import LineItemService from "../line-item"
|
||||
import { PricingServiceMock } from "../__mocks__/pricing"
|
||||
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
|
||||
import { RegionServiceMock } from "../__mocks__/region"
|
||||
|
||||
;[true, false].forEach((isTaxInclusiveEnabled) => {
|
||||
describe(`tax inclusive flag set to: ${isTaxInclusiveEnabled}`, () => {
|
||||
describe("LineItemService", () => {
|
||||
@@ -517,5 +521,106 @@ describe("LineItemService", () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("clone", () => {
|
||||
const buildLineItem = (id) => ({
|
||||
id,
|
||||
original_item_id: id,
|
||||
swap_id: "test",
|
||||
order_id: "test",
|
||||
tax_lines: [
|
||||
{
|
||||
rate: 10,
|
||||
item_id: id,
|
||||
},
|
||||
],
|
||||
adjustments: [
|
||||
{
|
||||
amount: 10,
|
||||
item_id: id,
|
||||
},
|
||||
],
|
||||
})
|
||||
const buildExpectedLineItem = (id) =>
|
||||
expect.objectContaining({
|
||||
original_item_id: id,
|
||||
swap_id: undefined,
|
||||
claim_order_id: undefined,
|
||||
cart_id: undefined,
|
||||
order_edit_id: undefined,
|
||||
order_id: "test",
|
||||
tax_lines: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
rate: 10,
|
||||
}),
|
||||
]),
|
||||
adjustments: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
amount: 10,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
|
||||
const lineItemRepository = MockRepository({
|
||||
create: (data) => data,
|
||||
save: (data) => data,
|
||||
find: (selector) => {
|
||||
return selector.where.id.value.map(buildLineItem)
|
||||
},
|
||||
})
|
||||
|
||||
const featureFlagRouter = new FlagRouter({})
|
||||
|
||||
const lineItemService = new LineItemService({
|
||||
manager: MockManager,
|
||||
pricingService: PricingServiceMock,
|
||||
lineItemRepository,
|
||||
productVariantService: ProductVariantServiceMock,
|
||||
regionService: RegionServiceMock,
|
||||
cartRepository: MockRepository,
|
||||
featureFlagRouter,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully clone line items with tax lines and adjustments", async () => {
|
||||
const lineItemId1 = IdMap.getId("line-item-1")
|
||||
const lineItemId2 = IdMap.getId("line-item-2")
|
||||
|
||||
await lineItemService.cloneTo([lineItemId1, lineItemId2], {
|
||||
order_id: "test",
|
||||
})
|
||||
|
||||
expect(lineItemRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemRepository.create).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
buildExpectedLineItem(lineItemId1),
|
||||
buildExpectedLineItem(lineItemId2),
|
||||
])
|
||||
)
|
||||
expect(lineItemRepository.save).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
buildExpectedLineItem(lineItemId1),
|
||||
buildExpectedLineItem(lineItemId2),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("throw on clone line items if none of the foreign keys is specified", async () => {
|
||||
const lineItemId1 = IdMap.getId("line-item-1")
|
||||
const lineItemId2 = IdMap.getId("line-item-2")
|
||||
|
||||
const err = await lineItemService
|
||||
.cloneTo([lineItemId1, lineItemId2])
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err.message).toBe(
|
||||
"Unable to clone a line item that is not attached to at least one of: order_edit, order, swap, claim or cart."
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
OrderEditItemChangeService,
|
||||
OrderEditService,
|
||||
OrderService,
|
||||
TaxProviderService,
|
||||
TotalsService,
|
||||
} from "../index"
|
||||
import { OrderEditItemChangeType, OrderEditStatus } from "../../models"
|
||||
@@ -13,6 +14,9 @@ import { EventBusServiceMock } from "../__mocks__/event-bus"
|
||||
import { LineItemServiceMock } from "../__mocks__/line-item"
|
||||
import { TotalsServiceMock } from "../__mocks__/totals"
|
||||
import { orderEditItemChangeServiceMock } from "../__mocks__/order-edit-item-change"
|
||||
import { taxProviderServiceMock } from "../__mocks__/tax-provider"
|
||||
import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment"
|
||||
import LineItemAdjustmentService from "../line-item-adjustment"
|
||||
|
||||
const orderEditToUpdate = {
|
||||
id: IdMap.getId("order-edit-to-update"),
|
||||
@@ -80,6 +84,7 @@ const lineItemServiceMock = {
|
||||
id,
|
||||
})
|
||||
}),
|
||||
cloneTo: () => [],
|
||||
}
|
||||
|
||||
describe("OrderEditService", () => {
|
||||
@@ -88,7 +93,10 @@ describe("OrderEditService", () => {
|
||||
})
|
||||
|
||||
const orderEditRepository = MockRepository({
|
||||
findOneWithRelations: (relations, query) => {
|
||||
findOne: (query) => {
|
||||
if (query?.where?.id === IdMap.getId("order-edit-to-update")) {
|
||||
return orderEditToUpdate
|
||||
}
|
||||
if (query?.where?.id === IdMap.getId("order-edit-with-changes")) {
|
||||
return orderEditWithChanges
|
||||
}
|
||||
@@ -117,7 +125,7 @@ describe("OrderEditService", () => {
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
return
|
||||
},
|
||||
create: (data) => {
|
||||
return {
|
||||
@@ -136,17 +144,17 @@ describe("OrderEditService", () => {
|
||||
lineItemService: lineItemServiceMock as unknown as LineItemService,
|
||||
orderEditItemChangeService:
|
||||
orderEditItemChangeServiceMock as unknown as OrderEditItemChangeService,
|
||||
taxProviderService: taxProviderServiceMock as unknown as TaxProviderService,
|
||||
lineItemAdjustmentService:
|
||||
LineItemAdjustmentServiceMock as unknown as LineItemAdjustmentService,
|
||||
})
|
||||
|
||||
it("should retrieve an order edit and call the repository with the right arguments", async () => {
|
||||
await orderEditService.retrieve(IdMap.getId("order-edit-with-changes"))
|
||||
expect(orderEditRepository.findOneWithRelations).toHaveBeenCalledTimes(1)
|
||||
expect(orderEditRepository.findOneWithRelations).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
{
|
||||
where: { id: IdMap.getId("order-edit-with-changes") },
|
||||
}
|
||||
)
|
||||
expect(orderEditRepository.findOne).toHaveBeenCalledTimes(1)
|
||||
expect(orderEditRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { id: IdMap.getId("order-edit-with-changes") },
|
||||
})
|
||||
})
|
||||
|
||||
it("should update an order edit with the right arguments", async () => {
|
||||
@@ -155,37 +163,11 @@ describe("OrderEditService", () => {
|
||||
})
|
||||
expect(orderEditRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(orderEditRepository.save).toHaveBeenCalledWith({
|
||||
id: IdMap.getId("order-edit-to-update"),
|
||||
internal_note: "test note",
|
||||
})
|
||||
})
|
||||
|
||||
it("should compute the items from the changes and attach them to the orderEdit", async () => {
|
||||
const { items, removedItems } = await orderEditService.computeLineItems(
|
||||
IdMap.getId("order-edit-with-changes")
|
||||
)
|
||||
|
||||
expect(items.length).toBe(2)
|
||||
expect(items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: IdMap.getId("line-item-2"),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: IdMap.getId("line-item-3"),
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
expect(removedItems.length).toBe(1)
|
||||
expect(removedItems).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: IdMap.getId("line-item-1"),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should create an order edit and call the repository with the right arguments as well as the event bus service", async () => {
|
||||
const data = {
|
||||
order_id: IdMap.getId("order-edit-order-id"),
|
||||
@@ -267,7 +249,9 @@ describe("OrderEditService", () => {
|
||||
let result
|
||||
|
||||
beforeEach(async () => {
|
||||
result = await orderEditService.requestConfirmation(orderEditId, {loggedInUser: userId})
|
||||
result = await orderEditService.requestConfirmation(orderEditId, {
|
||||
loggedInUser: userId,
|
||||
})
|
||||
})
|
||||
|
||||
it("sets fields correctly for update", async () => {
|
||||
@@ -283,13 +267,12 @@ describe("OrderEditService", () => {
|
||||
requested_at: expect.any(Date),
|
||||
requested_by: userId,
|
||||
})
|
||||
|
||||
|
||||
expect(EventBusServiceMock.emit).toHaveBeenCalledWith(
|
||||
OrderEditService.Events.REQUESTED,
|
||||
{ id: orderEditId }
|
||||
)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe("requested edit", () => {
|
||||
@@ -298,7 +281,9 @@ describe("OrderEditService", () => {
|
||||
let result
|
||||
|
||||
beforeEach(async () => {
|
||||
result = await orderEditService.requestConfirmation(orderEditId, {loggedInUser: userId})
|
||||
result = await orderEditService.requestConfirmation(orderEditId, {
|
||||
loggedInUser: userId,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { EntityManager, In } from "typeorm"
|
||||
import { DeepPartial } from "typeorm/common/DeepPartial"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import { LineItemTaxLine } from "../models"
|
||||
@@ -363,6 +363,77 @@ class LineItemService extends BaseService {
|
||||
|
||||
return itemTaxLineRepo.create(args)
|
||||
}
|
||||
|
||||
async cloneTo(
|
||||
ids: string | string[],
|
||||
data: DeepPartial<LineItem> = {},
|
||||
options: { setOriginalLineItemId?: boolean } = {
|
||||
setOriginalLineItemId: true,
|
||||
}
|
||||
): Promise<LineItem[]> {
|
||||
ids = typeof ids === "string" ? [ids] : ids
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
let lineItems: DeepPartial<LineItem>[] = await this.list(
|
||||
{
|
||||
id: In(ids as string[]),
|
||||
},
|
||||
{
|
||||
relations: ["tax_lines", "adjustments"],
|
||||
}
|
||||
)
|
||||
|
||||
const lineItemRepository = manager.getCustomRepository(
|
||||
this.lineItemRepository_
|
||||
)
|
||||
|
||||
const {
|
||||
order_id,
|
||||
swap_id,
|
||||
claim_order_id,
|
||||
cart_id,
|
||||
order_edit_id,
|
||||
...lineItemData
|
||||
} = data
|
||||
|
||||
if (
|
||||
!order_id &&
|
||||
!swap_id &&
|
||||
!claim_order_id &&
|
||||
!cart_id &&
|
||||
!order_edit_id
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Unable to clone a line item that is not attached to at least one of: order_edit, order, swap, claim or cart."
|
||||
)
|
||||
}
|
||||
|
||||
lineItems = lineItems.map((item) => ({
|
||||
...item,
|
||||
...lineItemData,
|
||||
id: undefined,
|
||||
order_id,
|
||||
swap_id,
|
||||
claim_order_id,
|
||||
cart_id,
|
||||
order_edit_id,
|
||||
original_item_id: options?.setOriginalLineItemId ? item.id : undefined,
|
||||
tax_lines: item.tax_lines?.map((tax_line) => ({
|
||||
...tax_line,
|
||||
id: undefined,
|
||||
item_id: undefined,
|
||||
})),
|
||||
adjustments: item.adjustments?.map((adj) => ({
|
||||
...adj,
|
||||
id: undefined,
|
||||
item_id: undefined,
|
||||
})),
|
||||
}))
|
||||
|
||||
const clonedLineItemEntities = lineItemRepository.create(lineItems)
|
||||
return await lineItemRepository.save(clonedLineItemEntities)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default LineItemService
|
||||
|
||||
@@ -3,22 +3,18 @@ import { FindConfig } from "../types/common"
|
||||
import { buildQuery, isDefined } from "../utils"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { OrderEditRepository } from "../repositories/order-edit"
|
||||
import {
|
||||
LineItem,
|
||||
Order,
|
||||
OrderEdit,
|
||||
OrderEditItemChangeType,
|
||||
OrderEditStatus,
|
||||
} from "../models"
|
||||
import { Order, OrderEdit, OrderEditStatus } from "../models"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import {
|
||||
EventBusService,
|
||||
LineItemService,
|
||||
OrderEditItemChangeService,
|
||||
OrderService,
|
||||
TaxProviderService,
|
||||
TotalsService,
|
||||
} from "./index"
|
||||
import { CreateOrderEditInput, UpdateOrderEditInput } from "../types/order-edit"
|
||||
import LineItemAdjustmentService from "./line-item-adjustment"
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
@@ -28,6 +24,8 @@ type InjectedDependencies = {
|
||||
totalsService: TotalsService
|
||||
lineItemService: LineItemService
|
||||
orderEditItemChangeService: OrderEditItemChangeService
|
||||
lineItemAdjustmentService: LineItemAdjustmentService
|
||||
taxProviderService: TaxProviderService
|
||||
}
|
||||
|
||||
export default class OrderEditService extends TransactionBaseService {
|
||||
@@ -46,6 +44,8 @@ export default class OrderEditService extends TransactionBaseService {
|
||||
protected readonly eventBusService_: EventBusService
|
||||
protected readonly totalsService_: TotalsService
|
||||
protected readonly orderEditItemChangeService_: OrderEditItemChangeService
|
||||
protected readonly lineItemAdjustmentService_: LineItemAdjustmentService
|
||||
protected readonly taxProviderService_: TaxProviderService
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
@@ -55,6 +55,8 @@ export default class OrderEditService extends TransactionBaseService {
|
||||
eventBusService,
|
||||
totalsService,
|
||||
orderEditItemChangeService,
|
||||
lineItemAdjustmentService,
|
||||
taxProviderService,
|
||||
}: InjectedDependencies) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
@@ -66,6 +68,8 @@ export default class OrderEditService extends TransactionBaseService {
|
||||
this.eventBusService_ = eventBusService
|
||||
this.totalsService_ = totalsService
|
||||
this.orderEditItemChangeService_ = orderEditItemChangeService
|
||||
this.lineItemAdjustmentService_ = lineItemAdjustmentService
|
||||
this.taxProviderService_ = taxProviderService
|
||||
}
|
||||
|
||||
async retrieve(
|
||||
@@ -76,12 +80,9 @@ export default class OrderEditService extends TransactionBaseService {
|
||||
const orderEditRepository = manager.getCustomRepository(
|
||||
this.orderEditRepository_
|
||||
)
|
||||
const { relations, ...query } = buildQuery({ id: orderEditId }, config)
|
||||
|
||||
const orderEdit = await orderEditRepository.findOneWithRelations(
|
||||
relations as (keyof OrderEdit)[],
|
||||
query
|
||||
)
|
||||
const query = buildQuery({ id: orderEditId }, config)
|
||||
const orderEdit = await orderEditRepository.findOne(query)
|
||||
|
||||
if (!orderEdit) {
|
||||
throw new MedusaError(
|
||||
@@ -114,80 +115,6 @@ export default class OrderEditService extends TransactionBaseService {
|
||||
return await orderEditRepository.findOne(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute line items across order and order edit
|
||||
* - if an item have been removed, it will appear in the removedItems collection and will not appear in the item collection
|
||||
* - if an item have been updated, it will appear in the item collection with id being the id of the original item and the rest of the data being the data of the new item generated from the update
|
||||
* - if an item have been added, it will appear in the item collection with id being the id of the new item and the rest of the data being the data of the new item generated from the add
|
||||
* @param orderEditId
|
||||
*/
|
||||
async computeLineItems(
|
||||
orderEditId: string
|
||||
): Promise<{ items: LineItem[]; removedItems: LineItem[] }> {
|
||||
const manager = this.transactionManager_ ?? this.manager_
|
||||
|
||||
const lineItemServiceTx = this.lineItemService_.withTransaction(manager)
|
||||
|
||||
const orderEdit = await this.retrieve(orderEditId, {
|
||||
select: ["id", "order_id", "changes"],
|
||||
relations: ["changes", "changes.original_line_item", "changes.line_item"],
|
||||
})
|
||||
|
||||
const items: LineItem[] = []
|
||||
const orderEditRemovedItemsMap: Map<string, LineItem> = new Map()
|
||||
const orderEditUpdatedItemsMap: Map<string, LineItem> = new Map()
|
||||
|
||||
for (const change of orderEdit.changes) {
|
||||
const lineItemId =
|
||||
change.type === OrderEditItemChangeType.ITEM_REMOVE
|
||||
? change.original_line_item_id!
|
||||
: change.line_item_id!
|
||||
|
||||
const lineItem = await lineItemServiceTx.retrieve(lineItemId!, {
|
||||
relations: ["tax_lines", "adjustments"],
|
||||
})
|
||||
|
||||
if (change.type === OrderEditItemChangeType.ITEM_REMOVE) {
|
||||
orderEditRemovedItemsMap.set(change.original_line_item_id!, lineItem)
|
||||
continue
|
||||
}
|
||||
|
||||
if (change.type === OrderEditItemChangeType.ITEM_ADD) {
|
||||
items.push(lineItem)
|
||||
continue
|
||||
}
|
||||
|
||||
orderEditUpdatedItemsMap.set(change.original_line_item_id!, {
|
||||
...lineItem,
|
||||
id: change.original_line_item_id!,
|
||||
} as LineItem)
|
||||
}
|
||||
|
||||
const originalLineItems = await this.lineItemService_
|
||||
.withTransaction(manager)
|
||||
.list(
|
||||
{
|
||||
order_id: orderEdit.order_id,
|
||||
},
|
||||
{
|
||||
relations: ["tax_lines", "adjustments"],
|
||||
}
|
||||
)
|
||||
|
||||
for (const originalLineItem of originalLineItems) {
|
||||
const itemRemoved = orderEditRemovedItemsMap.get(originalLineItem.id)
|
||||
if (itemRemoved) {
|
||||
continue
|
||||
}
|
||||
|
||||
const updatedLineItem = orderEditUpdatedItemsMap.get(originalLineItem.id)
|
||||
const lineItem = updatedLineItem ?? originalLineItem
|
||||
items.push(lineItem)
|
||||
}
|
||||
|
||||
return { items, removedItems: [...orderEditRemovedItemsMap.values()] }
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute and return the different totals from the order edit id
|
||||
* @param orderEditId
|
||||
@@ -202,8 +129,9 @@ export default class OrderEditService extends TransactionBaseService {
|
||||
total: number
|
||||
}> {
|
||||
const manager = this.transactionManager_ ?? this.manager_
|
||||
const { order_id } = await this.retrieve(orderEditId, {
|
||||
select: ["order_id"],
|
||||
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)
|
||||
@@ -218,7 +146,6 @@ export default class OrderEditService extends TransactionBaseService {
|
||||
"shipping_methods.tax_lines",
|
||||
],
|
||||
})
|
||||
const { items } = await this.computeLineItems(orderEditId)
|
||||
const computedOrder = { ...order, items } as Order
|
||||
|
||||
const totalsServiceTx = this.totalsService_.withTransaction(manager)
|
||||
@@ -267,6 +194,22 @@ export default class OrderEditService extends TransactionBaseService {
|
||||
|
||||
const orderEdit = await orderEditRepository.save(orderEditToCreate)
|
||||
|
||||
const lineItemServiceTx =
|
||||
this.lineItemService_.withTransaction(transactionManager)
|
||||
|
||||
const orderLineItems = await lineItemServiceTx.list(
|
||||
{
|
||||
order_id: data.order_id,
|
||||
},
|
||||
{
|
||||
select: ["id"],
|
||||
}
|
||||
)
|
||||
const lineItemIds = orderLineItems.map(({ id }) => id)
|
||||
await lineItemServiceTx.cloneTo(lineItemIds, {
|
||||
order_edit_id: orderEdit.id,
|
||||
})
|
||||
|
||||
await this.eventBusService_
|
||||
.withTransaction(transactionManager)
|
||||
.emit(OrderEditService.Events.CREATED, { id: orderEdit.id })
|
||||
@@ -304,13 +247,13 @@ export default class OrderEditService extends TransactionBaseService {
|
||||
})
|
||||
}
|
||||
|
||||
async delete(orderEditId: string): Promise<void> {
|
||||
async delete(id: string): Promise<void> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const orderEditRepo = manager.getCustomRepository(
|
||||
this.orderEditRepository_
|
||||
)
|
||||
|
||||
const edit = await orderEditRepo.findOne({ where: { id: orderEditId } })
|
||||
const edit = await this.retrieve(id).catch(() => void 0)
|
||||
|
||||
if (!edit) {
|
||||
return
|
||||
@@ -323,6 +266,7 @@ export default class OrderEditService extends TransactionBaseService {
|
||||
)
|
||||
}
|
||||
|
||||
await this.deleteClonedItems(id)
|
||||
await orderEditRepo.remove(edit)
|
||||
})
|
||||
}
|
||||
@@ -370,19 +314,6 @@ export default class OrderEditService extends TransactionBaseService {
|
||||
})
|
||||
}
|
||||
|
||||
async decorateLineItemsAndTotals(orderEdit: OrderEdit): Promise<OrderEdit> {
|
||||
const lineItemDecoratedOrderEdit = await this.decorateLineItems(orderEdit)
|
||||
return await this.decorateTotals(lineItemDecoratedOrderEdit)
|
||||
}
|
||||
|
||||
async decorateLineItems(orderEdit: OrderEdit): Promise<OrderEdit> {
|
||||
const { items, removedItems } = await this.computeLineItems(orderEdit.id)
|
||||
orderEdit.items = items
|
||||
orderEdit.removed_items = removedItems
|
||||
|
||||
return orderEdit
|
||||
}
|
||||
|
||||
async decorateTotals(orderEdit: OrderEdit): Promise<OrderEdit> {
|
||||
const totals = await this.getTotals(orderEdit.id)
|
||||
orderEdit.discount_total = totals.discount_total
|
||||
@@ -467,4 +398,41 @@ export default class OrderEditService extends TransactionBaseService {
|
||||
return orderEdit
|
||||
})
|
||||
}
|
||||
|
||||
protected async deleteClonedItems(orderEditId: string): Promise<void> {
|
||||
const manager = this.transactionManager_ ?? this.manager_
|
||||
const lineItemServiceTx = this.lineItemService_.withTransaction(manager)
|
||||
const lineItemAdjustmentServiceTx =
|
||||
this.lineItemAdjustmentService_.withTransaction(manager)
|
||||
const taxProviderServiceTs =
|
||||
this.taxProviderService_.withTransaction(manager)
|
||||
|
||||
const clonedLineItems = await lineItemServiceTx.list(
|
||||
{
|
||||
order_edit_id: orderEditId,
|
||||
},
|
||||
{
|
||||
select: ["id", "tax_lines", "adjustments"],
|
||||
relations: ["tax_lines", "adjustments"],
|
||||
}
|
||||
)
|
||||
const clonedItemIds = clonedLineItems.map((item) => item.id)
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
taxProviderServiceTs.clearLineItemsTaxLines(clonedItemIds),
|
||||
clonedItemIds.map((id) => {
|
||||
return lineItemAdjustmentServiceTx.delete({
|
||||
item_id: id,
|
||||
})
|
||||
}),
|
||||
].flat()
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
clonedItemIds.map((id) => {
|
||||
return lineItemServiceTx.delete(id)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,22 @@ export type UpdateOrderEditInput = {
|
||||
internal_note?: string
|
||||
}
|
||||
|
||||
export type CreateOrderEditInput = {
|
||||
order_id: string
|
||||
internal_note?: string
|
||||
}
|
||||
|
||||
export const defaultOrderEditRelations: string[] = [
|
||||
"changes",
|
||||
"changes.line_item",
|
||||
"changes.original_line_item",
|
||||
"items",
|
||||
"items.tax_lines",
|
||||
]
|
||||
|
||||
export const defaultOrderEditFields: (keyof OrderEdit)[] = [
|
||||
"id",
|
||||
"items",
|
||||
"changes",
|
||||
"order_id",
|
||||
"created_by",
|
||||
@@ -27,7 +35,16 @@ export const defaultOrderEditFields: (keyof OrderEdit)[] = [
|
||||
"internal_note",
|
||||
]
|
||||
|
||||
export type CreateOrderEditInput = {
|
||||
order_id: string
|
||||
internal_note?: string
|
||||
}
|
||||
export const storeOrderEditNotAllowedFields = [
|
||||
"internal_note",
|
||||
"created_by",
|
||||
"confirmed_by",
|
||||
"canceled_by",
|
||||
]
|
||||
|
||||
export const defaultStoreOrderEditRelations = defaultOrderEditRelations.filter(
|
||||
(field) => !storeOrderEditNotAllowedFields.includes(field)
|
||||
)
|
||||
export const defaultStoreOrderEditFields = defaultOrderEditFields.filter(
|
||||
(field) => !storeOrderEditNotAllowedFields.includes(field)
|
||||
)
|
||||
|
||||
@@ -50,6 +50,23 @@ export function FeatureFlagDecorators(
|
||||
}
|
||||
}
|
||||
|
||||
export function FeatureFlagClassDecorators(
|
||||
featureFlag: string,
|
||||
decorators: ClassDecorator[]
|
||||
): ClassDecorator {
|
||||
return function (target) {
|
||||
setImmediate_((): any => {
|
||||
if (!featureFlagRouter.isFeatureEnabled(featureFlag)) {
|
||||
return
|
||||
}
|
||||
|
||||
decorators.forEach((decorator: ClassDecorator) => {
|
||||
decorator(target)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function FeatureFlagEntity(
|
||||
featureFlag: string,
|
||||
name?: string,
|
||||
|
||||
Reference in New Issue
Block a user