feat(medusa, medusa-js, medusa-react): Implement store complete order… (#2275)

**What**

Allow a customer to complete a requested order edit.

**Test**
- Unit tests complete flow
- Unit tests medusa react
- Integration tests of order edit completion

FIXES CORE-501
This commit is contained in:
Adrien de Peretti
2022-09-29 19:06:45 +02:00
committed by GitHub
parent 678a06752a
commit 95c0dc653a
11 changed files with 350 additions and 34 deletions

View File

@@ -313,4 +313,68 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => {
})
})
})
describe("POST /store/order-edits/:id/complete", () => {
let requestedOrderEdit
let confirmedOrderEdit
let createdOrderEdit
beforeEach(async () => {
await adminSeeder(dbConnection)
requestedOrderEdit = await simpleOrderEditFactory(dbConnection, {
id: IdMap.getId("order-edit-1"),
created_by: "admin_user",
requested_at: new Date(),
})
confirmedOrderEdit = await simpleOrderEditFactory(dbConnection, {
id: IdMap.getId("order-edit-2"),
created_by: "admin_user",
confirmed_at: new Date(),
confirmed_by: "admin_user",
})
createdOrderEdit = await simpleOrderEditFactory(dbConnection, {
id: IdMap.getId("order-edit-3"),
created_by: "admin_user",
})
})
afterEach(async () => {
const db = useDb()
return await db.teardown()
})
// TODO once payment collection is done
/*it("complete an order edit", async () => {})*/
it("idempotently complete an already confirmed order edit", async () => {
const api = useApi()
const result = await api.post(
`/store/order-edits/${confirmedOrderEdit.id}/complete`
)
expect(result.status).toEqual(200)
expect(result.data.order_edit).toEqual(
expect.objectContaining({
id: confirmedOrderEdit.id,
status: "confirmed",
confirmed_at: expect.any(String),
})
)
})
it("fails to complete a non requested order edit", async () => {
const api = useApi()
const err = await api
.post(`/store/order-edits/${createdOrderEdit.id}/complete`)
.catch((e) => e)
expect(err.response.status).toEqual(400)
expect(err.response.data.message).toBe(
`Cannot complete an order edit with status created`
)
})
})
})

View File

@@ -1,4 +1,7 @@
import { StoreOrderEditsRes, StorePostOrderEditsOrderEditDecline } from "@medusajs/medusa"
import {
StoreOrderEditsRes,
StorePostOrderEditsOrderEditDecline,
} from "@medusajs/medusa"
import { ResponsePromise } from "../typings"
import BaseResource from "./base"
@@ -12,13 +15,18 @@ class OrderEditsResource extends BaseResource {
}
decline(
id: string,
id: string,
payload: StorePostOrderEditsOrderEditDecline,
customHeaders: Record<string, any> = {}
) {
const path = `/store/order-edits/${id}/decline`
return this.client.request("POST", path, payload, {}, customHeaders)
}
complete(id: string, customHeaders: Record<string, any> = {}) {
const path = `/store/order-edits/${id}/complete`
return this.client.request("POST", path, undefined, {}, customHeaders)
}
}
export default OrderEditsResource

View File

@@ -73,7 +73,23 @@ export const storeHandlers = [
return res(
ctx.status(200),
ctx.json({
order_edit: {...fixtures.get("order_edit"), declined_reason: req.body.declined_reason, status: 'declined'},
order_edit: {
...fixtures.get("store_order_edit"),
declined_reason: (req.body as any).declined_reason,
status: "declined",
},
})
)
}),
rest.post("/store/order-edits/:id/complete", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
order_edit: {
...fixtures.get("store_order_edit"),
status: "confirmed",
},
})
)
}),

View File

@@ -2,8 +2,8 @@ import { useMutation, UseMutationOptions, useQueryClient } from "react-query"
import { Response } from "@medusajs/medusa-js"
import {
StoreOrderEditsRes,
StorePostOrderEditsOrderEditDecline,
StoreOrderEditsRes
} from "@medusajs/medusa"
import { buildOptions } from "../../utils/buildOptions"
@@ -31,3 +31,20 @@ export const useDeclineOrderEdit = (
)
)
}
export const useCompleteOrderEdit = (
id: string,
options?: UseMutationOptions<Response<StoreOrderEditsRes>, Error>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
() => client.orderEdits.complete(id),
buildOptions(
queryClient,
[orderEditQueryKeys.lists(), orderEditQueryKeys.detail(id)],
options
)
)
}

View File

@@ -1,15 +1,15 @@
import { useDeclineOrderEdit } from "../../../../src/"
import { useCompleteOrderEdit, useDeclineOrderEdit } from "../../../../src/"
import { renderHook } from "@testing-library/react-hooks"
import { createWrapper } from "../../../utils"
describe("useCreateLineItem hook", () => {
test("creates a line item", async () => {
describe("useDeclineOrderEdit hook", () => {
test("decline an order edit", async () => {
const declineBody = {
declined_reason: "Wrong color",
}
const { result, waitFor } = renderHook(
() => useDeclineOrderEdit("test-cart"),
() => useDeclineOrderEdit("store_order_edit"),
{
wrapper: createWrapper(),
}
@@ -28,3 +28,25 @@ describe("useCreateLineItem hook", () => {
)
})
})
describe("useCompleteOrderEdit hook", () => {
test("complete an order edit", async () => {
const { result, waitFor } = renderHook(
() => useCompleteOrderEdit("store_order_edit"),
{
wrapper: createWrapper(),
}
)
result.current.mutate()
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.order_edit).toEqual(
expect.objectContaining({
status: "confirmed",
})
)
})
})

View File

@@ -0,0 +1,65 @@
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"
describe("GET /store/order-edits/:id/complete", () => {
describe("successfully complete an order edit", () => {
const orderEditId = IdMap.getId("testRequestOrder")
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/store/order-edits/${orderEditId}/complete`,
{
flags: [OrderEditingFeatureFlag],
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls orderService confirm", () => {
expect(orderEditServiceMock.confirm).toHaveBeenCalledTimes(1)
expect(orderEditServiceMock.confirm).toHaveBeenCalledWith(orderEditId, {
loggedInUserId: undefined,
})
expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1)
})
it("returns orderEdit", () => {
expect(subject.body.order_edit.id).toEqual(orderEditId)
})
})
describe("idempotently complete an order edit", () => {
const orderEditId = IdMap.getId("testConfirmOrderEdit")
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/store/order-edits/${orderEditId}/complete`,
{
flags: [OrderEditingFeatureFlag],
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls orderService confirm", () => {
expect(orderEditServiceMock.confirm).toHaveBeenCalledTimes(0)
expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1)
})
it("returns orderEdit", () => {
expect(subject.body.order_edit.id).toEqual(orderEditId)
})
})
})

View File

@@ -3,8 +3,8 @@ import { request } from "../../../../../helpers/test-request"
import { orderEditServiceMock } from "../../../../../services/__mocks__/order-edit"
import OrderEditingFeatureFlag from "../../../../../loaders/feature-flags/order-editing"
describe("GET /store/order-edits/:id", () => {
describe("successfully gets an order edit", () => {
describe("GET /store/order-edits/:id/decline", () => {
describe("successfully decline an order edit", () => {
const orderEditId = IdMap.getId("testDeclineOrderEdit")
let subject

View File

@@ -0,0 +1,98 @@
import { Request, Response } from "express"
import { EntityManager } from "typeorm"
import { OrderEditService } from "../../../../services"
import {
defaultStoreOrderEditFields,
defaultStoreOrderEditRelations,
} from "../../../../types/order-edit"
import { OrderEditStatus } from "../../../../models"
import { MedusaError } from "medusa-core-utils"
/**
* @oas [post] /order-edits/{id}/complete
* operationId: "PostOrderEditsOrderEditComplete"
* summary: "Completes an OrderEdit"
* description: "Completes an OrderEdit."
* 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 })
* medusa.orderEdit.complete(orderEditId)
* .then(({ order_edit }) => {
* console.log(order_edit.id)
* })
* - lang: Shell
* label: cURL
* source: |
* curl --location --request POST 'https://medusa-url.com/store/order-edits/{id}/complete'
* 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: OrderEditService =
req.scope.resolve("orderEditService")
const manager: EntityManager = req.scope.resolve("manager")
const userId = req.user?.customer_id ?? req.user?.id ?? req.user?.userId
await manager.transaction(async (manager) => {
const orderEditServiceTx = orderEditService.withTransaction(manager)
const orderEdit = await orderEditServiceTx.retrieve(id)
if (orderEdit.status === OrderEditStatus.CONFIRMED) {
return orderEdit
}
if (orderEdit.status !== OrderEditStatus.REQUESTED) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Cannot complete an order edit with status ${orderEdit.status}`
)
}
// TODO once payment collection is done
/*const paymentCollection = await this.paymentCollectionService_.withTransaction(manager).retrieve(orderEdit.payment_collection_id)
if (!paymentCollection.authorized_at) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Unable to complete an order edit if the payment is not authorized"
)
}*/
return await orderEditServiceTx.confirm(id, {
loggedInUserId: userId,
})
})
let orderEdit = await orderEditService.retrieve(id, {
select: defaultStoreOrderEditFields,
relations: defaultStoreOrderEditRelations,
})
orderEdit = await orderEditService.decorateTotals(orderEdit)
res.status(200).json({ order_edit: orderEdit })
}

View File

@@ -39,6 +39,11 @@ export default (app) => {
middlewares.wrap(require("./decline-order-edit").default)
)
route.post(
"/:id/complete",
middlewares.wrap(require("./complete-order-edit").default)
)
return app
}

View File

@@ -63,6 +63,15 @@ export const orderEditServiceMock = {
id: IdMap.getId("testDeclineOrderEdit"),
declined_reason: "Wrong size",
declined_at: new Date(),
status: "declined",
})
}
if (orderId === IdMap.getId("testCompleteOrderEdit")) {
return Promise.resolve({
...orderEdit,
id: IdMap.getId("testCompleteOrderEdit"),
confirmed_at: new Date(),
status: "completed",
})
}
if (orderId === IdMap.getId("testCancelOrderEdit")) {
@@ -79,6 +88,7 @@ export const orderEditServiceMock = {
id: IdMap.getId("testRequestOrder"),
requested_by: IdMap.getId("admin_user"),
requested_at: new Date(),
status: "requested",
})
}
return Promise.resolve(undefined)
@@ -130,6 +140,9 @@ export const orderEditServiceMock = {
confirm: jest.fn().mockImplementation(() => {
return Promise.resolve({})
}),
complete: jest.fn().mockImplementation(() => {
return Promise.resolve({})
}),
updateLineItem: jest.fn().mockImplementation((_) => {
return Promise.resolve()
}),

View File

@@ -20,6 +20,8 @@ import LineItemAdjustmentService from "../line-item-adjustment"
const orderEditToUpdate = {
id: IdMap.getId("order-edit-to-update"),
created_at: new Date(),
status: "created",
}
const orderEditWithChanges = {
@@ -208,10 +210,12 @@ describe("OrderEditService", () => {
internal_note: "test note",
})
expect(orderEditRepository.save).toHaveBeenCalledTimes(1)
expect(orderEditRepository.save).toHaveBeenCalledWith({
id: IdMap.getId("order-edit-to-update"),
internal_note: "test note",
})
expect(orderEditRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
id: IdMap.getId("order-edit-to-update"),
internal_note: "test note",
})
)
})
it("should create an order edit and call the repository with the right arguments as well as the event bus service", async () => {
@@ -304,6 +308,22 @@ describe("OrderEditService", () => {
})
})
it("should add a line item to an order edit", async () => {
jest
.spyOn(orderEditService, "refreshAdjustments")
.mockImplementation(async () => {})
await orderEditService.addLineItem(IdMap.getId("order-edit-with-changes"), {
variant_id: IdMap.getId("to-be-added-variant"),
quantity: 3,
})
expect(LineItemServiceMock.generate).toHaveBeenCalledTimes(1)
expect(orderEditService.refreshAdjustments).toHaveBeenCalledTimes(1)
expect(taxProviderServiceMock.createTaxLines).toHaveBeenCalledTimes(1)
expect(orderEditItemChangeServiceMock.create).toHaveBeenCalledTimes(1)
})
describe("requestConfirmation", () => {
describe("created edit", () => {
const orderEditId = IdMap.getId("order-edit-with-changes")
@@ -377,7 +397,9 @@ describe("OrderEditService", () => {
const id = IdMap.getId("canceled-order-edit")
const userId = IdMap.getId("user-id")
const result = await orderEditService.cancel(id, userId)
const result = await orderEditService.cancel(id, {
loggedInUserId: userId,
})
expect(result).toEqual(expect.objectContaining({ status: "canceled" }))
@@ -393,7 +415,7 @@ describe("OrderEditService", () => {
const userId = IdMap.getId("user-id")
try {
await orderEditService.cancel(id, userId)
await orderEditService.cancel(id, { loggedInUserId: userId })
} catch (err) {
expect(err.message).toEqual(
`Cannot cancel order edit with status ${status}`
@@ -423,11 +445,13 @@ describe("OrderEditService", () => {
)
})
it("Returns early in case of an already confirmed order edit", async () => {
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)
const result = await orderEditService.confirm(id, {
loggedInUserId: userId,
})
expect(result).toEqual(expect.objectContaining({ status: "confirmed" }))
@@ -436,20 +460,4 @@ describe("OrderEditService", () => {
})
})
})
it("should add a line item to an order edit", async () => {
jest
.spyOn(orderEditService, "refreshAdjustments")
.mockImplementation(async () => {})
await orderEditService.addLineItem(IdMap.getId("order-edit-with-changes"), {
variant_id: IdMap.getId("to-be-added-variant"),
quantity: 3,
})
expect(LineItemServiceMock.generate).toHaveBeenCalledTimes(1)
expect(orderEditService.refreshAdjustments).toHaveBeenCalledTimes(1)
expect(taxProviderServiceMock.createTaxLines).toHaveBeenCalledTimes(1)
expect(orderEditItemChangeServiceMock.create).toHaveBeenCalledTimes(1)
})
})