Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(medusa): request order transfer storefront API #10156

Merged
merged 3 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 148 additions & 32 deletions integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { Modules } from "@medusajs/utils"
import {
adminHeaders,
createAdminUser,
Expand All @@ -11,46 +12,47 @@ jest.setTimeout(300000)

medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => {
let order
let customer
let user
let storeHeaders
describe("Transfer Order flow (Admin)", () => {
let order
let customer
let user
let storeHeaders

beforeEach(async () => {
const container = getContainer()
beforeEach(async () => {
const container = getContainer()

user = (await createAdminUser(dbConnection, adminHeaders, container)).user
const publishableKey = await generatePublishableKey(container)
storeHeaders = generateStoreHeaders({ publishableKey })
user = (await createAdminUser(dbConnection, adminHeaders, container))
.user
const publishableKey = await generatePublishableKey(container)
storeHeaders = generateStoreHeaders({ publishableKey })

const seeders = await createOrderSeeder({ api, container })
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",
{
const registeredCustomerToken = (
await api.post("/auth/customer/emailpass/register", {
email: "[email protected]",
},
{
headers: {
Authorization: `Bearer ${registeredCustomerToken}`,
...storeHeaders.headers,
password: "password",
})
).data.token

customer = (
await api.post(
"/store/customers",
{
email: "[email protected]",
},
}
)
).data.customer
{
headers: {
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 () => {
// 1. Admin requests order transfer for a customer with an account
await api.post(
Expand Down Expand Up @@ -229,5 +231,119 @@ medusaIntegrationTestRunner({
)
})
})

describe("Transfer Order flow (Admin)", () => {
fPolic marked this conversation as resolved.
Show resolved Hide resolved
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: "[email protected]",
password: "password",
})
).data.token

customer = (
await api.post(
"/store/customers",
{
email: "[email protected]",
},
{
headers: {
Authorization: `Bearer ${registeredCustomerToken}`,
...storeHeaders.headers,
},
}
)
).data.customer

signInToken = (
await api.post("/auth/customer/emailpass", {
email: "[email protected]",
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("[email protected]")
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: "[email protected]",
}),
}),
]),
})
)

// 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("[email protected]")
// 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)
})
})
},
})
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ export const requestOrderTransferValidationStep = createStep(
export const requestOrderTransferWorkflowId = "request-order-transfer-workflow"
/**
* 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(
requestOrderTransferWorkflowId,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 })
}
13 changes: 13 additions & 0 deletions packages/medusa/src/api/store/orders/middlewares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
StoreGetOrderParams,
StoreGetOrdersParams,
StoreAcceptOrderTransfer,
StoreRequestOrderTransfer,
} from "./validators"

export const storeOrderRoutesMiddlewares: MiddlewareRoute[] = [
Expand All @@ -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"],
matcher: "/store/orders/:id/transfer/accept",
Expand Down
7 changes: 7 additions & 0 deletions packages/medusa/src/api/store/orders/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,10 @@ export const StoreAcceptOrderTransfer = z.object({
export type StoreAcceptOrderTransferType = z.infer<
typeof StoreAcceptOrderTransfer
>

export type StoreRequestOrderTransferType = z.infer<
typeof StoreRequestOrderTransfer
>
export const StoreRequestOrderTransfer = z.object({
description: z.string().optional(),
})
Loading