feat(core-flows,js-sdk,medusa,types): draft order delete (#12172)
This commit is contained in:
@@ -185,6 +185,24 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
})
|
||||
|
||||
describe("DELETE /draft-orders/:id", () => {
|
||||
it("should delete a draft order", async () => {
|
||||
const response = await api.delete(
|
||||
`/admin/draft-orders/${testDraftOrder.id}`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.data).toEqual(
|
||||
expect.objectContaining({
|
||||
id: testDraftOrder.id,
|
||||
object: "draft-order",
|
||||
deleted: true,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("POST /draft-orders/:id/convert-to-order", () => {
|
||||
it("should convert a draft order to an order", async () => {
|
||||
const response = await api.post(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||
import { IOrderModuleService } from "@medusajs/types"
|
||||
import { IOrderModuleService, OrderDTO } from "@medusajs/types"
|
||||
import { createOrderChangeWorkflow } from "@medusajs/core-flows"
|
||||
import { Modules } from "@medusajs/utils"
|
||||
import {
|
||||
adminHeaders,
|
||||
@@ -500,5 +501,252 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should delete an order and related entities", async () => {
|
||||
const toDeleteOrder = await orderModule.createOrders({
|
||||
region_id: "test_region_id",
|
||||
email: "foo@bar.com",
|
||||
metadata: {
|
||||
foo: "bar",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
title: "Custom Item 1",
|
||||
quantity: 1,
|
||||
unit_price: 20,
|
||||
adjustments: [
|
||||
{
|
||||
code: "VIP_25 ETH",
|
||||
amount: "0.000000000000000005",
|
||||
description: "VIP discount",
|
||||
promotion_id: "prom_123",
|
||||
provider_id: "coupon_kings",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
sales_channel_id: "test",
|
||||
shipping_address: {
|
||||
first_name: "Shipping 1",
|
||||
last_name: "Test 1",
|
||||
address_1: "Test 1",
|
||||
city: "Test 1",
|
||||
country_code: "US",
|
||||
postal_code: "12345",
|
||||
phone: "12345",
|
||||
},
|
||||
billing_address: {
|
||||
first_name: "Billing 1",
|
||||
last_name: "Test 1",
|
||||
address_1: "Test 1",
|
||||
city: "Test 1",
|
||||
country_code: "US",
|
||||
postal_code: "12345",
|
||||
},
|
||||
shipping_methods: [
|
||||
{
|
||||
name: "Test shipping method",
|
||||
amount: 10,
|
||||
data: {},
|
||||
tax_lines: [
|
||||
{
|
||||
description: "shipping Tax 1",
|
||||
tax_rate_id: "tax_usa_shipping",
|
||||
code: "code",
|
||||
rate: 10,
|
||||
},
|
||||
],
|
||||
adjustments: [
|
||||
{
|
||||
code: "VIP_10",
|
||||
amount: 1,
|
||||
description: "VIP discount",
|
||||
promotion_id: "prom_123",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
currency_code: "usd",
|
||||
customer_id: "joe",
|
||||
})
|
||||
|
||||
const persistedOrder = (await orderModule.createOrders({
|
||||
region_id: "test_region_id",
|
||||
email: "foo@bar.com",
|
||||
metadata: {
|
||||
foo: "bar",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
title: "Custom Item 2",
|
||||
quantity: 1,
|
||||
unit_price: 50,
|
||||
adjustments: [
|
||||
{
|
||||
code: "VIP_25 ETH",
|
||||
amount: "0.000000000000000005",
|
||||
description: "VIP discount",
|
||||
promotion_id: "prom_123",
|
||||
provider_id: "coupon_kings",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
sales_channel_id: "test",
|
||||
shipping_address: {
|
||||
first_name: "Shipping 2",
|
||||
last_name: "Test 2",
|
||||
address_1: "Test 2",
|
||||
city: "Test 2",
|
||||
country_code: "US",
|
||||
postal_code: "12345",
|
||||
phone: "12345",
|
||||
},
|
||||
billing_address: {
|
||||
first_name: "Billing 2",
|
||||
last_name: "Test 2",
|
||||
address_1: "Test 2",
|
||||
city: "Test 2",
|
||||
country_code: "US",
|
||||
postal_code: "12345",
|
||||
},
|
||||
shipping_methods: [
|
||||
{
|
||||
name: "Test shipping method",
|
||||
amount: 10,
|
||||
data: {},
|
||||
tax_lines: [
|
||||
{
|
||||
description: "shipping Tax 2",
|
||||
tax_rate_id: "tax_usa_shipping",
|
||||
code: "code",
|
||||
rate: 10,
|
||||
},
|
||||
],
|
||||
adjustments: [
|
||||
{
|
||||
code: "VIP_10",
|
||||
amount: 1,
|
||||
description: "VIP discount",
|
||||
promotion_id: "prom_123",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
currency_code: "usd",
|
||||
customer_id: "joe",
|
||||
})) as OrderDTO & {
|
||||
shipping_address_id: string
|
||||
billing_address_id: string
|
||||
}
|
||||
|
||||
const { result: toDeleteOrderEdit } = await createOrderChangeWorkflow(
|
||||
appContainer
|
||||
).run({
|
||||
input: {
|
||||
order_id: toDeleteOrder.id,
|
||||
},
|
||||
})
|
||||
|
||||
const { result: persistedOrderEdit } = await createOrderChangeWorkflow(
|
||||
appContainer
|
||||
).run({
|
||||
input: {
|
||||
order_id: persistedOrder.id,
|
||||
},
|
||||
})
|
||||
|
||||
await orderModule.deleteOrders([toDeleteOrder.id])
|
||||
|
||||
const orderItems = (await dbConnection.raw("select * from order_item;"))
|
||||
.rows
|
||||
|
||||
expect(orderItems.length).toBe(1)
|
||||
expect(orderItems[0].id).toBe(persistedOrder.items[0].detail.id)
|
||||
|
||||
/**
|
||||
* ORDER ITEMS AND LINE ITEMS
|
||||
*/
|
||||
|
||||
const orderLineItems = (
|
||||
await dbConnection.raw("select * from order_line_item;")
|
||||
).rows
|
||||
|
||||
expect(orderLineItems.length).toBe(1)
|
||||
expect(orderLineItems[0].id).toBe(persistedOrder.items[0].id)
|
||||
|
||||
const orderShipping = (
|
||||
await dbConnection.raw("select * from order_shipping;")
|
||||
).rows
|
||||
|
||||
expect(orderShipping.length).toBe(1)
|
||||
expect(orderShipping[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
order_id: persistedOrder.id,
|
||||
shipping_method_id: persistedOrder.shipping_methods[0].id,
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* ORDER SHIPPING AND SHIPPING METHODS
|
||||
*/
|
||||
|
||||
const orderShippingMethod = (
|
||||
await dbConnection.raw("select * from order_shipping_method;")
|
||||
).rows
|
||||
|
||||
expect(orderShippingMethod.length).toBe(1)
|
||||
expect(orderShippingMethod[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: persistedOrder.shipping_methods[0].id,
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* ORDER BILLING AND SHIPPING ADDRESSES
|
||||
*/
|
||||
|
||||
const addresses = (await dbConnection.raw("select * from order_address;"))
|
||||
.rows
|
||||
|
||||
expect(addresses.length).toBe(2)
|
||||
expect(addresses.map((a) => a.id)).toEqual(
|
||||
expect.arrayContaining([
|
||||
persistedOrder.shipping_address_id,
|
||||
persistedOrder.billing_address_id,
|
||||
])
|
||||
)
|
||||
|
||||
/**
|
||||
* ORDER SUMMARY
|
||||
*/
|
||||
|
||||
const orderSummary = (
|
||||
await dbConnection.raw("select * from order_summary;")
|
||||
).rows
|
||||
|
||||
expect(orderSummary.length).toBe(1)
|
||||
expect(orderSummary[0].totals.original_order_total).toBe(
|
||||
persistedOrder.summary.original_order_total
|
||||
)
|
||||
|
||||
/**
|
||||
* ORDER CHANGES
|
||||
*/
|
||||
|
||||
const orderChangeRows = (
|
||||
await dbConnection.raw("select * from order_change;")
|
||||
).rows
|
||||
|
||||
expect(orderChangeRows.length).toBe(1)
|
||||
expect(orderChangeRows[0].id).toBe(persistedOrderEdit.id)
|
||||
|
||||
const orders = (
|
||||
await api.get("/admin/orders?fields=*shipping_address", adminHeaders)
|
||||
).data.orders
|
||||
|
||||
expect(orders.length).toBe(1)
|
||||
expect(orders[0].id).toBe(persistedOrder.id)
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { IOrderModuleService } from "@medusajs/framework/types"
|
||||
import { createStep } from "@medusajs/framework/workflows-sdk"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
/**
|
||||
* The details of canceling the orders.
|
||||
*/
|
||||
export type DeleteDraftOrdersStepInput = {
|
||||
/**
|
||||
* The IDs of the orders to delete.
|
||||
*/
|
||||
orderIds: string[]
|
||||
}
|
||||
|
||||
export const deleteDraftOrdersStepId = "delete-draft-orders"
|
||||
/**
|
||||
* This step deletes one or more draft orders.
|
||||
*/
|
||||
export const deleteDraftOrdersStep = createStep(
|
||||
deleteDraftOrdersStepId,
|
||||
async (data: DeleteDraftOrdersStepInput, { container }) => {
|
||||
const service = container.resolve<IOrderModuleService>(Modules.ORDER)
|
||||
|
||||
await service.deleteOrders(data.orderIds)
|
||||
}
|
||||
)
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./validate-draft-order"
|
||||
export * from "./delete-draft-order"
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
createStep,
|
||||
createWorkflow,
|
||||
transform,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { OrderDTO } from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
import { removeRemoteLinkStep, useQueryGraphStep } from "../../common"
|
||||
import { deleteDraftOrdersStep } from "../steps"
|
||||
|
||||
/**
|
||||
* The data to validate the order's cancelation.
|
||||
*/
|
||||
export type DeleteDraftOrderStepInput = {
|
||||
/**
|
||||
* The order ids to delete.
|
||||
*/
|
||||
order_ids: string[]
|
||||
}
|
||||
|
||||
const validateDraftOrdersStep = createStep(
|
||||
"validate-draft-orders",
|
||||
async (data: { orders: OrderDTO[] }) => {
|
||||
if (
|
||||
data.orders.some(
|
||||
(order) => order.status !== "draft" || !order.is_draft_order
|
||||
)
|
||||
) {
|
||||
throw new Error("One or more orders are not draft")
|
||||
}
|
||||
|
||||
if (data.orders.some((order) => order.deleted_at)) {
|
||||
throw new Error("One or more orders are already deleted")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const deleteDraftOrderWorkflowId = "delete-draft-order"
|
||||
/**
|
||||
* This workflow deletes draft orders.
|
||||
*
|
||||
* You can also use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around canceling an order.
|
||||
*
|
||||
* @example
|
||||
* const { result } = await deleteDraftOrderWorkflow(container)
|
||||
* .run({
|
||||
* input: {
|
||||
* order_ids: ["order_123", "order_456"],
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* @summary
|
||||
*
|
||||
* Delete draft orders.
|
||||
*
|
||||
* @property hooks.orderCanceled - This hook is executed after the order is canceled. You can consume this hook to perform custom actions on the canceled order.
|
||||
*/
|
||||
export const deleteDraftOrdersWorkflow = createWorkflow(
|
||||
deleteDraftOrderWorkflowId,
|
||||
(input: WorkflowData<DeleteDraftOrderStepInput>) => {
|
||||
const orderQuery = useQueryGraphStep({
|
||||
entity: "orders",
|
||||
fields: ["id", "status", "is_draft_order", "deleted_at"],
|
||||
filters: { id: input.order_ids },
|
||||
options: { throwIfKeyNotFound: true },
|
||||
}).config({ name: "get-draft-order" })
|
||||
|
||||
const orders = transform({ orderQuery }, ({ orderQuery }) => {
|
||||
return orderQuery.data
|
||||
})
|
||||
|
||||
validateDraftOrdersStep({ orders })
|
||||
|
||||
removeRemoteLinkStep({
|
||||
[Modules.ORDER]: { order_id: input.order_ids },
|
||||
})
|
||||
|
||||
deleteDraftOrdersStep({ orderIds: input.order_ids })
|
||||
|
||||
return new WorkflowResponse(void 0)
|
||||
}
|
||||
)
|
||||
@@ -15,3 +15,4 @@ export * from "./update-draft-order-action-shipping-method"
|
||||
export * from "./update-draft-order-item"
|
||||
export * from "./update-draft-order-shipping-method"
|
||||
export * from "./remove-draft-order-shipping-method"
|
||||
export * from "./delete-draft-order"
|
||||
|
||||
@@ -162,6 +162,29 @@ export class DraftOrder {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method deletes a draft order. It sends a request to the
|
||||
* [Delete Draft Order](https://docs.medusajs.com/api/admin#draft-orders_deleteordereditsid) API route.
|
||||
*
|
||||
* @param id - The draft order's ID.
|
||||
* @param headers - Headers to pass in the request.
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.draftOrder.delete("order_123")
|
||||
* .then(({ id, object, deleted }) => {
|
||||
* console.log(id, object, deleted)
|
||||
* })
|
||||
*/
|
||||
async delete(id: string, headers?: ClientHeaders) {
|
||||
return await this.client.fetch<HttpTypes.DeleteResponse<"draft-order">>(
|
||||
`/admin/draft-orders/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method updates a draft order. It sends a request to the
|
||||
* [Update Draft Order](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersid) API route.
|
||||
|
||||
@@ -1147,6 +1147,11 @@ export interface OrderDTO {
|
||||
*/
|
||||
updated_at: string | Date
|
||||
|
||||
/**
|
||||
* When the order was deleted.
|
||||
*/
|
||||
deleted_at?: string | Date
|
||||
|
||||
/**
|
||||
* The original item total of the order.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
getOrderDetailWorkflow,
|
||||
updateDraftOrderWorkflow,
|
||||
deleteDraftOrdersWorkflow,
|
||||
} from "@medusajs/core-flows"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
@@ -54,3 +55,22 @@ export const POST = async (
|
||||
.status(200)
|
||||
.json({ draft_order: result.data[0] as HttpTypes.AdminDraftOrder })
|
||||
}
|
||||
|
||||
export const DELETE = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const { id } = req.params
|
||||
|
||||
await deleteDraftOrdersWorkflow(req.scope).run({
|
||||
input: {
|
||||
order_ids: [id],
|
||||
},
|
||||
})
|
||||
|
||||
res.status(200).json({
|
||||
id,
|
||||
object: "draft-order",
|
||||
deleted: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20250522181137 extends Migration {
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(
|
||||
`DELETE FROM "order_summary" WHERE "order_id" NOT IN (SELECT id FROM "order");`
|
||||
)
|
||||
|
||||
this.addSql(`ALTER TABLE "order_summary"
|
||||
ADD CONSTRAINT
|
||||
"order_summary_order_id_foreign" FOREIGN KEY ("order_id") REFERENCES "order" ("id")
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE;`)
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(
|
||||
`ALTER TABLE "order_summary" DROP CONSTRAINT IF EXISTS "order_summary_order_id_foreign";`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -728,7 +728,14 @@ export default class OrderModuleService
|
||||
const creditLinesToCreate: CreateOrderCreditLineDTO[] = []
|
||||
const createdOrders: InferEntityType<typeof Order>[] = []
|
||||
|
||||
for (const { items, shipping_methods, credit_lines, ...order } of data) {
|
||||
for (const {
|
||||
items,
|
||||
shipping_methods,
|
||||
credit_lines,
|
||||
shipping_address,
|
||||
billing_address,
|
||||
...order
|
||||
} of data) {
|
||||
const ord = order as any
|
||||
|
||||
const shippingMethods = shipping_methods?.map((sm: any) => {
|
||||
@@ -844,6 +851,70 @@ export default class OrderModuleService
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
// @ts-expect-error
|
||||
async deleteOrders(
|
||||
orderIds: string | string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
const ids = Array.isArray(orderIds) ? orderIds : [orderIds]
|
||||
|
||||
const orders = await this.orderService_.list(
|
||||
{ id: ids },
|
||||
{
|
||||
select: ["id", "shipping_address_id", "billing_address_id"],
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const orderAddressIds = orders
|
||||
.map((order) => [order.shipping_address_id, order.billing_address_id])
|
||||
.flat(1)
|
||||
|
||||
const orderChanges = await this.orderChangeService_.list(
|
||||
{ order_id: ids },
|
||||
{ select: ["id"] },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const orderChangeIds = orderChanges.map((orderChange) => orderChange.id)
|
||||
|
||||
const orderItems = await this.orderItemService_.list(
|
||||
{ order_id: ids },
|
||||
{ select: ["id", "item_id"] },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const lineItemIds = orderItems.map((orderItem) => orderItem.item_id)
|
||||
|
||||
const orderShipping = await this.orderShippingService_.list(
|
||||
{ order_id: ids },
|
||||
{ select: ["shipping_method_id"] },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const orderShippingMethodIds = orderShipping.map(
|
||||
(orderShipping) => orderShipping.shipping_method_id
|
||||
)
|
||||
|
||||
await promiseAll([
|
||||
this.orderAddressService_.delete(orderAddressIds, sharedContext),
|
||||
// Delete order changes & actions
|
||||
this.orderChangeService_.delete(orderChangeIds, sharedContext),
|
||||
])
|
||||
|
||||
// Delete order, order items, summary, shipping methods and transactions
|
||||
await super.deleteOrders(ids, sharedContext)
|
||||
|
||||
await promiseAll([
|
||||
this.orderLineItemService_.delete(lineItemIds, sharedContext),
|
||||
this.orderShippingMethodService_.delete(
|
||||
orderShippingMethodIds,
|
||||
sharedContext
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
async updateOrders(
|
||||
data: OrderTypes.UpdateOrderDTO[]
|
||||
|
||||
Reference in New Issue
Block a user