Feat(medusa, medusa-js, medusa-react): order edit confirmation (#2264)
**what** Support confirm of an order edit: Upon confirmation, the items of the original order are detached and the items from the order edit are attached to the order. The order total is recomputed with the correct total which can defer from the paid_total and refundable_amount (based on the paid_total) **Tests** - Unit tests medusa-js and medusa-react as well as the core - Integration test of the confirmation flow which check that the order edit is properly confirmed and can be confirmed idempotently. Also validate the totals and that the order items correspond to the order edit items. Also validate the order totals. FIXES CORE-498
This commit is contained in:
1 parent
87ad29dda4
commit
2be00007b2
22 files changed
+673
-135
No files matched your search
@@ -1322,7 +1322,7 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("POST /admin/order-edits/:id", () => {
|
describe("POST /admin/order-edits/:id/cancel", () => {
|
||||||
const cancellableEditId = IdMap.getId("order-edit-1")
|
const cancellableEditId = IdMap.getId("order-edit-1")
|
||||||
const canceledEditId = IdMap.getId("order-edit-2")
|
const canceledEditId = IdMap.getId("order-edit-2")
|
||||||
const confirmedEditId = IdMap.getId("order-edit-3")
|
const confirmedEditId = IdMap.getId("order-edit-3")
|
||||||
@@ -1387,7 +1387,6 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
|
|||||||
{},
|
{},
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.order_edit).toEqual(
|
expect(response.data.order_edit).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -1419,6 +1418,210 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/order-edits/:id/confirm", () => {
|
||||||
|
let product, product2
|
||||||
|
const prodId1 = IdMap.getId("product-1")
|
||||||
|
const prodId2 = IdMap.getId("product-2")
|
||||||
|
const lineItemId1 = IdMap.getId("line-item-1")
|
||||||
|
const lineItemId2 = IdMap.getId("line-item-2")
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
product = await simpleProductFactory(dbConnection, {
|
||||||
|
id: prodId1,
|
||||||
|
})
|
||||||
|
|
||||||
|
product2 = await simpleProductFactory(dbConnection, {
|
||||||
|
id: prodId2,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
return await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("confirms an order edit", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const region = await simpleRegionFactory(dbConnection, { tax_rate: 10 })
|
||||||
|
|
||||||
|
const cart = await simpleCartFactory(dbConnection, {
|
||||||
|
email: "adrien@test.com",
|
||||||
|
region: region.id,
|
||||||
|
line_items: [
|
||||||
|
{
|
||||||
|
id: lineItemId1,
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
quantity: 1,
|
||||||
|
unit_price: 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: lineItemId2,
|
||||||
|
variant_id: product2.variants[0].id,
|
||||||
|
quantity: 1,
|
||||||
|
unit_price: 1000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
await api.post(`/store/carts/${cart.id}/payment-sessions`)
|
||||||
|
|
||||||
|
const completeRes = await api.post(`/store/carts/${cart.id}/complete`)
|
||||||
|
|
||||||
|
const order = completeRes.data.data
|
||||||
|
|
||||||
|
let response = await api.post(
|
||||||
|
`/admin/order-edits/`,
|
||||||
|
{
|
||||||
|
order_id: order.id,
|
||||||
|
internal_note: "This is an internal note",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const orderEditId = response.data.order_edit.id
|
||||||
|
|
||||||
|
const itemToUpdate = response.data.order_edit.items.find(
|
||||||
|
(item) => item.original_item_id === lineItemId1
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await api.post(
|
||||||
|
`/admin/order-edits/${orderEditId}/items/${itemToUpdate.id}`,
|
||||||
|
{ quantity: 2 },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const orderEditItems = response.data.order_edit.items
|
||||||
|
|
||||||
|
response = await api.post(
|
||||||
|
`/admin/order-edits/${orderEditId}/confirm`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.order_edit).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: orderEditId,
|
||||||
|
created_by: "admin_user",
|
||||||
|
confirmed_by: "admin_user",
|
||||||
|
confirmed_at: expect.any(String),
|
||||||
|
status: "confirmed",
|
||||||
|
discount_total: 0,
|
||||||
|
gift_card_total: 0,
|
||||||
|
gift_card_tax_total: 0,
|
||||||
|
shipping_total: 0,
|
||||||
|
subtotal: 3000,
|
||||||
|
tax_total: 300,
|
||||||
|
total: 3300,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await api.get(`/admin/orders/${order.id}`, adminHeaders)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.order).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: orderEditItems[0].id,
|
||||||
|
original_item_id: orderEditItems[0].original_item_id,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: orderEditItems[1].id,
|
||||||
|
original_item_id: orderEditItems[1].original_item_id,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
shipping_total: 0,
|
||||||
|
discount_total: 0,
|
||||||
|
tax_total: 300,
|
||||||
|
refunded_total: 0,
|
||||||
|
gift_card_total: 0,
|
||||||
|
gift_card_tax_total: 0,
|
||||||
|
subtotal: 3000,
|
||||||
|
total: 3300,
|
||||||
|
paid_total: 2200,
|
||||||
|
refundable_amount: 2200,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("confirms an already confirmed order edit", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const confirmedOrderEdit = await simpleOrderEditFactory(dbConnection, {
|
||||||
|
created_by: "admin_user",
|
||||||
|
confirmed_at: new Date(),
|
||||||
|
confirmed_by: "admin_user",
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/order-edits/${confirmedOrderEdit.id}/confirm`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.order_edit).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: confirmedOrderEdit.id,
|
||||||
|
created_by: "admin_user",
|
||||||
|
confirmed_by: "admin_user",
|
||||||
|
confirmed_at: expect.any(String),
|
||||||
|
status: "confirmed",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("confirms an already canceled order edit", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const canceledOrderEdit = await simpleOrderEditFactory(dbConnection, {
|
||||||
|
created_by: "admin_user",
|
||||||
|
canceled_at: new Date(),
|
||||||
|
canceled_by: "admin_user",
|
||||||
|
})
|
||||||
|
|
||||||
|
const err = await api
|
||||||
|
.post(
|
||||||
|
`/admin/order-edits/${canceledOrderEdit.id}/confirm`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(err.response.status).toEqual(400)
|
||||||
|
expect(err.response.data.message).toEqual(
|
||||||
|
"Cannot confirm an order edit with status canceled"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("confirms an already declined order edit", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const declinedOrderEdit = await simpleOrderEditFactory(dbConnection, {
|
||||||
|
created_by: "admin_user",
|
||||||
|
declined_at: new Date(),
|
||||||
|
declined_by: "admin_user",
|
||||||
|
})
|
||||||
|
|
||||||
|
const err = await api
|
||||||
|
.post(
|
||||||
|
`/admin/order-edits/${declinedOrderEdit.id}/confirm`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(err.response.status).toEqual(400)
|
||||||
|
expect(err.response.data.message).toEqual(
|
||||||
|
"Cannot confirm an order edit with status declined"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("POST /admin/order-edits/:id/items/:item_id", () => {
|
describe("POST /admin/order-edits/:id/items/:item_id", () => {
|
||||||
let product, product2
|
let product, product2
|
||||||
const orderId = IdMap.getId("order-1")
|
const orderId = IdMap.getId("order-1")
|
||||||
|
|||||||
@@ -78,6 +78,14 @@ class AdminOrderEditsResource extends BaseResource {
|
|||||||
return this.client.request("POST", path, undefined, {}, customHeaders)
|
return this.client.request("POST", path, undefined, {}, customHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
confirm(
|
||||||
|
id: string,
|
||||||
|
customHeaders: Record<string, any> = {}
|
||||||
|
): ResponsePromise<AdminOrderEditsRes> {
|
||||||
|
const path = `/admin/order-edits/${id}/confirm`
|
||||||
|
return this.client.request("POST", path, undefined, {}, customHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
updateLineItem(
|
updateLineItem(
|
||||||
orderEditId: string,
|
orderEditId: string,
|
||||||
itemId: string,
|
itemId: string,
|
||||||
|
|||||||
@@ -1700,7 +1700,24 @@ export const adminHandlers = [
|
|||||||
return res(
|
return res(
|
||||||
ctx.status(200),
|
ctx.status(200),
|
||||||
ctx.json({
|
ctx.json({
|
||||||
order_edit: { ...fixtures.get("order_edit"), canceled_at: new Date(), status: 'canceled' },
|
order_edit: {
|
||||||
|
...fixtures.get("order_edit"),
|
||||||
|
canceled_at: new Date(),
|
||||||
|
status: "canceled",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
|
||||||
|
rest.post("/admin/order-edits/:id/confirm", (req, res, ctx) => {
|
||||||
|
return res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({
|
||||||
|
order_edit: {
|
||||||
|
...fixtures.get("order_edit"),
|
||||||
|
confirmed_at: new Date(),
|
||||||
|
status: "confirmed",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -153,20 +153,32 @@ export const useAdminRequestOrderEditConfirmation = (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const useAdminCancelOrderEdit = (
|
export const useAdminCancelOrderEdit = (
|
||||||
id: string,
|
id: string,
|
||||||
options?: UseMutationOptions<
|
options?: UseMutationOptions<Response<AdminOrderEditsRes>, Error>
|
||||||
Response<AdminOrderEditsRes>,
|
|
||||||
Error
|
|
||||||
>
|
|
||||||
) => {
|
) => {
|
||||||
const { client } = useMedusa()
|
const { client } = useMedusa()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return useMutation(
|
return useMutation(
|
||||||
() =>
|
() => client.admin.orderEdits.cancel(id),
|
||||||
client.admin.orderEdits.cancel(id),
|
buildOptions(
|
||||||
|
queryClient,
|
||||||
|
[adminOrderEditsKeys.lists(), adminOrderEditsKeys.detail(id)],
|
||||||
|
options
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAdminConfirmOrderEdit = (
|
||||||
|
id: string,
|
||||||
|
options?: UseMutationOptions<Response<AdminOrderEditsRes>, Error>
|
||||||
|
) => {
|
||||||
|
const { client } = useMedusa()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation(
|
||||||
|
() => client.admin.orderEdits.confirm(id),
|
||||||
buildOptions(
|
buildOptions(
|
||||||
queryClient,
|
queryClient,
|
||||||
[adminOrderEditsKeys.lists(), adminOrderEditsKeys.detail(id)],
|
[adminOrderEditsKeys.lists(), adminOrderEditsKeys.detail(id)],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { renderHook } from "@testing-library/react-hooks"
|
import { renderHook } from "@testing-library/react-hooks"
|
||||||
import {
|
import {
|
||||||
useAdminCancelOrderEdit,
|
useAdminCancelOrderEdit,
|
||||||
|
useAdminConfirmOrderEdit,
|
||||||
useAdminCreateOrderEdit,
|
useAdminCreateOrderEdit,
|
||||||
useAdminDeleteOrderEdit,
|
useAdminDeleteOrderEdit,
|
||||||
useAdminDeleteOrderEditItemChange,
|
useAdminDeleteOrderEditItemChange,
|
||||||
@@ -220,3 +221,29 @@ describe("useAdminCancelOrderEdit hook", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("useAdminConfirmOrderEdit hook", () => {
|
||||||
|
test("confirm an order edit", async () => {
|
||||||
|
const { result, waitFor } = renderHook(
|
||||||
|
() => useAdminConfirmOrderEdit(fixtures.get("order_edit").id),
|
||||||
|
{
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result.current.mutate()
|
||||||
|
|
||||||
|
await waitFor(() => result.current.isSuccess)
|
||||||
|
|
||||||
|
expect(result.current.data.response.status).toEqual(200)
|
||||||
|
expect(result.current.data).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
order_edit: {
|
||||||
|
...fixtures.get("order_edit"),
|
||||||
|
confirmed_at: expect.any(String),
|
||||||
|
status: "confirmed",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -9,14 +9,18 @@ describe("POST /admin/order-edits/:id/cancel", () => {
|
|||||||
let subject
|
let subject
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
subject = await request("POST", `/admin/order-edits/${orderEditId}/cancel`, {
|
subject = await request(
|
||||||
|
"POST",
|
||||||
|
`/admin/order-edits/${orderEditId}/cancel`,
|
||||||
|
{
|
||||||
adminSession: {
|
adminSession: {
|
||||||
jwt: {
|
jwt: {
|
||||||
userId: IdMap.getId("admin_user"),
|
userId: IdMap.getId("admin_user"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
flags: [OrderEditingFeatureFlag],
|
flags: [OrderEditingFeatureFlag],
|
||||||
})
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@@ -25,7 +29,9 @@ describe("POST /admin/order-edits/:id/cancel", () => {
|
|||||||
|
|
||||||
it("calls orderService cancel", () => {
|
it("calls orderService cancel", () => {
|
||||||
expect(orderEditServiceMock.cancel).toHaveBeenCalledTimes(1)
|
expect(orderEditServiceMock.cancel).toHaveBeenCalledTimes(1)
|
||||||
expect(orderEditServiceMock.cancel).toHaveBeenCalledWith(orderEditId, {loggedInUser: IdMap.getId("admin_user")})
|
expect(orderEditServiceMock.cancel).toHaveBeenCalledWith(orderEditId, {
|
||||||
|
loggedInUserId: IdMap.getId("admin_user"),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns 200", () => {
|
it("returns 200", () => {
|
||||||
@@ -33,11 +39,13 @@ describe("POST /admin/order-edits/:id/cancel", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("returns cancel result", () => {
|
it("returns cancel result", () => {
|
||||||
expect(subject.body.order_edit).toEqual(expect.objectContaining({
|
expect(subject.body.order_edit).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
id: orderEditId,
|
id: orderEditId,
|
||||||
canceled_at: expect.any(String),
|
canceled_at: expect.any(String),
|
||||||
status: 'canceled'
|
status: "canceled",
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
import { request } from "../../../../../helpers/test-request"
|
||||||
|
import OrderEditingFeatureFlag from "../../../../../loaders/feature-flags/order-editing"
|
||||||
|
import { orderEditServiceMock } from "../../../../../services/__mocks__/order-edit"
|
||||||
|
|
||||||
|
describe("POST /admin/order-edits/:id/confirm", () => {
|
||||||
|
describe("confirms an order edit", () => {
|
||||||
|
const orderEditId = IdMap.getId("testConfirmOrderEdit")
|
||||||
|
let subject
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
subject = await request(
|
||||||
|
"POST",
|
||||||
|
`/admin/order-edits/${orderEditId}/confirm`,
|
||||||
|
{
|
||||||
|
adminSession: {
|
||||||
|
jwt: {
|
||||||
|
userId: "admin_user",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: [OrderEditingFeatureFlag],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls orderService confirm", () => {
|
||||||
|
expect(orderEditServiceMock.confirm).toHaveBeenCalledTimes(1)
|
||||||
|
expect(orderEditServiceMock.confirm).toHaveBeenCalledWith(orderEditId, {
|
||||||
|
loggedInUserId: "admin_user",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns 200", () => {
|
||||||
|
expect(subject.status).toEqual(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns confirm result", () => {
|
||||||
|
expect(subject.body.order_edit).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: orderEditId,
|
||||||
|
confirmed_at: expect.any(String),
|
||||||
|
confirmed_by: "admin_user",
|
||||||
|
status: "confirmed",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -31,7 +31,7 @@ describe("GET /admin/order-edits/:id", () => {
|
|||||||
expect(orderEditServiceMock.requestConfirmation).toHaveBeenCalledTimes(1)
|
expect(orderEditServiceMock.requestConfirmation).toHaveBeenCalledTimes(1)
|
||||||
expect(orderEditServiceMock.requestConfirmation).toHaveBeenCalledWith(
|
expect(orderEditServiceMock.requestConfirmation).toHaveBeenCalledWith(
|
||||||
orderEditId,
|
orderEditId,
|
||||||
{ loggedInUser: IdMap.getId("admin_user") }
|
{ loggedInUserId: IdMap.getId("admin_user") }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Request, Response } from "express"
|
import { Request, Response } from "express"
|
||||||
import { OrderEditService } from "../../../../services"
|
import { OrderEditService } from "../../../../services"
|
||||||
import { IsOptional, IsString } from "class-validator"
|
|
||||||
import { EntityManager } from "typeorm"
|
import { EntityManager } from "typeorm"
|
||||||
|
import {
|
||||||
|
defaultOrderEditFields,
|
||||||
|
defaultOrderEditRelations,
|
||||||
|
} from "../../../../types/order-edit"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [post] /order-edits/{id}/cancel
|
* @oas [post] /order-edits/{id}/cancel
|
||||||
@@ -64,10 +67,13 @@ export default async (req: Request, res: Response) => {
|
|||||||
await manager.transaction(async (transactionManager) => {
|
await manager.transaction(async (transactionManager) => {
|
||||||
await orderEditService
|
await orderEditService
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.cancel(id, { loggedInUser: userId })
|
.cancel(id, { loggedInUserId: userId })
|
||||||
})
|
})
|
||||||
|
|
||||||
const orderEdit = await orderEditService.retrieve(id)
|
const orderEdit = await orderEditService.retrieve(id, {
|
||||||
|
select: defaultOrderEditFields,
|
||||||
|
relations: defaultOrderEditRelations,
|
||||||
|
})
|
||||||
|
|
||||||
return res.json({ order_edit: orderEdit })
|
return res.json({ order_edit: orderEdit })
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { Request, Response } from "express"
|
||||||
|
import { OrderEditService } from "../../../../services"
|
||||||
|
import { EntityManager } from "typeorm"
|
||||||
|
import {
|
||||||
|
defaultOrderEditFields,
|
||||||
|
defaultOrderEditRelations,
|
||||||
|
} from "../../../../types/order-edit"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @oas [post] /order-edits/{id}/confirm
|
||||||
|
* operationId: "PostOrderEditsOrderEditConfirm"
|
||||||
|
* summary: "Confirms an OrderEdit"
|
||||||
|
* description: "Confirms an OrderEdit."
|
||||||
|
* x-authenticated: true
|
||||||
|
* parameters:
|
||||||
|
* - (path) id=* {string} The ID of the order edit.
|
||||||
|
* x-codeSamples:
|
||||||
|
* - lang: JavaScript
|
||||||
|
* label: JS Client
|
||||||
|
* source: |
|
||||||
|
* import Medusa from "@medusajs/medusa-js"
|
||||||
|
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||||
|
* // must be previously logged in or use api token
|
||||||
|
* medusa.admin.orderEdit.confirm(orderEditId)
|
||||||
|
* .then(({ order_edit }) => {
|
||||||
|
* console.log(order_edit.id)
|
||||||
|
* })
|
||||||
|
* - lang: Shell
|
||||||
|
* label: cURL
|
||||||
|
* source: |
|
||||||
|
* curl --location --request POST 'https://medusa-url.com/admin/order-edits/:id/confirm' \
|
||||||
|
* --header 'Authorization: Bearer {api_token}'
|
||||||
|
* security:
|
||||||
|
* - api_token: []
|
||||||
|
* - cookie_auth: []
|
||||||
|
* tags:
|
||||||
|
* - OrderEdit
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* properties:
|
||||||
|
* order_edit:
|
||||||
|
* $ref: "#/components/schemas/order_edit"
|
||||||
|
* "400":
|
||||||
|
* $ref: "#/components/responses/400_error"
|
||||||
|
* "401":
|
||||||
|
* $ref: "#/components/responses/unauthorized"
|
||||||
|
* "404":
|
||||||
|
* $ref: "#/components/responses/not_found_error"
|
||||||
|
* "500":
|
||||||
|
* $ref: "#/components/responses/500_error"
|
||||||
|
*/
|
||||||
|
export default async (req: Request, res: Response) => {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
const orderEditService = req.scope.resolve(
|
||||||
|
"orderEditService"
|
||||||
|
) as OrderEditService
|
||||||
|
|
||||||
|
const manager = req.scope.resolve("manager") as EntityManager
|
||||||
|
|
||||||
|
const userId = req.user?.id ?? req.user?.userId
|
||||||
|
|
||||||
|
await manager.transaction(async (transactionManager) => {
|
||||||
|
await orderEditService
|
||||||
|
.withTransaction(transactionManager)
|
||||||
|
.confirm(id, { loggedInUserId: userId })
|
||||||
|
})
|
||||||
|
|
||||||
|
let orderEdit = await orderEditService.retrieve(id, {
|
||||||
|
select: defaultOrderEditFields,
|
||||||
|
relations: defaultOrderEditRelations,
|
||||||
|
})
|
||||||
|
orderEdit = await orderEditService.decorateTotals(orderEdit)
|
||||||
|
|
||||||
|
return res.json({ order_edit: orderEdit })
|
||||||
|
}
|
||||||
@@ -59,6 +59,11 @@ export default (app) => {
|
|||||||
middlewares.wrap(require("./add-line-item").default)
|
middlewares.wrap(require("./add-line-item").default)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
"/:id/confirm",
|
||||||
|
middlewares.wrap(require("./confirm-order-edit").default)
|
||||||
|
)
|
||||||
|
|
||||||
route.delete("/:id", middlewares.wrap(require("./delete-order-edit").default))
|
route.delete("/:id", middlewares.wrap(require("./delete-order-edit").default))
|
||||||
|
|
||||||
route.delete(
|
route.delete(
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default async (req, res) => {
|
|||||||
await manager.transaction(async (transactionManager) => {
|
await manager.transaction(async (transactionManager) => {
|
||||||
await orderEditService
|
await orderEditService
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.requestConfirmation(id, { loggedInUser })
|
.requestConfirmation(id, { loggedInUserId: loggedInUser })
|
||||||
})
|
})
|
||||||
|
|
||||||
const orderEdit = await orderEditService.retrieve(id, {
|
const orderEdit = await orderEditService.retrieve(id, {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ describe("GET /store/order-edits/:id", () => {
|
|||||||
expect(orderEditServiceMock.decline).toHaveBeenCalledTimes(1)
|
expect(orderEditServiceMock.decline).toHaveBeenCalledTimes(1)
|
||||||
expect(orderEditServiceMock.decline).toHaveBeenCalledWith(orderEditId, {
|
expect(orderEditServiceMock.decline).toHaveBeenCalledWith(orderEditId, {
|
||||||
declinedReason: "test",
|
declinedReason: "test",
|
||||||
loggedInUser: undefined,
|
loggedInUserId: undefined,
|
||||||
})
|
})
|
||||||
expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1)
|
expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default async (req: Request, res: Response) => {
|
|||||||
await manager.transaction(async (manager) => {
|
await manager.transaction(async (manager) => {
|
||||||
await orderEditService.withTransaction(manager).decline(id, {
|
await orderEditService.withTransaction(manager).decline(id, {
|
||||||
declinedReason: validatedBody.declined_reason,
|
declinedReason: validatedBody.declined_reason,
|
||||||
loggedInUser: userId,
|
loggedInUserId: userId,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export class LineItem extends BaseEntity {
|
|||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
order_id: string
|
order_id: string | null
|
||||||
|
|
||||||
@ManyToOne(() => Order, (order) => order.items)
|
@ManyToOne(() => Order, (order) => order.items)
|
||||||
@JoinColumn({ name: "order_id" })
|
@JoinColumn({ name: "order_id" })
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { IdMap } from "medusa-test-utils"
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
|
||||||
export const orderEdits = {
|
export const orderEdit = {
|
||||||
testCreatedOrder: {
|
|
||||||
id: IdMap.getId("testCreatedOrder"),
|
id: IdMap.getId("testCreatedOrder"),
|
||||||
order_id: "empty-id",
|
order_id: "empty-id",
|
||||||
internal_note: "internal note",
|
internal_note: "internal note",
|
||||||
@@ -16,7 +15,6 @@ export const orderEdits = {
|
|||||||
created_by: "admin_user",
|
created_by: "admin_user",
|
||||||
confirmed_at: null,
|
confirmed_at: null,
|
||||||
confirmed_by: null,
|
confirmed_by: null,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const computeLineItems = (orderEdit) => ({
|
const computeLineItems = (orderEdit) => ({
|
||||||
@@ -48,11 +46,20 @@ export const orderEditServiceMock = {
|
|||||||
},
|
},
|
||||||
retrieve: jest.fn().mockImplementation((orderId) => {
|
retrieve: jest.fn().mockImplementation((orderId) => {
|
||||||
if (orderId === IdMap.getId("testCreatedOrder")) {
|
if (orderId === IdMap.getId("testCreatedOrder")) {
|
||||||
return Promise.resolve(orderEdits.testCreatedOrder)
|
return Promise.resolve(orderEdit)
|
||||||
|
}
|
||||||
|
if (orderId === IdMap.getId("testConfirmOrderEdit")) {
|
||||||
|
return Promise.resolve({
|
||||||
|
...orderEdit,
|
||||||
|
id: IdMap.getId("testConfirmOrderEdit"),
|
||||||
|
confirmed_at: new Date(),
|
||||||
|
confirmed_by: "admin_user",
|
||||||
|
status: "confirmed",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (orderId === IdMap.getId("testDeclineOrderEdit")) {
|
if (orderId === IdMap.getId("testDeclineOrderEdit")) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
...orderEdits.testCreatedOrder,
|
...orderEdit,
|
||||||
id: IdMap.getId("testDeclineOrderEdit"),
|
id: IdMap.getId("testDeclineOrderEdit"),
|
||||||
declined_reason: "Wrong size",
|
declined_reason: "Wrong size",
|
||||||
declined_at: new Date(),
|
declined_at: new Date(),
|
||||||
@@ -60,7 +67,7 @@ export const orderEditServiceMock = {
|
|||||||
}
|
}
|
||||||
if (orderId === IdMap.getId("testCancelOrderEdit")) {
|
if (orderId === IdMap.getId("testCancelOrderEdit")) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
...orderEdits.testCreatedOrder,
|
...orderEdit,
|
||||||
id: orderId,
|
id: orderId,
|
||||||
canceled_at: new Date(),
|
canceled_at: new Date(),
|
||||||
status: "canceled",
|
status: "canceled",
|
||||||
@@ -68,7 +75,7 @@ export const orderEditServiceMock = {
|
|||||||
}
|
}
|
||||||
if (orderId === IdMap.getId("testRequestOrder")) {
|
if (orderId === IdMap.getId("testRequestOrder")) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
...orderEdits.testCreatedOrder,
|
...orderEdit,
|
||||||
id: IdMap.getId("testRequestOrder"),
|
id: IdMap.getId("testRequestOrder"),
|
||||||
requested_by: IdMap.getId("admin_user"),
|
requested_by: IdMap.getId("admin_user"),
|
||||||
requested_at: new Date(),
|
requested_at: new Date(),
|
||||||
@@ -111,7 +118,7 @@ export const orderEditServiceMock = {
|
|||||||
}),
|
}),
|
||||||
requestConfirmation: jest.fn().mockImplementation((orderEditId, userId) => {
|
requestConfirmation: jest.fn().mockImplementation((orderEditId, userId) => {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
...orderEdits.testCreatedOrder,
|
...orderEdit,
|
||||||
id: orderEditId,
|
id: orderEditId,
|
||||||
requested_at: new Date(),
|
requested_at: new Date(),
|
||||||
requested_by: userId,
|
requested_by: userId,
|
||||||
@@ -120,6 +127,9 @@ export const orderEditServiceMock = {
|
|||||||
cancel: jest.fn().mockImplementation(() => {
|
cancel: jest.fn().mockImplementation(() => {
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}),
|
}),
|
||||||
|
confirm: jest.fn().mockImplementation(() => {
|
||||||
|
return Promise.resolve({})
|
||||||
|
}),
|
||||||
updateLineItem: jest.fn().mockImplementation((_) => {
|
updateLineItem: jest.fn().mockImplementation((_) => {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import LineItemService from "../line-item"
|
|||||||
import { PricingServiceMock } from "../__mocks__/pricing"
|
import { PricingServiceMock } from "../__mocks__/pricing"
|
||||||
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
|
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
|
||||||
import { RegionServiceMock } from "../__mocks__/region"
|
import { RegionServiceMock } from "../__mocks__/region"
|
||||||
|
|
||||||
;[true, false].forEach((isTaxInclusiveEnabled) => {
|
;[true, false].forEach((isTaxInclusiveEnabled) => {
|
||||||
describe(`tax inclusive flag set to: ${isTaxInclusiveEnabled}`, () => {
|
describe(`tax inclusive flag set to: ${isTaxInclusiveEnabled}`, () => {
|
||||||
describe("LineItemService", () => {
|
describe("LineItemService", () => {
|
||||||
@@ -167,8 +168,9 @@ import { RegionServiceMock } from "../__mocks__/region"
|
|||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
const lineItemRepository = MockRepository({
|
const lineItemRepository = MockRepository({
|
||||||
findOne: () =>
|
find: () =>
|
||||||
Promise.resolve({
|
Promise.resolve([
|
||||||
|
{
|
||||||
id: IdMap.getId("test-line-item"),
|
id: IdMap.getId("test-line-item"),
|
||||||
variant_id: IdMap.getId("test-variant"),
|
variant_id: IdMap.getId("test-variant"),
|
||||||
variant: {
|
variant: {
|
||||||
@@ -181,7 +183,8 @@ import { RegionServiceMock } from "../__mocks__/region"
|
|||||||
thumbnail: "",
|
thumbnail: "",
|
||||||
unit_price: 50,
|
unit_price: 50,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
}),
|
},
|
||||||
|
]),
|
||||||
})
|
})
|
||||||
|
|
||||||
const lineItemService = new LineItemService({
|
const lineItemService = new LineItemService({
|
||||||
@@ -200,7 +203,8 @@ import { RegionServiceMock } from "../__mocks__/region"
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(lineItemRepository.save).toHaveBeenCalledTimes(1)
|
expect(lineItemRepository.save).toHaveBeenCalledTimes(1)
|
||||||
expect(lineItemRepository.save).toHaveBeenCalledWith({
|
expect(lineItemRepository.save).toHaveBeenCalledWith([
|
||||||
|
{
|
||||||
id: IdMap.getId("test-line-item"),
|
id: IdMap.getId("test-line-item"),
|
||||||
variant_id: IdMap.getId("test-variant"),
|
variant_id: IdMap.getId("test-variant"),
|
||||||
variant: {
|
variant: {
|
||||||
@@ -214,7 +218,8 @@ import { RegionServiceMock } from "../__mocks__/region"
|
|||||||
unit_price: 50,
|
unit_price: 50,
|
||||||
quantity: 2,
|
quantity: 2,
|
||||||
has_shipping: true,
|
has_shipping: true,
|
||||||
})
|
},
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("successfully updates a line item with metadata", async () => {
|
it("successfully updates a line item with metadata", async () => {
|
||||||
@@ -225,7 +230,8 @@ import { RegionServiceMock } from "../__mocks__/region"
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(lineItemRepository.save).toHaveBeenCalledTimes(1)
|
expect(lineItemRepository.save).toHaveBeenCalledTimes(1)
|
||||||
expect(lineItemRepository.save).toHaveBeenCalledWith({
|
expect(lineItemRepository.save).toHaveBeenCalledWith([
|
||||||
|
{
|
||||||
id: IdMap.getId("test-line-item"),
|
id: IdMap.getId("test-line-item"),
|
||||||
variant_id: IdMap.getId("test-variant"),
|
variant_id: IdMap.getId("test-variant"),
|
||||||
variant: {
|
variant: {
|
||||||
@@ -241,7 +247,8 @@ import { RegionServiceMock } from "../__mocks__/region"
|
|||||||
metadata: {
|
metadata: {
|
||||||
testKey: "testValue",
|
testKey: "testValue",
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe("delete", () => {
|
describe("delete", () => {
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ describe("OrderEditService", () => {
|
|||||||
IdMap.getId("requested-order-edit"),
|
IdMap.getId("requested-order-edit"),
|
||||||
{
|
{
|
||||||
declinedReason: "I requested a different color for the new product",
|
declinedReason: "I requested a different color for the new product",
|
||||||
loggedInUser: "admin_user",
|
loggedInUserId: "admin_user",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -276,7 +276,7 @@ describe("OrderEditService", () => {
|
|||||||
await expect(
|
await expect(
|
||||||
orderEditService.decline(IdMap.getId("confirmed-order-edit"), {
|
orderEditService.decline(IdMap.getId("confirmed-order-edit"), {
|
||||||
declinedReason: "I requested a different color for the new product",
|
declinedReason: "I requested a different color for the new product",
|
||||||
loggedInUser: "admin_user",
|
loggedInUserId: "admin_user",
|
||||||
})
|
})
|
||||||
).rejects.toThrowError(
|
).rejects.toThrowError(
|
||||||
"Cannot decline an order edit with status confirmed."
|
"Cannot decline an order edit with status confirmed."
|
||||||
@@ -288,7 +288,7 @@ describe("OrderEditService", () => {
|
|||||||
IdMap.getId("declined-order-edit"),
|
IdMap.getId("declined-order-edit"),
|
||||||
{
|
{
|
||||||
declinedReason: "I requested a different color for the new product",
|
declinedReason: "I requested a different color for the new product",
|
||||||
loggedInUser: "admin_user",
|
loggedInUserId: "admin_user",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -312,7 +312,7 @@ describe("OrderEditService", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
result = await orderEditService.requestConfirmation(orderEditId, {
|
result = await orderEditService.requestConfirmation(orderEditId, {
|
||||||
loggedInUser: userId,
|
loggedInUserId: userId,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -344,7 +344,7 @@ describe("OrderEditService", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
result = await orderEditService.requestConfirmation(orderEditId, {
|
result = await orderEditService.requestConfirmation(orderEditId, {
|
||||||
loggedInUser: userId,
|
loggedInUserId: userId,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -358,7 +358,7 @@ describe("OrderEditService", () => {
|
|||||||
const id = IdMap.getId("order-edit-with-changes")
|
const id = IdMap.getId("order-edit-with-changes")
|
||||||
const userId = IdMap.getId("user-id")
|
const userId = IdMap.getId("user-id")
|
||||||
|
|
||||||
await orderEditService.cancel(id, { loggedInUser: userId })
|
await orderEditService.cancel(id, { loggedInUserId: userId })
|
||||||
|
|
||||||
expect(orderEditRepository.save).toHaveBeenCalledWith({
|
expect(orderEditRepository.save).toHaveBeenCalledWith({
|
||||||
...orderEditWithChanges,
|
...orderEditWithChanges,
|
||||||
@@ -402,6 +402,39 @@ describe("OrderEditService", () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("confirm", () => {
|
||||||
|
it("confirms an order edit", async () => {
|
||||||
|
const id = IdMap.getId("order-edit-with-changes")
|
||||||
|
const userId = IdMap.getId("user-id")
|
||||||
|
|
||||||
|
await orderEditService.confirm(id, { loggedInUserId: userId })
|
||||||
|
|
||||||
|
expect(orderEditRepository.save).toHaveBeenCalledWith({
|
||||||
|
...orderEditWithChanges,
|
||||||
|
confirmed_by: userId,
|
||||||
|
confirmed_at: expect.any(Date),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1)
|
||||||
|
expect(EventBusServiceMock.emit).toHaveBeenCalledWith(
|
||||||
|
OrderEditService.Events.CONFIRMED,
|
||||||
|
{ id }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Returns early in case of an already confirmed order edit", async () => {
|
||||||
|
const id = IdMap.getId("confirmed-order-edit")
|
||||||
|
const userId = IdMap.getId("user-id")
|
||||||
|
|
||||||
|
const result = await orderEditService.confirm(id, userId)
|
||||||
|
|
||||||
|
expect(result).toEqual(expect.objectContaining({ status: "confirmed" }))
|
||||||
|
|
||||||
|
expect(orderEditRepository.save).toHaveBeenCalledTimes(0)
|
||||||
|
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should add a line item to an order edit", async () => {
|
it("should add a line item to an order edit", async () => {
|
||||||
|
|||||||
@@ -1838,6 +1838,9 @@ class CartService extends TransactionBaseService {
|
|||||||
relations: ["countries"],
|
relations: ["countries"],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const lineItemServiceTx =
|
||||||
|
this.lineItemService_.withTransaction(transactionManager)
|
||||||
|
|
||||||
cart.items = (
|
cart.items = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
cart.items.map(async (item) => {
|
cart.items.map(async (item) => {
|
||||||
@@ -1856,21 +1859,19 @@ class CartService extends TransactionBaseService {
|
|||||||
availablePrice !== undefined &&
|
availablePrice !== undefined &&
|
||||||
availablePrice.calculatedPrice !== null
|
availablePrice.calculatedPrice !== null
|
||||||
) {
|
) {
|
||||||
return this.lineItemService_
|
return lineItemServiceTx.update(item.id, {
|
||||||
.withTransaction(transactionManager)
|
|
||||||
.update(item.id, {
|
|
||||||
has_shipping: false,
|
has_shipping: false,
|
||||||
unit_price: availablePrice.calculatedPrice,
|
unit_price: availablePrice.calculatedPrice,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await this.lineItemService_
|
await lineItemServiceTx.delete(item.id)
|
||||||
.withTransaction(transactionManager)
|
|
||||||
.delete(item.id)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
).filter((item): item is LineItem => !!item)
|
)
|
||||||
|
.flat()
|
||||||
|
.filter((item): item is LineItem => !!item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { DeepPartial } from "typeorm/common/DeepPartial"
|
|||||||
import { CartRepository } from "../repositories/cart"
|
import { CartRepository } from "../repositories/cart"
|
||||||
import { LineItemRepository } from "../repositories/line-item"
|
import { LineItemRepository } from "../repositories/line-item"
|
||||||
import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line"
|
import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line"
|
||||||
import { Cart, LineItemTaxLine, LineItem, LineItemAdjustment } from "../models"
|
import { Cart, LineItem, LineItemAdjustment, LineItemTaxLine } from "../models"
|
||||||
import { FindConfig, Selector } from "../types/common"
|
import { FindConfig, Selector } from "../types/common"
|
||||||
import { FlagRouter } from "../utils/flag-router"
|
import { FlagRouter } from "../utils/flag-router"
|
||||||
import LineItemAdjustmentService from "./line-item-adjustment"
|
import LineItemAdjustmentService from "./line-item-adjustment"
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
ProductVariantService,
|
ProductVariantService,
|
||||||
RegionService,
|
RegionService,
|
||||||
} from "./index"
|
} from "./index"
|
||||||
|
import { setMetadata } from "../utils"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
@@ -310,11 +311,14 @@ class LineItemService extends BaseService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates a line item
|
* Updates a line item
|
||||||
* @param {string} id - the id of the line item to update
|
* @param idOrSelector - the id or selector of the line item(s) to update
|
||||||
* @param {Partial<LineItem>} data - the properties to update on line item
|
* @param data - the properties to update the line item(s)
|
||||||
* @return {Promise<LineItem>} the update line item
|
* @return the updated line item(s)
|
||||||
*/
|
*/
|
||||||
async update(id: string, data: Partial<LineItem>): Promise<LineItem> {
|
async update(
|
||||||
|
idOrSelector: string | Selector<LineItem>,
|
||||||
|
data: Partial<LineItem>
|
||||||
|
): Promise<LineItem[]> {
|
||||||
const { metadata, ...rest } = data
|
const { metadata, ...rest } = data
|
||||||
|
|
||||||
return await this.atomicPhase_(
|
return await this.atomicPhase_(
|
||||||
@@ -323,17 +327,34 @@ class LineItemService extends BaseService {
|
|||||||
this.lineItemRepository_
|
this.lineItemRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
const lineItem = await this.retrieve(id).then((lineItem) => {
|
const selector =
|
||||||
const lineItemMetadata = metadata
|
typeof idOrSelector === "string" ? { id: idOrSelector } : idOrSelector
|
||||||
? this.setMetadata_(lineItem, metadata)
|
|
||||||
: lineItem.metadata
|
|
||||||
|
|
||||||
return Object.assign(lineItem, {
|
let lineItems = await this.list(selector)
|
||||||
|
|
||||||
|
if (!lineItems.length) {
|
||||||
|
const selectorConstraints = Object.entries(selector)
|
||||||
|
.map(([key, value]) => `${key}: ${value}`)
|
||||||
|
.join(", ")
|
||||||
|
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`Line item with ${selectorConstraints} was not found`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
lineItems = lineItems.map((item) => {
|
||||||
|
const lineItemMetadata = metadata
|
||||||
|
? setMetadata(item, metadata)
|
||||||
|
: item.metadata
|
||||||
|
|
||||||
|
return Object.assign(item, {
|
||||||
...rest,
|
...rest,
|
||||||
metadata: lineItemMetadata,
|
metadata: lineItemMetadata,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return await lineItemRepository.save(lineItem)
|
|
||||||
|
return await lineItemRepository.save(lineItems)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { EntityManager, IsNull } from "typeorm"
|
import { DeepPartial, EntityManager, IsNull } from "typeorm"
|
||||||
import { MedusaError } from "medusa-core-utils"
|
import { MedusaError } from "medusa-core-utils"
|
||||||
|
|
||||||
import { FindConfig } from "../types/common"
|
import { FindConfig } from "../types/common"
|
||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
AddOrderEditLineItemInput,
|
AddOrderEditLineItemInput,
|
||||||
CreateOrderEditInput,
|
CreateOrderEditInput,
|
||||||
UpdateOrderEditInput,
|
|
||||||
} from "../types/order-edit"
|
} from "../types/order-edit"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
@@ -47,6 +46,7 @@ export default class OrderEditService extends TransactionBaseService {
|
|||||||
DECLINED: "order-edit.declined",
|
DECLINED: "order-edit.declined",
|
||||||
REQUESTED: "order-edit.requested",
|
REQUESTED: "order-edit.requested",
|
||||||
CANCELED: "order-edit.canceled",
|
CANCELED: "order-edit.canceled",
|
||||||
|
CONFIRMED: "order-edit.confirmed",
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly manager_: EntityManager
|
protected readonly manager_: EntityManager
|
||||||
@@ -109,27 +109,6 @@ export default class OrderEditService extends TransactionBaseService {
|
|||||||
return orderEdit
|
return orderEdit
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async retrieveActive(
|
|
||||||
orderId: string,
|
|
||||||
config: FindConfig<OrderEdit> = {}
|
|
||||||
): Promise<OrderEdit | undefined> {
|
|
||||||
const manager = this.transactionManager_ ?? this.manager_
|
|
||||||
const orderEditRepository = manager.getCustomRepository(
|
|
||||||
this.orderEditRepository_
|
|
||||||
)
|
|
||||||
|
|
||||||
const query = buildQuery(
|
|
||||||
{
|
|
||||||
order_id: orderId,
|
|
||||||
confirmed_at: IsNull(),
|
|
||||||
canceled_at: IsNull(),
|
|
||||||
declined_at: IsNull(),
|
|
||||||
},
|
|
||||||
config
|
|
||||||
)
|
|
||||||
return await orderEditRepository.findOne(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute and return the different totals from the order edit id
|
* Compute and return the different totals from the order edit id
|
||||||
* @param orderEditId
|
* @param orderEditId
|
||||||
@@ -235,7 +214,7 @@ export default class OrderEditService extends TransactionBaseService {
|
|||||||
|
|
||||||
async update(
|
async update(
|
||||||
orderEditId: string,
|
orderEditId: string,
|
||||||
data: UpdateOrderEditInput
|
data: DeepPartial<OrderEdit>
|
||||||
): Promise<OrderEdit> {
|
): Promise<OrderEdit> {
|
||||||
return await this.atomicPhase_(async (manager) => {
|
return await this.atomicPhase_(async (manager) => {
|
||||||
const orderEditRepo = manager.getCustomRepository(
|
const orderEditRepo = manager.getCustomRepository(
|
||||||
@@ -290,7 +269,7 @@ export default class OrderEditService extends TransactionBaseService {
|
|||||||
orderEditId: string,
|
orderEditId: string,
|
||||||
context: {
|
context: {
|
||||||
declinedReason?: string
|
declinedReason?: string
|
||||||
loggedInUser?: string
|
loggedInUserId?: string
|
||||||
}
|
}
|
||||||
): Promise<OrderEdit> {
|
): Promise<OrderEdit> {
|
||||||
return await this.atomicPhase_(async (manager) => {
|
return await this.atomicPhase_(async (manager) => {
|
||||||
@@ -298,7 +277,7 @@ export default class OrderEditService extends TransactionBaseService {
|
|||||||
this.orderEditRepository_
|
this.orderEditRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
const { loggedInUser, declinedReason } = context
|
const { loggedInUserId, declinedReason } = context
|
||||||
|
|
||||||
const orderEdit = await this.retrieve(orderEditId)
|
const orderEdit = await this.retrieve(orderEditId)
|
||||||
|
|
||||||
@@ -314,7 +293,7 @@ export default class OrderEditService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
orderEdit.declined_at = new Date()
|
orderEdit.declined_at = new Date()
|
||||||
orderEdit.declined_by = loggedInUser
|
orderEdit.declined_by = loggedInUserId
|
||||||
orderEdit.declined_reason = declinedReason
|
orderEdit.declined_reason = declinedReason
|
||||||
|
|
||||||
const result = await orderEditRepo.save(orderEdit)
|
const result = await orderEditRepo.save(orderEdit)
|
||||||
@@ -573,7 +552,7 @@ export default class OrderEditService extends TransactionBaseService {
|
|||||||
async requestConfirmation(
|
async requestConfirmation(
|
||||||
orderEditId: string,
|
orderEditId: string,
|
||||||
context: {
|
context: {
|
||||||
loggedInUser?: string
|
loggedInUserId?: string
|
||||||
} = {}
|
} = {}
|
||||||
): Promise<OrderEdit> {
|
): Promise<OrderEdit> {
|
||||||
return await this.atomicPhase_(async (manager) => {
|
return await this.atomicPhase_(async (manager) => {
|
||||||
@@ -598,7 +577,7 @@ export default class OrderEditService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
orderEdit.requested_at = new Date()
|
orderEdit.requested_at = new Date()
|
||||||
orderEdit.requested_by = context.loggedInUser
|
orderEdit.requested_by = context.loggedInUserId
|
||||||
|
|
||||||
orderEdit = await orderEditRepo.save(orderEdit)
|
orderEdit = await orderEditRepo.save(orderEdit)
|
||||||
|
|
||||||
@@ -610,6 +589,118 @@ export default class OrderEditService extends TransactionBaseService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cancel(
|
||||||
|
orderEditId: string,
|
||||||
|
context: { loggedInUserId?: string } = {}
|
||||||
|
): Promise<OrderEdit> {
|
||||||
|
return await this.atomicPhase_(async (manager) => {
|
||||||
|
const orderEditRepository = manager.getCustomRepository(
|
||||||
|
this.orderEditRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
const orderEdit = await this.retrieve(orderEditId)
|
||||||
|
|
||||||
|
if (orderEdit.status === OrderEditStatus.CANCELED) {
|
||||||
|
return orderEdit
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
[OrderEditStatus.CONFIRMED, OrderEditStatus.DECLINED].includes(
|
||||||
|
orderEdit.status
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
`Cannot cancel order edit with status ${orderEdit.status}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
orderEdit.canceled_at = new Date()
|
||||||
|
orderEdit.canceled_by = context.loggedInUserId
|
||||||
|
|
||||||
|
const saved = await orderEditRepository.save(orderEdit)
|
||||||
|
|
||||||
|
await this.eventBusService_
|
||||||
|
.withTransaction(manager)
|
||||||
|
.emit(OrderEditService.Events.CANCELED, { id: orderEditId })
|
||||||
|
|
||||||
|
return saved
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirm(
|
||||||
|
orderEditId: string,
|
||||||
|
context: { loggedInUserId?: string } = {}
|
||||||
|
): Promise<OrderEdit> {
|
||||||
|
return await this.atomicPhase_(async (manager) => {
|
||||||
|
const orderEditRepository = manager.getCustomRepository(
|
||||||
|
this.orderEditRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
let orderEdit = await this.retrieve(orderEditId)
|
||||||
|
|
||||||
|
if (
|
||||||
|
[OrderEditStatus.CANCELED, OrderEditStatus.DECLINED].includes(
|
||||||
|
orderEdit.status
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
`Cannot confirm an order edit with status ${orderEdit.status}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orderEdit.status === OrderEditStatus.CONFIRMED) {
|
||||||
|
return orderEdit
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineItemServiceTx = this.lineItemService_.withTransaction(manager)
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
lineItemServiceTx.update(
|
||||||
|
{ order_id: orderEdit.order_id },
|
||||||
|
{ order_id: null }
|
||||||
|
),
|
||||||
|
lineItemServiceTx.update(
|
||||||
|
{ order_edit_id: orderEditId },
|
||||||
|
{ order_id: orderEdit.order_id }
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
orderEdit.confirmed_at = new Date()
|
||||||
|
orderEdit.confirmed_by = context.loggedInUserId
|
||||||
|
|
||||||
|
orderEdit = await orderEditRepository.save(orderEdit)
|
||||||
|
|
||||||
|
await this.eventBusService_
|
||||||
|
.withTransaction(manager)
|
||||||
|
.emit(OrderEditService.Events.CONFIRMED, { id: orderEditId })
|
||||||
|
|
||||||
|
return orderEdit
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async retrieveActive(
|
||||||
|
orderId: string,
|
||||||
|
config: FindConfig<OrderEdit> = {}
|
||||||
|
): Promise<OrderEdit | undefined> {
|
||||||
|
const manager = this.transactionManager_ ?? this.manager_
|
||||||
|
const orderEditRepository = manager.getCustomRepository(
|
||||||
|
this.orderEditRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
const query = buildQuery(
|
||||||
|
{
|
||||||
|
order_id: orderId,
|
||||||
|
confirmed_at: IsNull(),
|
||||||
|
canceled_at: IsNull(),
|
||||||
|
declined_at: IsNull(),
|
||||||
|
},
|
||||||
|
config
|
||||||
|
)
|
||||||
|
return await orderEditRepository.findOne(query)
|
||||||
|
}
|
||||||
|
|
||||||
protected async deleteClonedItems(orderEditId: string): Promise<void> {
|
protected async deleteClonedItems(orderEditId: string): Promise<void> {
|
||||||
const manager = this.transactionManager_ ?? this.manager_
|
const manager = this.transactionManager_ ?? this.manager_
|
||||||
const lineItemServiceTx = this.lineItemService_.withTransaction(manager)
|
const lineItemServiceTx = this.lineItemService_.withTransaction(manager)
|
||||||
@@ -647,45 +738,6 @@ export default class OrderEditService extends TransactionBaseService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancel(
|
|
||||||
orderEditId: string,
|
|
||||||
context: { loggedInUser?: string } = {}
|
|
||||||
): Promise<OrderEdit> {
|
|
||||||
return await this.atomicPhase_(async (manager) => {
|
|
||||||
const orderEditRepository = manager.getCustomRepository(
|
|
||||||
this.orderEditRepository_
|
|
||||||
)
|
|
||||||
|
|
||||||
const orderEdit = await this.retrieve(orderEditId)
|
|
||||||
|
|
||||||
if (orderEdit.status === OrderEditStatus.CANCELED) {
|
|
||||||
return orderEdit
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
[OrderEditStatus.CONFIRMED, OrderEditStatus.DECLINED].includes(
|
|
||||||
orderEdit.status
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.NOT_ALLOWED,
|
|
||||||
`Cannot cancel order edit with status ${orderEdit.status}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
orderEdit.canceled_at = new Date()
|
|
||||||
orderEdit.canceled_by = context.loggedInUser
|
|
||||||
|
|
||||||
const saved = await orderEditRepository.save(orderEdit)
|
|
||||||
|
|
||||||
await this.eventBusService_
|
|
||||||
.withTransaction(manager)
|
|
||||||
.emit(OrderEditService.Events.CANCELED, { id: orderEditId })
|
|
||||||
|
|
||||||
return saved
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private static isOrderEditActive(orderEdit: OrderEdit): boolean {
|
private static isOrderEditActive(orderEdit: OrderEdit): boolean {
|
||||||
return !(
|
return !(
|
||||||
orderEdit.status === OrderEditStatus.CONFIRMED ||
|
orderEdit.status === OrderEditStatus.CONFIRMED ||
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { OrderEdit, OrderEditItemChangeType } from "../models"
|
import { OrderEdit, OrderEditItemChangeType } from "../models"
|
||||||
|
|
||||||
export type UpdateOrderEditInput = {
|
|
||||||
internal_note?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreateOrderEditInput = {
|
export type CreateOrderEditInput = {
|
||||||
order_id: string
|
order_id: string
|
||||||
internal_note?: string
|
internal_note?: string
|
||||||
|
|||||||
Reference in new issue
Block a user