diff --git a/integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts b/integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts index 51fd210b19a66..80455cc919d69 100644 --- a/integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts +++ b/integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts @@ -1,4 +1,5 @@ import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { Modules } from "@medusajs/utils" import { adminHeaders, createAdminUser, @@ -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: "test@email.com", - password: "password", - }) - ).data.token - - customer = ( - await api.post( - "/store/customers", - { + const registeredCustomerToken = ( + await api.post("/auth/customer/emailpass/register", { email: "test@email.com", - }, - { - headers: { - Authorization: `Bearer ${registeredCustomerToken}`, - ...storeHeaders.headers, + password: "password", + }) + ).data.token + + customer = ( + await api.post( + "/store/customers", + { + email: "test@email.com", }, - } - ) - ).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( @@ -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) + }) + }) }, }) diff --git a/packages/core/core-flows/src/order/workflows/transfer/request-order-transfer.ts b/packages/core/core-flows/src/order/workflows/transfer/request-order-transfer.ts index cee99c313fb46..dabda451f843c 100644 --- a/packages/core/core-flows/src/order/workflows/transfer/request-order-transfer.ts +++ b/packages/core/core-flows/src/order/workflows/transfer/request-order-transfer.ts @@ -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, diff --git a/packages/medusa/src/api/store/orders/[id]/transfer/request/route.ts b/packages/medusa/src/api/store/orders/[id]/transfer/request/route.ts new file mode 100644 index 0000000000000..43f4bc7f62ea8 --- /dev/null +++ b/packages/medusa/src/api/store/orders/[id]/transfer/request/route.ts @@ -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, + res: MedusaResponse +) => { + 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 }) +} diff --git a/packages/medusa/src/api/store/orders/middlewares.ts b/packages/medusa/src/api/store/orders/middlewares.ts index c30ea4f3e90ec..a79dd7a71d225 100644 --- a/packages/medusa/src/api/store/orders/middlewares.ts +++ b/packages/medusa/src/api/store/orders/middlewares.ts @@ -9,6 +9,7 @@ import { StoreGetOrderParams, StoreGetOrdersParams, StoreAcceptOrderTransfer, + StoreRequestOrderTransfer, } from "./validators" 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"], matcher: "/store/orders/:id/transfer/accept", diff --git a/packages/medusa/src/api/store/orders/validators.ts b/packages/medusa/src/api/store/orders/validators.ts index bb60fce0c6b2b..a4c297c624e74 100644 --- a/packages/medusa/src/api/store/orders/validators.ts +++ b/packages/medusa/src/api/store/orders/validators.ts @@ -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(), +})