feat(medusa): request order transfer storefront API (#10156)
**What** - add an endpoint for order transfer requests from the storefront --- CLOSES CMRC-702
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||||
|
import { Modules } from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
adminHeaders,
|
adminHeaders,
|
||||||
createAdminUser,
|
createAdminUser,
|
||||||
@@ -11,46 +12,47 @@ jest.setTimeout(300000)
|
|||||||
|
|
||||||
medusaIntegrationTestRunner({
|
medusaIntegrationTestRunner({
|
||||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
let order
|
describe("Transfer Order flow (Admin)", () => {
|
||||||
let customer
|
let order
|
||||||
let user
|
let customer
|
||||||
let storeHeaders
|
let user
|
||||||
|
let storeHeaders
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const container = getContainer()
|
const container = getContainer()
|
||||||
|
|
||||||
user = (await createAdminUser(dbConnection, adminHeaders, container)).user
|
user = (await createAdminUser(dbConnection, adminHeaders, container))
|
||||||
const publishableKey = await generatePublishableKey(container)
|
.user
|
||||||
storeHeaders = generateStoreHeaders({ publishableKey })
|
const publishableKey = await generatePublishableKey(container)
|
||||||
|
storeHeaders = generateStoreHeaders({ publishableKey })
|
||||||
|
|
||||||
const seeders = await createOrderSeeder({ api, container })
|
const seeders = await createOrderSeeder({ api, container })
|
||||||
|
|
||||||
const registeredCustomerToken = (
|
const registeredCustomerToken = (
|
||||||
await api.post("/auth/customer/emailpass/register", {
|
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",
|
email: "test@email.com",
|
||||||
},
|
password: "password",
|
||||||
{
|
})
|
||||||
headers: {
|
).data.token
|
||||||
Authorization: `Bearer ${registeredCustomerToken}`,
|
|
||||||
...storeHeaders.headers,
|
customer = (
|
||||||
|
await api.post(
|
||||||
|
"/store/customers",
|
||||||
|
{
|
||||||
|
email: "test@email.com",
|
||||||
},
|
},
|
||||||
}
|
{
|
||||||
)
|
headers: {
|
||||||
).data.customer
|
Authorization: `Bearer ${registeredCustomerToken}`,
|
||||||
|
...storeHeaders.headers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data.customer
|
||||||
|
|
||||||
order = seeders.order
|
order = seeders.order
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Transfer Order flow", () => {
|
|
||||||
it("should pass order transfer flow from admin successfully", async () => {
|
it("should pass order transfer flow from admin successfully", async () => {
|
||||||
// 1. Admin requests order transfer for a customer with an account
|
// 1. Admin requests order transfer for a customer with an account
|
||||||
await api.post(
|
await api.post(
|
||||||
@@ -229,5 +231,119 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("Transfer Order flow (Store, self-serve)", () => {
|
||||||
|
let order
|
||||||
|
let customer
|
||||||
|
let storeHeaders
|
||||||
|
let signInToken
|
||||||
|
|
||||||
|
let orderModule
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const container = getContainer()
|
||||||
|
|
||||||
|
orderModule = await container.resolve(Modules.ORDER)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
signInToken = (
|
||||||
|
await api.post("/auth/customer/emailpass", {
|
||||||
|
email: "test@email.com",
|
||||||
|
password: "password",
|
||||||
|
})
|
||||||
|
).data.token
|
||||||
|
|
||||||
|
order = seeders.order
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should pass order transfer flow from storefront successfully", async () => {
|
||||||
|
// 1. Customer requests order transfer
|
||||||
|
const storeOrder = (
|
||||||
|
await api.post(
|
||||||
|
`/store/orders/${order.id}/transfer/request?fields=+email,+customer_id`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${signInToken}`,
|
||||||
|
...storeHeaders.headers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data.order
|
||||||
|
|
||||||
|
// 2. Order still belongs to the guest customer since the transfer hasn't been accepted yet
|
||||||
|
expect(storeOrder.email).toEqual("tony@stark-industries.com")
|
||||||
|
expect(storeOrder.customer_id).not.toEqual(customer.id)
|
||||||
|
|
||||||
|
const orderChanges = await orderModule.listOrderChanges(
|
||||||
|
{ order_id: order.id },
|
||||||
|
{ relations: ["actions"] }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(orderChanges.length).toEqual(1)
|
||||||
|
expect(orderChanges[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
change_type: "transfer",
|
||||||
|
status: "requested",
|
||||||
|
requested_by: customer.id,
|
||||||
|
created_by: customer.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
|
||||||
|
const finalOrder = (
|
||||||
|
await api.post(
|
||||||
|
`/store/orders/${order.id}/transfer/accept?fields=+email,+customer_id`,
|
||||||
|
{ token: orderChanges[0].actions[0].details.token },
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
).data.order
|
||||||
|
|
||||||
|
expect(finalOrder.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(finalOrder.customer_id).toEqual(customer.id)
|
||||||
|
})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ export const requestOrderTransferValidationStep = createStep(
|
|||||||
export const requestOrderTransferWorkflowId = "request-order-transfer-workflow"
|
export const requestOrderTransferWorkflowId = "request-order-transfer-workflow"
|
||||||
/**
|
/**
|
||||||
* This workflow requests an order transfer.
|
* This workflow requests an order transfer.
|
||||||
|
*
|
||||||
|
* Can be initiated by a store admin or the customer.
|
||||||
|
* If customer requested the transfer `input.logged_in_user === input.customer_id`.
|
||||||
*/
|
*/
|
||||||
export const requestOrderTransferWorkflow = createWorkflow(
|
export const requestOrderTransferWorkflow = createWorkflow(
|
||||||
requestOrderTransferWorkflowId,
|
requestOrderTransferWorkflowId,
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
getOrderDetailWorkflow,
|
||||||
|
requestOrderTransferWorkflow,
|
||||||
|
} from "@medusajs/core-flows"
|
||||||
|
import {
|
||||||
|
AuthenticatedMedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
} from "@medusajs/framework/http"
|
||||||
|
import { HttpTypes } from "@medusajs/framework/types"
|
||||||
|
import { StoreRequestOrderTransferType } from "../../../validators"
|
||||||
|
|
||||||
|
export const POST = async (
|
||||||
|
req: AuthenticatedMedusaRequest<StoreRequestOrderTransferType>,
|
||||||
|
res: MedusaResponse<HttpTypes.StoreOrderResponse>
|
||||||
|
) => {
|
||||||
|
const orderId = req.params.id
|
||||||
|
const customerId = req.auth_context.actor_id
|
||||||
|
|
||||||
|
await requestOrderTransferWorkflow(req.scope).run({
|
||||||
|
input: {
|
||||||
|
order_id: orderId,
|
||||||
|
customer_id: customerId,
|
||||||
|
logged_in_user: customerId,
|
||||||
|
description: req.validatedBody.description,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = await getOrderDetailWorkflow(req.scope).run({
|
||||||
|
input: {
|
||||||
|
fields: req.remoteQueryConfig.fields,
|
||||||
|
order_id: orderId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({ order: result as HttpTypes.StoreOrder })
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
StoreGetOrderParams,
|
StoreGetOrderParams,
|
||||||
StoreGetOrdersParams,
|
StoreGetOrdersParams,
|
||||||
StoreAcceptOrderTransfer,
|
StoreAcceptOrderTransfer,
|
||||||
|
StoreRequestOrderTransfer,
|
||||||
} from "./validators"
|
} from "./validators"
|
||||||
|
|
||||||
export const storeOrderRoutesMiddlewares: MiddlewareRoute[] = [
|
export const storeOrderRoutesMiddlewares: MiddlewareRoute[] = [
|
||||||
@@ -33,6 +34,18 @@ export const storeOrderRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: ["POST"],
|
||||||
|
matcher: "/store/orders/:id/transfer/request",
|
||||||
|
middlewares: [
|
||||||
|
authenticate("customer", ["session", "bearer"]),
|
||||||
|
validateAndTransformBody(StoreRequestOrderTransfer),
|
||||||
|
validateAndTransformQuery(
|
||||||
|
StoreGetOrderParams,
|
||||||
|
QueryConfig.retrieveTransformQueryConfig
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
method: ["POST"],
|
method: ["POST"],
|
||||||
matcher: "/store/orders/:id/transfer/accept",
|
matcher: "/store/orders/:id/transfer/accept",
|
||||||
|
|||||||
@@ -26,3 +26,10 @@ export const StoreAcceptOrderTransfer = z.object({
|
|||||||
export type StoreAcceptOrderTransferType = z.infer<
|
export type StoreAcceptOrderTransferType = z.infer<
|
||||||
typeof StoreAcceptOrderTransfer
|
typeof StoreAcceptOrderTransfer
|
||||||
>
|
>
|
||||||
|
|
||||||
|
export type StoreRequestOrderTransferType = z.infer<
|
||||||
|
typeof StoreRequestOrderTransfer
|
||||||
|
>
|
||||||
|
export const StoreRequestOrderTransfer = z.object({
|
||||||
|
description: z.string().optional(),
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user