-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(medusa, types, utils, core-flows, order) request & accept order …
…transfer (#10106) **What** - add request order transfer workflow - add admin endpoint for transferring an order to a customer - accept order transfer storefront endpoint - accept transfer workflow - changes in the order module to introduce new change and action types --- **Note** - we return 400 instead 409 currently if there is already an active order edit, I will revisit this in a followup - endpoint for requesting order transfer from the storefront will be added in a separate PR --- RESOLVES CMRC-701 RESOLVES CMRC-703 RESOLVES CMRC-704 RESOLVES CMRC-705
- Loading branch information
Showing
21 changed files
with
660 additions
and
4 deletions.
There are no files selected for viewing
233 changes: 233 additions & 0 deletions
233
integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils" | ||
import { | ||
adminHeaders, | ||
createAdminUser, | ||
generatePublishableKey, | ||
generateStoreHeaders, | ||
} from "../../../../helpers/create-admin-user" | ||
import { createOrderSeeder } from "../../fixtures/order" | ||
|
||
jest.setTimeout(300000) | ||
|
||
medusaIntegrationTestRunner({ | ||
testSuite: ({ dbConnection, getContainer, api }) => { | ||
let order | ||
let customer | ||
let user | ||
let storeHeaders | ||
|
||
beforeEach(async () => { | ||
const container = getContainer() | ||
|
||
user = (await createAdminUser(dbConnection, adminHeaders, container)).user | ||
const publishableKey = await generatePublishableKey(container) | ||
storeHeaders = generateStoreHeaders({ publishableKey }) | ||
|
||
const seeders = await createOrderSeeder({ api, container }) | ||
|
||
const registeredCustomerToken = ( | ||
await api.post("/auth/customer/emailpass/register", { | ||
email: "[email protected]", | ||
password: "password", | ||
}) | ||
).data.token | ||
|
||
customer = ( | ||
await api.post( | ||
"/store/customers", | ||
{ | ||
email: "[email protected]", | ||
}, | ||
{ | ||
headers: { | ||
Authorization: `Bearer ${registeredCustomerToken}`, | ||
...storeHeaders.headers, | ||
}, | ||
} | ||
) | ||
).data.customer | ||
|
||
order = seeders.order | ||
}) | ||
|
||
describe("Transfer Order flow", () => { | ||
it("should pass order transfer flow from admin successfully", async () => { | ||
// 1. Admin requests order transfer for a customer with an account | ||
await api.post( | ||
`/admin/orders/${order.id}/transfer`, | ||
{ | ||
customer_id: customer.id, | ||
}, | ||
adminHeaders | ||
) | ||
|
||
const orderResult = ( | ||
await api.get( | ||
`/admin/orders/${order.id}?fields=+customer_id,+email`, | ||
adminHeaders | ||
) | ||
).data.order | ||
|
||
// 2. Order still belongs to the guest customer since the transfer hasn't been accepted yet | ||
expect(orderResult.email).toEqual("[email protected]") | ||
expect(orderResult.customer_id).not.toEqual(customer.id) | ||
|
||
const orderPreviewResult = ( | ||
await api.get(`/admin/orders/${order.id}/preview`, adminHeaders) | ||
).data.order | ||
|
||
expect(orderPreviewResult).toEqual( | ||
expect.objectContaining({ | ||
customer_id: customer.id, | ||
order_change: expect.objectContaining({ | ||
change_type: "transfer", | ||
status: "requested", | ||
requested_by: user.id, | ||
}), | ||
}) | ||
) | ||
|
||
const orderChangesResult = ( | ||
await api.get(`/admin/orders/${order.id}/changes`, adminHeaders) | ||
).data.order_changes | ||
|
||
expect(orderChangesResult.length).toEqual(1) | ||
expect(orderChangesResult[0]).toEqual( | ||
expect.objectContaining({ | ||
change_type: "transfer", | ||
status: "requested", | ||
requested_by: user.id, | ||
created_by: user.id, | ||
confirmed_by: null, | ||
confirmed_at: null, | ||
declined_by: null, | ||
actions: expect.arrayContaining([ | ||
expect.objectContaining({ | ||
version: 2, | ||
action: "TRANSFER_CUSTOMER", | ||
reference: "customer", | ||
reference_id: customer.id, | ||
details: expect.objectContaining({ | ||
token: expect.any(String), | ||
original_email: "[email protected]", | ||
}), | ||
}), | ||
]), | ||
}) | ||
) | ||
|
||
// 3. Guest customer who received the token accepts the transfer | ||
await api.post( | ||
`/store/orders/${order.id}/transfer/accept`, | ||
{ token: orderChangesResult[0].actions[0].details.token }, | ||
{ | ||
headers: { | ||
...storeHeaders.headers, | ||
}, | ||
} | ||
) | ||
|
||
const finalOrderResult = ( | ||
await api.get( | ||
`/admin/orders/${order.id}?fields=+customer_id,+email`, | ||
adminHeaders | ||
) | ||
).data.order | ||
|
||
expect(finalOrderResult.email).toEqual("[email protected]") | ||
// 4. Customer account is now associated with the order (email on the order is still as original, guest email) | ||
expect(finalOrderResult.customer_id).toEqual(customer.id) | ||
}) | ||
|
||
it("should fail to request order transfer to a guest customer", async () => { | ||
const customer = ( | ||
await api.post( | ||
"/admin/customers", | ||
{ | ||
first_name: "guest", | ||
email: "[email protected]", | ||
}, | ||
adminHeaders | ||
) | ||
).data.customer | ||
|
||
const err = await api | ||
.post( | ||
`/admin/orders/${order.id}/transfer`, | ||
{ | ||
customer_id: customer.id, | ||
}, | ||
adminHeaders | ||
) | ||
.catch((e) => e) | ||
|
||
expect(err.response.status).toBe(400) | ||
expect(err.response.data).toEqual( | ||
expect.objectContaining({ | ||
type: "invalid_data", | ||
message: `Cannot transfer order: ${order.id} to a guest customer account: [email protected]`, | ||
}) | ||
) | ||
}) | ||
|
||
it("should fail to accept order transfer with invalid token", async () => { | ||
await api.post( | ||
`/admin/orders/${order.id}/transfer`, | ||
{ | ||
customer_id: customer.id, | ||
}, | ||
adminHeaders | ||
) | ||
|
||
const orderChangesResult = ( | ||
await api.get(`/admin/orders/${order.id}/changes`, adminHeaders) | ||
).data.order_changes | ||
|
||
expect(orderChangesResult.length).toEqual(1) | ||
expect(orderChangesResult[0]).toEqual( | ||
expect.objectContaining({ | ||
change_type: "transfer", | ||
status: "requested", | ||
requested_by: user.id, | ||
created_by: user.id, | ||
confirmed_by: null, | ||
confirmed_at: null, | ||
declined_by: null, | ||
actions: expect.arrayContaining([ | ||
expect.objectContaining({ | ||
version: 2, | ||
action: "TRANSFER_CUSTOMER", | ||
reference: "customer", | ||
reference_id: customer.id, | ||
details: expect.objectContaining({ | ||
token: expect.any(String), | ||
original_email: "[email protected]", | ||
}), | ||
}), | ||
]), | ||
}) | ||
) | ||
|
||
const err = await api | ||
.post( | ||
`/store/orders/${order.id}/transfer/accept`, | ||
{ token: "fake-token" }, | ||
{ | ||
headers: { | ||
...storeHeaders.headers, | ||
}, | ||
} | ||
) | ||
.catch((e) => e) | ||
|
||
expect(err.response.status).toBe(400) | ||
expect(err.response.data).toEqual( | ||
expect.objectContaining({ | ||
type: "not_allowed", | ||
message: `Invalid token.`, | ||
}) | ||
) | ||
}) | ||
}) | ||
}, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
109 changes: 109 additions & 0 deletions
109
packages/core/core-flows/src/order/workflows/transfer/accept-order-transfer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { | ||
OrderChangeDTO, | ||
OrderDTO, | ||
OrderWorkflow, | ||
} from "@medusajs/framework/types" | ||
import { | ||
WorkflowData, | ||
WorkflowResponse, | ||
createStep, | ||
createWorkflow, | ||
} from "@medusajs/framework/workflows-sdk" | ||
import { OrderPreviewDTO } from "@medusajs/types" | ||
|
||
import { useRemoteQueryStep } from "../../../common" | ||
import { throwIfOrderIsCancelled } from "../../utils/order-validation" | ||
import { previewOrderChangeStep } from "../../steps" | ||
import { | ||
ChangeActionType, | ||
MedusaError, | ||
OrderChangeStatus, | ||
} from "@medusajs/utils" | ||
import { confirmOrderChanges } from "../../steps/confirm-order-changes" | ||
|
||
/** | ||
* This step validates that an order transfer can be accepted. | ||
*/ | ||
export const acceptOrderTransferValidationStep = createStep( | ||
"accept-order-transfer-validation", | ||
async function ({ | ||
token, | ||
order, | ||
orderChange, | ||
}: { | ||
token: string | ||
order: OrderDTO | ||
orderChange: OrderChangeDTO | ||
}) { | ||
throwIfOrderIsCancelled({ order }) | ||
|
||
if (!orderChange || orderChange.change_type !== "transfer") { | ||
throw new MedusaError( | ||
MedusaError.Types.INVALID_DATA, | ||
`Order ${order.id} does not have an order transfer request.` | ||
) | ||
} | ||
const transferCustomerAction = orderChange.actions.find( | ||
(a) => a.action === ChangeActionType.TRANSFER_CUSTOMER | ||
) | ||
|
||
if (!token.length || token !== transferCustomerAction?.details!.token) { | ||
throw new MedusaError(MedusaError.Types.NOT_ALLOWED, "Invalid token.") | ||
} | ||
} | ||
) | ||
|
||
export const acceptOrderTransferWorkflowId = "accept-order-transfer-workflow" | ||
/** | ||
* This workflow accepts an order transfer. | ||
*/ | ||
export const acceptOrderTransferWorkflow = createWorkflow( | ||
acceptOrderTransferWorkflowId, | ||
function ( | ||
input: WorkflowData<OrderWorkflow.AcceptOrderTransferWorkflowInput> | ||
): WorkflowResponse<OrderPreviewDTO> { | ||
const order: OrderDTO = useRemoteQueryStep({ | ||
entry_point: "orders", | ||
fields: ["id", "email", "status", "customer_id"], | ||
variables: { id: input.order_id }, | ||
list: false, | ||
throw_if_key_not_found: true, | ||
}) | ||
|
||
const orderChange: OrderChangeDTO = useRemoteQueryStep({ | ||
entry_point: "order_change", | ||
fields: [ | ||
"id", | ||
"status", | ||
"change_type", | ||
"actions.id", | ||
"actions.order_id", | ||
"actions.action", | ||
"actions.details", | ||
"actions.reference", | ||
"actions.reference_id", | ||
"actions.internal_note", | ||
], | ||
variables: { | ||
filters: { | ||
order_id: input.order_id, | ||
status: [OrderChangeStatus.REQUESTED], | ||
}, | ||
}, | ||
list: false, | ||
}).config({ name: "order-change-query" }) | ||
|
||
acceptOrderTransferValidationStep({ | ||
order, | ||
orderChange, | ||
token: input.token, | ||
}) | ||
|
||
confirmOrderChanges({ | ||
changes: [orderChange], | ||
orderId: order.id, | ||
}) | ||
|
||
return new WorkflowResponse(previewOrderChangeStep(input.order_id)) | ||
} | ||
) |
Oops, something went wrong.