feat(medusa): Support OrderEdit removal (#2204)

This commit is contained in:
Philip Korsholm
2022-09-16 08:39:40 +02:00
committed by GitHub
parent 6225aa57b8
commit 09627c01d3
13 changed files with 374 additions and 5 deletions

View File

@@ -30,6 +30,7 @@ const adminHeaders = {
describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
let medusaProcess
let dbConnection
const adminUserId = "admin_user"
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", ".."))
@@ -159,7 +160,6 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.order_edit).toEqual(
expect.objectContaining({
id: orderEditId,
@@ -177,6 +177,119 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
]),
})
)
expect(response.status).toEqual(200)
})
})
describe("DELETE /admin/order-edits/:id", () => {
beforeEach(async () => {
await adminSeeder(dbConnection)
})
afterEach(async () => {
const db = useDb()
return await db.teardown()
})
it("deletes order edit", async () => {
const { id } = await simpleOrderEditFactory(dbConnection, {
created_by: adminUserId,
})
const api = useApi()
const response = await api.delete(
`/admin/order-edits/${id}`,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data).toEqual({
id,
object: "order_edit",
deleted: true,
})
})
it("deletes already removed order edit", async () => {
const { id } = await simpleOrderEditFactory(dbConnection, {
created_by: adminUserId,
})
const api = useApi()
const response = await api.delete(
`/admin/order-edits/${id}`,
adminHeaders
)
const idempontentResponse = await api.delete(
`/admin/order-edits/${id}`,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data).toEqual({
id,
object: "order_edit",
deleted: true,
})
expect(idempontentResponse.status).toEqual(200)
expect(idempontentResponse.data).toEqual({
id,
object: "order_edit",
deleted: true,
})
})
test.each([
[
"requested",
{
requested_at: new Date(),
requested_by: adminUserId,
},
],
[
"confirmed",
{
confirmed_at: new Date(),
confirmed_by: adminUserId,
},
],
[
"declined",
{
declined_at: new Date(),
declined_by: adminUserId,
},
],
[
"canceled",
{
canceled_at: new Date(),
canceled_by: adminUserId,
},
],
])("fails to delete order edit with status %s", async (status, data) => {
expect.assertions(2)
const { id } = await simpleOrderEditFactory(dbConnection, {
created_by: adminUserId,
...data,
})
const api = useApi()
await api
.delete(`/admin/order-edits/${id}`, adminHeaders)
.catch((err) => {
expect(err.response.status).toEqual(400)
expect(err.response.data.message).toEqual(
`Cannot delete order edit with status ${status}`
)
})
})
})
})

View File

@@ -1,4 +1,7 @@
import { AdminOrdersEditsRes } from "@medusajs/medusa"
import {
AdminOrdersEditsRes,
AdminOrderEditDeleteRes
} from "@medusajs/medusa"
import { ResponsePromise } from "../../typings"
import BaseResource from "../base"
@@ -10,6 +13,14 @@ class AdminOrderEditsResource extends BaseResource {
const path = `/admin/order-edits/${id}`
return this.client.request("GET", path, undefined, {}, customHeaders)
}
delete(
id: string,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminOrderEditDeleteRes> {
const path = `/admin/order-edits/${id}`
return this.client.request("DELETE", path, undefined, {}, customHeaders)
}
}
export default AdminOrderEditsResource

View File

@@ -1675,6 +1675,18 @@ export const adminHandlers = [
)
}),
rest.delete("/admin/order-edits/:id", (req, res, ctx) => {
const { id } = req.params
return res(
ctx.status(200),
ctx.json({
id,
object: "order_edit",
deleted: true
})
)
}),
rest.get("/admin/auth", (req, res, ctx) => {
return res(
ctx.status(200),

View File

@@ -1 +1,2 @@
export * from "./queries"
export * from "./mutations"

View File

@@ -0,0 +1,25 @@
import {
AdminOrderEditDeleteRes,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useMutation, UseMutationOptions, useQueryClient } from "react-query"
import { adminOrderEditsKeys } from "."
import { useMedusa } from "../../../contexts/medusa"
import { buildOptions } from "../../utils/buildOptions"
export const useAdminDeleteOrderEdit = (
id: string,
options?: UseMutationOptions<Response<AdminOrderEditDeleteRes>, Error, void>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
() => client.admin.orderEdits.delete(id),
buildOptions(
queryClient,
[adminOrderEditsKeys.detail(id), adminOrderEditsKeys.lists()],
options
)
)
}

View File

@@ -0,0 +1,26 @@
import { useAdminDeleteOrderEdit } from "../../../../src/"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../../mocks/data"
import { createWrapper } from "../../../utils"
describe("useAdminDelete hook", () => {
test("Deletes an order edit", async () => {
const id = "oe_1"
const { result, waitFor } = renderHook(() => useAdminDeleteOrderEdit(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({
id,
object: "order_edit",
deleted: true,
})
)
})
})

View File

@@ -80,7 +80,7 @@ export default (app, container, config) => {
noteRoutes(route)
notificationRoutes(route)
orderRoutes(route, featureFlagRouter)
orderEditRoutes(route, featureFlagRouter)
orderEditRoutes(route)
priceListRoutes(route, featureFlagRouter)
productRoutes(route, featureFlagRouter)
productTagRoutes(route)

View File

@@ -0,0 +1,43 @@
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("DELETE /admin/order-edits/:id", () => {
describe("deletes an order edit", () => {
const orderEditId = IdMap.getId("test-order-edit")
let subject
beforeAll(async () => {
subject = await request("DELETE", `/admin/order-edits/${orderEditId}`, {
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
flags: [OrderEditingFeatureFlag],
})
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls orderService retrieve", () => {
expect(orderEditServiceMock.delete).toHaveBeenCalledTimes(1)
expect(orderEditServiceMock.delete).toHaveBeenCalledWith(orderEditId)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns delete result", () => {
expect(subject.body).toEqual({
id: orderEditId,
object: "order_edit",
deleted: true,
})
})
})
})

View File

@@ -0,0 +1,71 @@
import { EntityManager } from "typeorm"
import { OrderEditService } from "../../../../services"
/**
* @oas [delete] /order-edits/{id}
* operationId: "DeleteOrderEditsOrderEdit"
* summary: "Delete an Order Edit"
* description: "Deletes an Order Edit"
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Note to delete.
* 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.orderEdits.delete(edit_id)
* .then(({ id, object, deleted }) => {
* console.log(id);
* });
* - lang: Shell
* label: cURL
* source: |
* curl --location --request DELETE 'https://medusa-url.com/admin/order-edits/{id}' \
* --header 'Authorization: Bearer {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* tags:
* - OrderEdit
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* properties:
* id:
* type: string
* description: The ID of the deleted Order Edit.
* object:
* type: string
* description: The type of the object that was deleted.
* format: order_edit
* deleted:
* type: boolean
* description: Whether or not the Order Edit was deleted.
* default: true
* "400":
* $ref: "#/components/responses/400_error"
*/
export default async (req, res) => {
const { id } = req.params
const orderEditService: OrderEditService =
req.scope.resolve("orderEditService")
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
await orderEditService.withTransaction(transactionManager).delete(id)
})
res.status(200).send({
id,
object: "order_edit",
deleted: true,
})
}

View File

@@ -8,6 +8,7 @@ import {
defaultOrderEditRelations,
} from "../../../../types/order-edit"
import { OrderEdit } from "../../../../models"
import { DeleteResponse } from "../../../../types/common"
const route = Router()
@@ -28,9 +29,12 @@ export default (app) => {
middlewares.wrap(require("./get-order-edit").default)
)
route.delete("/:id", middlewares.wrap(require("./delete-order-edit").default))
return app
}
export type AdminOrdersEditsRes = {
order_edit: OrderEdit
}
export type AdminOrderEditDeleteRes = DeleteResponse

View File

@@ -1,4 +1,12 @@
import { BeforeInsert, Column, JoinColumn, ManyToOne, OneToMany } from "typeorm"
import {
AfterLoad,
BeforeInsert,
Column,
CreateDateColumn,
JoinColumn,
ManyToOne,
OneToMany,
} from "typeorm"
import OrderEditingFeatureFlag from "../loaders/feature-flags/order-editing"
import { FeatureFlagEntity } from "../utils/feature-flag-decorators"
@@ -9,6 +17,14 @@ import { generateEntityId } from "../utils"
import { LineItem } from "./line-item"
import { Order } from "./order"
export enum OrderEditStatus {
CONFIRMED = "confirmed",
DECLINED = "declined",
REQUESTED = "requested",
CREATED = "created",
CANCELED = "canceled",
}
@FeatureFlagEntity(OrderEditingFeatureFlag.key)
export class OrderEdit extends SoftDeletableEntity {
@Column()
@@ -63,6 +79,8 @@ export class OrderEdit extends SoftDeletableEntity {
total: number
difference_due: number
status: OrderEditStatus
items: LineItem[]
removed_items: LineItem[]
@@ -70,6 +88,24 @@ export class OrderEdit extends SoftDeletableEntity {
private beforeInsert(): void {
this.id = generateEntityId(this.id, "oe")
}
@AfterLoad()
loadStatus(): void {
if (this.requested_at) {
this.status = OrderEditStatus.REQUESTED
}
if (this.declined_at) {
this.status = OrderEditStatus.DECLINED
}
if (this.confirmed_at) {
this.status = OrderEditStatus.CONFIRMED
}
if (this.canceled_at) {
this.status = OrderEditStatus.CANCELED
}
this.status = this.status ?? OrderEditStatus.CREATED
}
}
/**
@@ -158,6 +194,6 @@ export class OrderEdit extends SoftDeletableEntity {
* removed_items:
* type: array
* description: Computed line items from the changes that have been marked as deleted.
* removed_items:
* items:
* $ref: "#/components/schemas/line_item"
*/

View File

@@ -32,6 +32,9 @@ export const orderEditServiceMock = {
computeLineItems: jest.fn().mockImplementation((orderEdit) => {
return Promise.resolve(orderEdit)
}),
delete: jest.fn().mockImplementation((_) => {
return Promise.resolve()
}),
}
const mock = jest.fn().mockImplementation(() => {

View File

@@ -7,6 +7,7 @@ import {
LineItem,
OrderEdit,
OrderEditItemChangeType,
OrderEditStatus,
OrderItemChange,
} from "../models"
import { TransactionBaseService } from "../interfaces"
@@ -114,4 +115,27 @@ export default class OrderEditService extends TransactionBaseService {
return { items, removedItems }
}
async delete(orderEditId: string): Promise<void> {
return await this.atomicPhase_(async (manager) => {
const orderEditRepo = manager.getCustomRepository(
this.orderEditRepository_
)
const edit = await orderEditRepo.findOne({ where: { id: orderEditId } })
if (!edit) {
return
}
if (edit.status !== OrderEditStatus.CREATED) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Cannot delete order edit with status ${edit.status}`
)
}
await orderEditRepo.softRemove(edit)
})
}
}