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
This commit is contained in:
@@ -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: "test@email.com",
|
||||||
|
password: "password",
|
||||||
|
})
|
||||||
|
).data.token
|
||||||
|
|
||||||
|
customer = (
|
||||||
|
await api.post(
|
||||||
|
"/store/customers",
|
||||||
|
{
|
||||||
|
email: "test@email.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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("tony@stark-industries.com")
|
||||||
|
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: "tony@stark-industries.com",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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("tony@stark-industries.com")
|
||||||
|
// 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: "guest@medusajs.com",
|
||||||
|
},
|
||||||
|
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: guest@medusajs.com`,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
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: "tony@stark-industries.com",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
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.`,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -78,3 +78,5 @@ export * from "./return/update-return-shipping-method"
|
|||||||
export * from "./update-order-change-actions"
|
export * from "./update-order-change-actions"
|
||||||
export * from "./update-order-changes"
|
export * from "./update-order-changes"
|
||||||
export * from "./update-tax-lines"
|
export * from "./update-tax-lines"
|
||||||
|
export * from "./transfer/request-order-transfer"
|
||||||
|
export * from "./transfer/accept-order-transfer"
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { OrderDTO, OrderWorkflow } from "@medusajs/framework/types"
|
||||||
|
import {
|
||||||
|
WorkflowData,
|
||||||
|
WorkflowResponse,
|
||||||
|
createStep,
|
||||||
|
createWorkflow,
|
||||||
|
transform,
|
||||||
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
|
import { CustomerDTO, OrderPreviewDTO } from "@medusajs/types"
|
||||||
|
import { v4 as uid } from "uuid"
|
||||||
|
|
||||||
|
import { emitEventStep, useRemoteQueryStep } from "../../../common"
|
||||||
|
import { createOrderChangeStep } from "../../steps/create-order-change"
|
||||||
|
import { throwIfOrderIsCancelled } from "../../utils/order-validation"
|
||||||
|
import { createOrderChangeActionsWorkflow } from "../create-order-change-actions"
|
||||||
|
import {
|
||||||
|
ChangeActionType,
|
||||||
|
MedusaError,
|
||||||
|
OrderChangeStatus,
|
||||||
|
OrderWorkflowEvents,
|
||||||
|
} from "@medusajs/utils"
|
||||||
|
import { previewOrderChangeStep, updateOrderChangesStep } from "../../steps"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This step validates that an order transfer can be requested.
|
||||||
|
*/
|
||||||
|
export const requestOrderTransferValidationStep = createStep(
|
||||||
|
"request-order-transfer-validation",
|
||||||
|
async function ({
|
||||||
|
order,
|
||||||
|
customer,
|
||||||
|
}: {
|
||||||
|
order: OrderDTO
|
||||||
|
customer: CustomerDTO
|
||||||
|
}) {
|
||||||
|
throwIfOrderIsCancelled({ order })
|
||||||
|
|
||||||
|
if (!customer.has_account) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Cannot transfer order: ${order.id} to a guest customer account: ${customer.email}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order.customer_id === customer.id) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Order: ${order.id} already belongs to customer: ${customer.id}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const requestOrderTransferWorkflowId = "request-order-transfer-workflow"
|
||||||
|
/**
|
||||||
|
* This workflow requests an order transfer.
|
||||||
|
*/
|
||||||
|
export const requestOrderTransferWorkflow = createWorkflow(
|
||||||
|
requestOrderTransferWorkflowId,
|
||||||
|
function (
|
||||||
|
input: WorkflowData<OrderWorkflow.RequestOrderTransferWorkflowInput>
|
||||||
|
): 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 customer: CustomerDTO = useRemoteQueryStep({
|
||||||
|
entry_point: "customers",
|
||||||
|
fields: ["id", "email", "has_account"],
|
||||||
|
variables: { id: input.customer_id },
|
||||||
|
list: false,
|
||||||
|
throw_if_key_not_found: true,
|
||||||
|
}).config({ name: "customer-query" })
|
||||||
|
|
||||||
|
requestOrderTransferValidationStep({ order, customer })
|
||||||
|
|
||||||
|
const orderChangeInput = transform({ input }, ({ input }) => {
|
||||||
|
return {
|
||||||
|
change_type: "transfer" as const,
|
||||||
|
order_id: input.order_id,
|
||||||
|
created_by: input.logged_in_user,
|
||||||
|
description: input.description,
|
||||||
|
internal_note: input.internal_note,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const change = createOrderChangeStep(orderChangeInput)
|
||||||
|
|
||||||
|
const actionInput = transform(
|
||||||
|
{ order, input, change },
|
||||||
|
({ order, input, change }) => [
|
||||||
|
{
|
||||||
|
order_change_id: change.id,
|
||||||
|
order_id: input.order_id,
|
||||||
|
action: ChangeActionType.TRANSFER_CUSTOMER,
|
||||||
|
version: change.version,
|
||||||
|
reference: "customer",
|
||||||
|
reference_id: input.customer_id,
|
||||||
|
details: {
|
||||||
|
token: uid(),
|
||||||
|
original_email: order.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
createOrderChangeActionsWorkflow.runAsStep({
|
||||||
|
input: actionInput,
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateOrderChangeInput = transform(
|
||||||
|
{ input, change },
|
||||||
|
({ input, change }) => [
|
||||||
|
{
|
||||||
|
id: change.id,
|
||||||
|
status: OrderChangeStatus.REQUESTED,
|
||||||
|
requested_by: input.logged_in_user,
|
||||||
|
requested_at: new Date(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
updateOrderChangesStep(updateOrderChangeInput)
|
||||||
|
|
||||||
|
emitEventStep({
|
||||||
|
eventName: OrderWorkflowEvents.TRANSFER_REQUESTED,
|
||||||
|
data: { id: input.order_id },
|
||||||
|
})
|
||||||
|
|
||||||
|
return new WorkflowResponse(previewOrderChangeStep(input.order_id))
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -24,6 +24,7 @@ export type ChangeActionType =
|
|||||||
| "SHIP_ITEM"
|
| "SHIP_ITEM"
|
||||||
| "WRITE_OFF_ITEM"
|
| "WRITE_OFF_ITEM"
|
||||||
| "REINSTATE_ITEM"
|
| "REINSTATE_ITEM"
|
||||||
|
| "TRANSFER_CUSTOMER"
|
||||||
|
|
||||||
export type OrderChangeStatus =
|
export type OrderChangeStatus =
|
||||||
| "confirmed"
|
| "confirmed"
|
||||||
@@ -2116,7 +2117,7 @@ export interface OrderChangeDTO {
|
|||||||
/**
|
/**
|
||||||
* The type of the order change
|
* The type of the order change
|
||||||
*/
|
*/
|
||||||
change_type?: "return" | "exchange" | "claim" | "edit"
|
change_type?: "return" | "exchange" | "claim" | "edit" | "transfer"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ID of the associated order
|
* The ID of the associated order
|
||||||
|
|||||||
@@ -866,6 +866,7 @@ export interface CreateOrderChangeDTO {
|
|||||||
| "exchange"
|
| "exchange"
|
||||||
| "claim"
|
| "claim"
|
||||||
| "edit"
|
| "edit"
|
||||||
|
| "transfer"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The description of the order change.
|
* The description of the order change.
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface AcceptOrderTransferWorkflowInput {
|
||||||
|
order_id: string
|
||||||
|
token: string
|
||||||
|
}
|
||||||
@@ -15,3 +15,5 @@ export * from "./receive-return"
|
|||||||
export * from "./request-item-return"
|
export * from "./request-item-return"
|
||||||
export * from "./shipping-method"
|
export * from "./shipping-method"
|
||||||
export * from "./update-return"
|
export * from "./update-return"
|
||||||
|
export * from "./request-transfer"
|
||||||
|
export * from "./accept-transfer"
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export interface RequestOrderTransferWorkflowInput {
|
||||||
|
order_id: string
|
||||||
|
customer_id: string
|
||||||
|
logged_in_user: string
|
||||||
|
|
||||||
|
description?: string
|
||||||
|
internal_note?: string
|
||||||
|
}
|
||||||
@@ -25,6 +25,8 @@ export const OrderWorkflowEvents = {
|
|||||||
|
|
||||||
CLAIM_CREATED: "order.claim_created",
|
CLAIM_CREATED: "order.claim_created",
|
||||||
EXCHANGE_CREATED: "order.exchange_created",
|
EXCHANGE_CREATED: "order.exchange_created",
|
||||||
|
|
||||||
|
TRANSFER_REQUESTED: "order.transfer_requested",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserWorkflowEvents = {
|
export const UserWorkflowEvents = {
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ export enum ChangeActionType {
|
|||||||
SHIP_ITEM = "SHIP_ITEM",
|
SHIP_ITEM = "SHIP_ITEM",
|
||||||
WRITE_OFF_ITEM = "WRITE_OFF_ITEM",
|
WRITE_OFF_ITEM = "WRITE_OFF_ITEM",
|
||||||
REINSTATE_ITEM = "REINSTATE_ITEM",
|
REINSTATE_ITEM = "REINSTATE_ITEM",
|
||||||
|
TRANSFER_CUSTOMER = "TRANSFER_CUSTOMER",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { requestOrderTransferWorkflow } from "@medusajs/core-flows"
|
||||||
|
import {
|
||||||
|
AuthenticatedMedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
} from "@medusajs/framework/http"
|
||||||
|
import { HttpTypes } from "@medusajs/framework/types"
|
||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
remoteQueryObjectFromString,
|
||||||
|
} from "@medusajs/framework/utils"
|
||||||
|
import { AdminTransferOrderType } from "../../validators"
|
||||||
|
|
||||||
|
export const POST = async (
|
||||||
|
req: AuthenticatedMedusaRequest<AdminTransferOrderType>,
|
||||||
|
res: MedusaResponse<HttpTypes.AdminOrderResponse>
|
||||||
|
) => {
|
||||||
|
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||||
|
|
||||||
|
const variables = { id: req.params.id }
|
||||||
|
|
||||||
|
await requestOrderTransferWorkflow(req.scope).run({
|
||||||
|
input: {
|
||||||
|
order_id: req.params.id,
|
||||||
|
customer_id: req.validatedBody.customer_id,
|
||||||
|
logged_in_user: req.auth_context.actor_id,
|
||||||
|
description: req.validatedBody.description,
|
||||||
|
internal_note: req.validatedBody.internal_note,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const queryObject = remoteQueryObjectFromString({
|
||||||
|
entryPoint: "order",
|
||||||
|
variables,
|
||||||
|
fields: req.remoteQueryConfig.fields,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [order] = await remoteQuery(queryObject)
|
||||||
|
res.status(200).json({ order })
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
AdminOrderChanges,
|
AdminOrderChanges,
|
||||||
AdminOrderCreateFulfillment,
|
AdminOrderCreateFulfillment,
|
||||||
AdminOrderCreateShipment,
|
AdminOrderCreateShipment,
|
||||||
|
AdminTransferOrder,
|
||||||
} from "./validators"
|
} from "./validators"
|
||||||
|
|
||||||
export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [
|
export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [
|
||||||
@@ -144,4 +145,15 @@ export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: ["POST"],
|
||||||
|
matcher: "/admin/orders/:id/transfer",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformBody(AdminTransferOrder),
|
||||||
|
validateAndTransformQuery(
|
||||||
|
AdminGetOrdersOrderParams,
|
||||||
|
QueryConfig.retrieveTransformQueryConfig
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -120,3 +120,10 @@ export type AdminMarkOrderFulfillmentDeliveredType = z.infer<
|
|||||||
typeof AdminMarkOrderFulfillmentDelivered
|
typeof AdminMarkOrderFulfillmentDelivered
|
||||||
>
|
>
|
||||||
export const AdminMarkOrderFulfillmentDelivered = z.object({})
|
export const AdminMarkOrderFulfillmentDelivered = z.object({})
|
||||||
|
|
||||||
|
export type AdminTransferOrderType = z.infer<typeof AdminTransferOrder>
|
||||||
|
export const AdminTransferOrder = z.object({
|
||||||
|
customer_id: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
internal_note: z.string().optional(),
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework"
|
||||||
|
import { HttpTypes } from "@medusajs/framework/types"
|
||||||
|
import {
|
||||||
|
acceptOrderTransferWorkflow,
|
||||||
|
getOrderDetailWorkflow,
|
||||||
|
} from "@medusajs/core-flows"
|
||||||
|
|
||||||
|
import { StoreAcceptOrderTransferType } from "../../../validators"
|
||||||
|
|
||||||
|
export const POST = async (
|
||||||
|
req: AuthenticatedMedusaRequest<StoreAcceptOrderTransferType>,
|
||||||
|
res: MedusaResponse<HttpTypes.StoreOrderResponse>
|
||||||
|
) => {
|
||||||
|
await acceptOrderTransferWorkflow(req.scope).run({
|
||||||
|
input: {
|
||||||
|
order_id: req.params.id,
|
||||||
|
token: req.validatedBody.token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = await getOrderDetailWorkflow(req.scope).run({
|
||||||
|
input: {
|
||||||
|
fields: req.remoteQueryConfig.fields,
|
||||||
|
order_id: req.params.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({ order: result as HttpTypes.StoreOrder })
|
||||||
|
}
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
import { MiddlewareRoute } from "@medusajs/framework/http"
|
import {
|
||||||
|
MiddlewareRoute,
|
||||||
|
validateAndTransformBody,
|
||||||
|
} from "@medusajs/framework/http"
|
||||||
import { authenticate } from "../../../utils/middlewares/authenticate-middleware"
|
import { authenticate } from "../../../utils/middlewares/authenticate-middleware"
|
||||||
import { validateAndTransformQuery } from "@medusajs/framework"
|
import { validateAndTransformQuery } from "@medusajs/framework"
|
||||||
import * as QueryConfig from "./query-config"
|
import * as QueryConfig from "./query-config"
|
||||||
import { StoreGetOrderParams, StoreGetOrdersParams } from "./validators"
|
import {
|
||||||
|
StoreGetOrderParams,
|
||||||
|
StoreGetOrdersParams,
|
||||||
|
StoreAcceptOrderTransfer,
|
||||||
|
} from "./validators"
|
||||||
|
|
||||||
export const storeOrderRoutesMiddlewares: MiddlewareRoute[] = [
|
export const storeOrderRoutesMiddlewares: MiddlewareRoute[] = [
|
||||||
{
|
{
|
||||||
@@ -26,4 +33,15 @@ export const storeOrderRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: ["POST"],
|
||||||
|
matcher: "/store/orders/:id/transfer/accept",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformBody(StoreAcceptOrderTransfer),
|
||||||
|
validateAndTransformQuery(
|
||||||
|
StoreGetOrderParams,
|
||||||
|
QueryConfig.retrieveTransformQueryConfig
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -18,3 +18,11 @@ export const StoreGetOrdersParams = createFindParams({
|
|||||||
.merge(applyAndAndOrOperators(StoreGetOrdersParamsFields))
|
.merge(applyAndAndOrOperators(StoreGetOrdersParamsFields))
|
||||||
|
|
||||||
export type StoreGetOrdersParamsType = z.infer<typeof StoreGetOrdersParams>
|
export type StoreGetOrdersParamsType = z.infer<typeof StoreGetOrdersParams>
|
||||||
|
|
||||||
|
export const StoreAcceptOrderTransfer = z.object({
|
||||||
|
token: z.string().min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type StoreAcceptOrderTransferType = z.infer<
|
||||||
|
typeof StoreAcceptOrderTransfer
|
||||||
|
>
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ export type VirtualOrder = {
|
|||||||
|
|
||||||
total: BigNumberInput
|
total: BigNumberInput
|
||||||
|
|
||||||
|
customer_id?: string
|
||||||
|
|
||||||
transactions?: OrderTransaction[]
|
transactions?: OrderTransaction[]
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ export * from "./ship-item"
|
|||||||
export * from "./shipping-add"
|
export * from "./shipping-add"
|
||||||
export * from "./shipping-remove"
|
export * from "./shipping-remove"
|
||||||
export * from "./write-off-item"
|
export * from "./write-off-item"
|
||||||
|
export * from "./transfer-customer"
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { ChangeActionType, MedusaError } from "@medusajs/framework/utils"
|
||||||
|
import { OrderChangeProcessing } from "../calculate-order-change"
|
||||||
|
import { setActionReference } from "../set-action-reference"
|
||||||
|
|
||||||
|
OrderChangeProcessing.registerActionType(ChangeActionType.TRANSFER_CUSTOMER, {
|
||||||
|
operation({ action, currentOrder, options }) {
|
||||||
|
currentOrder.customer_id = action.reference_id
|
||||||
|
|
||||||
|
setActionReference(currentOrder, action, options)
|
||||||
|
},
|
||||||
|
validate({ action }) {
|
||||||
|
if (!action.reference_id) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
"Reference to customer ID is required"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -27,6 +27,13 @@ export function applyChangesToOrder(
|
|||||||
const summariesToUpsert: any[] = []
|
const summariesToUpsert: any[] = []
|
||||||
const orderToUpdate: any[] = []
|
const orderToUpdate: any[] = []
|
||||||
|
|
||||||
|
const orderEditableAttributes = [
|
||||||
|
"customer_id",
|
||||||
|
"sales_channel_id",
|
||||||
|
"email",
|
||||||
|
"no_notification",
|
||||||
|
]
|
||||||
|
|
||||||
const calculatedOrders = {}
|
const calculatedOrders = {}
|
||||||
for (const order of orders) {
|
for (const order of orders) {
|
||||||
const calculated = calculateOrderChange({
|
const calculated = calculateOrderChange({
|
||||||
@@ -41,6 +48,17 @@ export function applyChangesToOrder(
|
|||||||
calculatedOrders[order.id] = calculated
|
calculatedOrders[order.id] = calculated
|
||||||
|
|
||||||
const version = actionsMap[order.id]?.[0]?.version ?? order.version
|
const version = actionsMap[order.id]?.[0]?.version ?? order.version
|
||||||
|
const orderAttributes: {
|
||||||
|
version?: number
|
||||||
|
customer_id?: string
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
// Editable attributes that have changed
|
||||||
|
for (const attr of orderEditableAttributes) {
|
||||||
|
if (order[attr] !== calculated.order[attr]) {
|
||||||
|
orderAttributes[attr] = calculated.order[attr]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const item of calculated.order.items) {
|
for (const item of calculated.order.items) {
|
||||||
if (MathBN.lte(item.quantity, 0)) {
|
if (MathBN.lte(item.quantity, 0)) {
|
||||||
@@ -113,12 +131,16 @@ export function applyChangesToOrder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
orderAttributes.version = version
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(orderAttributes).length > 0) {
|
||||||
orderToUpdate.push({
|
orderToUpdate.push({
|
||||||
selector: {
|
selector: {
|
||||||
id: order.id,
|
id: order.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
version,
|
...orderAttributes,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user