From 8055a79c5268528261c88d5f6f245d1003d87a6d Mon Sep 17 00:00:00 2001 From: Nicolas Gorga <62995075+NicolasGorga@users.noreply.github.com> Date: Mon, 5 Jan 2026 12:12:42 -0300 Subject: [PATCH] fix(core-flows): conditionally create customer on order email update if unset (#14264) * Find or create customer on order email update when order email is unset * Tests * Add changeset --- .changeset/quick-candles-leave.md | 5 + .../http/__tests__/fixtures/order.ts | 6 +- .../http/__tests__/order/admin/order.spec.ts | 214 +++++++++++++++++- .../src/order/workflows/update-order.ts | 60 +++-- 4 files changed, 260 insertions(+), 25 deletions(-) create mode 100644 .changeset/quick-candles-leave.md diff --git a/.changeset/quick-candles-leave.md b/.changeset/quick-candles-leave.md new file mode 100644 index 0000000000..5f01ddf089 --- /dev/null +++ b/.changeset/quick-candles-leave.md @@ -0,0 +1,5 @@ +--- +"@medusajs/core-flows": patch +--- + +fix(core-flows): conditionally create customer on order email update if unset diff --git a/integration-tests/http/__tests__/fixtures/order.ts b/integration-tests/http/__tests__/fixtures/order.ts index 4f1f0bf61b..1c96f19750 100644 --- a/integration-tests/http/__tests__/fixtures/order.ts +++ b/integration-tests/http/__tests__/fixtures/order.ts @@ -6,7 +6,11 @@ import { AdminStockLocation, MedusaContainer, } from "@medusajs/types" -import { ContainerRegistrationKeys, Modules, ProductStatus } from "@medusajs/utils" +import { + ContainerRegistrationKeys, + Modules, + ProductStatus, +} from "@medusajs/utils" import { adminHeaders, generatePublishableKey, diff --git a/integration-tests/http/__tests__/order/admin/order.spec.ts b/integration-tests/http/__tests__/order/admin/order.spec.ts index a863e84a5b..6910e4e462 100644 --- a/integration-tests/http/__tests__/order/admin/order.spec.ts +++ b/integration-tests/http/__tests__/order/admin/order.spec.ts @@ -165,7 +165,10 @@ medusaIntegrationTestRunner({ order = seeder.order order = ( - await api.get(`/admin/orders/${order.id}?fields=+email`, adminHeaders) + await api.get( + `/admin/orders/${order.id}?fields=+email,+customer_id`, + adminHeaders + ) ).data.order }) @@ -477,6 +480,215 @@ medusaIntegrationTestRunner({ expect(orderChangesResult.length).toEqual(0) }) + + describe("conditional customer creation", () => { + it("should create or find a customer when order email is unset and input email is provided", async () => { + const container = getContainer() + const orderService = container.resolve(ModuleRegistrationName.ORDER) + const customerService = container.resolve( + ModuleRegistrationName.CUSTOMER + ) + + const orderWithoutEmail = await orderService.createOrders({ + region_id: seeder.region.id, + currency_code: "usd", + items: [ + { + title: "Test Item", + quantity: 1, + unit_price: 100, + }, + ], + shipping_address: { + first_name: "Test", + last_name: "Test", + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + }, + billing_address: { + first_name: "Test", + last_name: "Test", + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + }, + }) + + expect(orderWithoutEmail.email).toBeNull() + + const newEmail = "newcustomer@example.com" + + const customersBefore = await customerService.listCustomers() + const customerCountBefore = customersBefore.length + + const response = await api.post( + `/admin/orders/${orderWithoutEmail.id}?fields=+email,+customer_id`, + { + email: newEmail, + }, + adminHeaders + ) + + expect(response.data.order.email).toBe(newEmail) + expect(response.data.order.customer_id).toBeDefined() + + const customersAfter = await customerService.listCustomers() + expect(customersAfter.length).toBe(customerCountBefore + 1) + + const customer = await customerService.retrieveCustomer( + response.data.order.customer_id + ) + expect(customer.email).toBe(newEmail) + }) + + it("should NOT create or find a customer when order email is already set and input email is provided", async () => { + const container = getContainer() + const customerService = container.resolve( + ModuleRegistrationName.CUSTOMER + ) + + const existingEmail = order.email + + expect(existingEmail).toBeDefined() + + const originalCustomerId = order.customer_id + const newEmail = "updated@example.com" + + const customersBefore = await customerService.listCustomers() + const customerCountBefore = customersBefore.length + + const response = await api.post( + `/admin/orders/${order.id}?fields=+email,+customer_id`, + { + email: newEmail, + }, + adminHeaders + ) + + expect(response.data.order.email).toBe(newEmail) + expect(response.data.order.customer_id).toBe(originalCustomerId) + + const customersAfter = await customerService.listCustomers() + expect(customersAfter.length).toBe(customerCountBefore) + }) + + it("should NOT create or find a customer when order email is unset and input email is not provided", async () => { + const container = getContainer() + const orderService = container.resolve(ModuleRegistrationName.ORDER) + const customerService = container.resolve( + ModuleRegistrationName.CUSTOMER + ) + + const orderWithoutEmail = await orderService.createOrders({ + region_id: seeder.region.id, + currency_code: "usd", + email: undefined, + items: [ + { + title: "Test Item", + quantity: 1, + unit_price: 100, + }, + ], + shipping_address: { + first_name: "Test", + last_name: "Test", + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + }, + billing_address: { + first_name: "Test", + last_name: "Test", + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + }, + }) + + const customersBefore = await customerService.listCustomers() + const customerCountBefore = customersBefore.length + + const response = await api.post( + `/admin/orders/${orderWithoutEmail.id}?fields=+customer_id`, + { + metadata: { + test: "value", + }, + }, + adminHeaders + ) + + expect(response.data.order.customer_id).toBeNull() + + const customersAfter = await customerService.listCustomers() + expect(customersAfter.length).toBe(customerCountBefore) + }) + + it("should find existing customer when order email is unset and input email matches existing customer", async () => { + const container = getContainer() + const orderService = container.resolve(ModuleRegistrationName.ORDER) + const customerService = container.resolve( + ModuleRegistrationName.CUSTOMER + ) + + const existingEmail = "existingcustomer@example.com" + + const existingCustomer = await customerService.createCustomers({ + email: existingEmail, + }) + + const orderWithoutEmail = await orderService.createOrders({ + region_id: seeder.region.id, + currency_code: "usd", + items: [ + { + title: "Test Item", + quantity: 1, + unit_price: 100, + }, + ], + shipping_address: { + first_name: "Test", + last_name: "Test", + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + }, + billing_address: { + first_name: "Test", + last_name: "Test", + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + }, + }) + + const customersBefore = await customerService.listCustomers() + const customerCountBefore = customersBefore.length + + const response = await api.post( + `/admin/orders/${orderWithoutEmail.id}?fields=+email,+customer_id`, + { + email: existingEmail, + }, + adminHeaders + ) + + expect(response.data.order.email).toBe(existingEmail) + expect(response.data.order.customer_id).toBe(existingCustomer.id) + + const customersAfter = await customerService.listCustomers() + expect(customersAfter.length).toBe(customerCountBefore) + }) + }) }) describe("POST /orders/:id/cancel", () => { diff --git a/packages/core/core-flows/src/order/workflows/update-order.ts b/packages/core/core-flows/src/order/workflows/update-order.ts index c17f29e250..7a4dd38e45 100644 --- a/packages/core/core-flows/src/order/workflows/update-order.ts +++ b/packages/core/core-flows/src/order/workflows/update-order.ts @@ -28,6 +28,7 @@ import { updateOrdersStep, } from "../steps" import { throwIfOrderIsCancelled } from "../utils/order-validation" +import { findOrCreateCustomerStep } from "../../cart" /** * The data to validate the order update. @@ -151,31 +152,44 @@ export const updateOrderWorkflow = createWorkflow( updateOrderValidationStep({ order, input }) - const updateInput = transform({ input, order }, ({ input, order }) => { - const update: UpdateOrderDTO = {} - - if (input.shipping_address) { - const address = { - // we want to create a new address - ...order.shipping_address, - ...input.shipping_address, - } - delete address.id - update.shipping_address = address - } - - if (input.billing_address) { - const address = { - ...order.billing_address, - ...input.billing_address, - } - delete address.id - update.billing_address = address - } - - return { ...input, ...update } + const customerData = when({ input, order }, ({ input, order }) => { + return !order.email && !!input.email + }).then(() => { + return findOrCreateCustomerStep({ email: input.email }) }) + const updateInput = transform( + { input, order, customerData }, + ({ input, order, customerData }) => { + const update: UpdateOrderDTO = {} + + if (input.shipping_address) { + const address = { + // we want to create a new address + ...order.shipping_address, + ...input.shipping_address, + } + delete address.id + update.shipping_address = address + } + + if (input.billing_address) { + const address = { + ...order.billing_address, + ...input.billing_address, + } + delete address.id + update.billing_address = address + } + + if (!!customerData?.customer) { + update.customer_id = customerData.customer.id + } + + return { ...input, ...update } + } + ) + const updatedOrders = updateOrdersStep({ selector: { id: input.id }, update: updateInput,