From cbb0a6adc7762f508aa06f6a9186cff7ba6049ed Mon Sep 17 00:00:00 2001 From: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Sun, 1 Sep 2024 11:41:39 +0200 Subject: [PATCH] fix: Customer registration (#8896) * fix: Customer registration * update test * one mroe test * chore: add transformation --- .../__tests__/customer/store/customer.spec.ts | 196 ++++++++++++++++++ .../validate-customer-account-creation.ts | 45 ++++ .../workflows/create-customer-account.ts | 22 +- packages/core/types/src/customer/mutations.ts | 5 + .../api/admin/fulfillment-providers/route.ts | 2 +- .../src/api/store/customers/query-config.ts | 1 + .../medusa/src/api/store/customers/route.ts | 8 +- 7 files changed, 269 insertions(+), 10 deletions(-) create mode 100644 integration-tests/http/__tests__/customer/store/customer.spec.ts create mode 100644 packages/core/core-flows/src/customer/steps/validate-customer-account-creation.ts diff --git a/integration-tests/http/__tests__/customer/store/customer.spec.ts b/integration-tests/http/__tests__/customer/store/customer.spec.ts new file mode 100644 index 0000000000..cbc864ed70 --- /dev/null +++ b/integration-tests/http/__tests__/customer/store/customer.spec.ts @@ -0,0 +1,196 @@ +import { MedusaContainer } from "@medusajs/types" +import { medusaIntegrationTestRunner } from "medusa-test-utils" +import { + adminHeaders, + createAdminUser, +} from "../../../../helpers/create-admin-user" + +jest.setTimeout(30000) + +medusaIntegrationTestRunner({ + testSuite: ({ dbConnection, api, getContainer }) => { + let appContainer: MedusaContainer + + beforeEach(async () => { + appContainer = getContainer() + await createAdminUser(dbConnection, adminHeaders, appContainer) + }) + + describe("POST /admin/customers", () => { + it("should fails to create a customer without an identity", async () => { + const customer = await api + .post("/store/customers", { + email: "newcustomer@medusa.js", + first_name: "John", + last_name: "Doe", + }) + .catch((e) => e) + + expect(customer.response.status).toEqual(401) + }) + + it("should successfully create a customer with an identity", async () => { + const signup = await api.post("/auth/customer/emailpass/register", { + email: "newcustomer@medusa.js", + password: "secret_password", + }) + + expect(signup.status).toEqual(200) + expect(signup.data).toEqual({ token: expect.any(String) }) + + const customer = await api.post( + "/store/customers", + { + email: "newcustomer@medusa.js", + first_name: "John", + last_name: "Doe", + }, + { + headers: { + authorization: `Bearer ${signup.data.token}`, + }, + } + ) + + expect(customer.status).toEqual(200) + expect(customer.data).toEqual({ + customer: expect.objectContaining({ + email: "newcustomer@medusa.js", + first_name: "John", + last_name: "Doe", + has_account: true, + }), + }) + }) + + it("should successfully create a customer with an identity even if the email is already taken by a non-registered customer", async () => { + const nonRegisteredCustomer = await api.post( + "/admin/customers", + { + email: "newcustomer@medusa.js", + first_name: "John", + last_name: "Doe", + }, + adminHeaders + ) + + expect(nonRegisteredCustomer.status).toEqual(200) + expect(nonRegisteredCustomer.data).toEqual({ + customer: expect.objectContaining({ + email: "newcustomer@medusa.js", + first_name: "John", + last_name: "Doe", + has_account: false, + }), + }) + + const signup = await api.post("/auth/customer/emailpass/register", { + email: "newcustomer@medusa.js", + password: "secret_password", + }) + + expect(signup.status).toEqual(200) + expect(signup.data).toEqual({ token: expect.any(String) }) + + const customer = await api.post( + "/store/customers", + { + email: "newcustomer@medusa.js", + first_name: "Jane", + last_name: "Doe", + }, + { + headers: { + authorization: `Bearer ${signup.data.token}`, + }, + } + ) + + expect(customer.status).toEqual(200) + expect(customer.data).toEqual({ + customer: expect.objectContaining({ + email: "newcustomer@medusa.js", + first_name: "Jane", + last_name: "Doe", + has_account: true, + }), + }) + + // Check that customers co-exist + const customers = await api.get("/admin/customers", adminHeaders) + + expect(customers.status).toEqual(200) + expect(customers.data.customers).toHaveLength(2) + expect(customers.data.customers).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + first_name: "Jane", + last_name: "Doe", + email: "newcustomer@medusa.js", + has_account: true, + }), + expect.objectContaining({ + first_name: "John", + last_name: "Doe", + email: "newcustomer@medusa.js", + has_account: false, + }), + ]) + ) + }) + + it("should fail to create a customer with an identity when the email is already taken by a registered customer", async () => { + const firstSignup = await api.post( + "/auth/customer/emailpass/register", + { + email: "newcustomer@medusa.js", + password: "secret_password", + } + ) + + expect(firstSignup.status).toEqual(200) + expect(firstSignup.data).toEqual({ token: expect.any(String) }) + + await api.post( + "/store/customers", + { + email: "newcustomer@medusa.js", + first_name: "John", + last_name: "Doe", + }, + { + headers: { + authorization: `Bearer ${firstSignup.data.token}`, + }, + } + ) + + const firstSignin = await api.post("/auth/customer/emailpass", { + email: "newcustomer@medusa.js", + password: "secret_password", + }) + + const customer = await api + .post( + "/store/customers", + { + email: "newcustomer@medusa.js", + first_name: "Jane", + last_name: "Doe", + }, + { + headers: { + authorization: `Bearer ${firstSignin.data.token}`, + }, + } + ) + .catch((e) => e) + + expect(customer.response.status).toEqual(400) + expect(customer.response.data.message).toEqual( + "Request already authenticated as a customer." + ) + }) + }) + }, +}) diff --git a/packages/core/core-flows/src/customer/steps/validate-customer-account-creation.ts b/packages/core/core-flows/src/customer/steps/validate-customer-account-creation.ts new file mode 100644 index 0000000000..01e4beb79a --- /dev/null +++ b/packages/core/core-flows/src/customer/steps/validate-customer-account-creation.ts @@ -0,0 +1,45 @@ +import { MedusaError, ModuleRegistrationName } from "@medusajs/utils" +import { createStep } from "@medusajs/workflows-sdk" +import { CreateCustomerAccountWorkflowInput } from "../workflows" + +export const validateCustomerAccountCreationStepId = + "validate-customer-account-creation" + +export const validateCustomerAccountCreation = createStep( + validateCustomerAccountCreationStepId, + async (input: CreateCustomerAccountWorkflowInput, { container }) => { + const customerService = container.resolve(ModuleRegistrationName.CUSTOMER) + + const { email } = input.customerData + + if (!email) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Email is required to create a customer" + ) + } + + // Check if customer with email already exists + const existingCustomers = await customerService.listCustomers({ email }) + + if (existingCustomers?.length) { + const hasExistingAccount = existingCustomers.some( + (customer) => customer.has_account + ) + + if (hasExistingAccount && input.authIdentityId) { + throw new MedusaError( + MedusaError.Types.DUPLICATE_ERROR, + "Customer with this email already has an account" + ) + } + + if (!hasExistingAccount && !input.authIdentityId) { + throw new MedusaError( + MedusaError.Types.DUPLICATE_ERROR, + "Guest customer with this email already exists" + ) + } + } + } +) diff --git a/packages/core/core-flows/src/customer/workflows/create-customer-account.ts b/packages/core/core-flows/src/customer/workflows/create-customer-account.ts index 269fc71a34..18c80b3f40 100644 --- a/packages/core/core-flows/src/customer/workflows/create-customer-account.ts +++ b/packages/core/core-flows/src/customer/workflows/create-customer-account.ts @@ -1,16 +1,17 @@ import { CreateCustomerDTO, CustomerDTO } from "@medusajs/types" import { createWorkflow, + transform, WorkflowData, WorkflowResponse, } from "@medusajs/workflows-sdk" -import { createCustomersStep } from "../steps" -import { transform } from "@medusajs/workflows-sdk" import { setAuthAppMetadataStep } from "../../auth" +import { createCustomersStep } from "../steps" +import { validateCustomerAccountCreation } from "../steps/validate-customer-account-creation" export type CreateCustomerAccountWorkflowInput = { authIdentityId: string - customersData: CreateCustomerDTO + customerData: CreateCustomerDTO } export const createCustomerAccountWorkflowId = "create-customer-account" @@ -19,8 +20,19 @@ export const createCustomerAccountWorkflowId = "create-customer-account" */ export const createCustomerAccountWorkflow = createWorkflow( createCustomerAccountWorkflowId, - (input: WorkflowData): WorkflowResponse => { - const customers = createCustomersStep([input.customersData]) + ( + input: WorkflowData + ): WorkflowResponse => { + validateCustomerAccountCreation(input) + + const customerData = transform({ input }, (data) => { + return { + ...data.input.customerData, + has_account: !!data.input.authIdentityId, + } + }) + + const customers = createCustomersStep([customerData]) const customer = transform( customers, diff --git a/packages/core/types/src/customer/mutations.ts b/packages/core/types/src/customer/mutations.ts index e88cef8f6c..b023801025 100644 --- a/packages/core/types/src/customer/mutations.ts +++ b/packages/core/types/src/customer/mutations.ts @@ -199,6 +199,11 @@ export interface CreateCustomerDTO { */ created_by?: string | null + /** + * Whether the customer has an account. + */ + has_account?: boolean + /** * The addresses of the customer. */ diff --git a/packages/medusa/src/api/admin/fulfillment-providers/route.ts b/packages/medusa/src/api/admin/fulfillment-providers/route.ts index 1c47545ca9..aec8bcc402 100644 --- a/packages/medusa/src/api/admin/fulfillment-providers/route.ts +++ b/packages/medusa/src/api/admin/fulfillment-providers/route.ts @@ -1,3 +1,4 @@ +import { HttpTypes } from "@medusajs/types" import { ContainerRegistrationKeys, remoteQueryObjectFromString, @@ -6,7 +7,6 @@ import { AuthenticatedMedusaRequest, MedusaResponse, } from "../../../types/routing" -import { HttpTypes } from "@medusajs/types" export const GET = async ( req: AuthenticatedMedusaRequest, diff --git a/packages/medusa/src/api/store/customers/query-config.ts b/packages/medusa/src/api/store/customers/query-config.ts index 783963ce79..92b3df7885 100644 --- a/packages/medusa/src/api/store/customers/query-config.ts +++ b/packages/medusa/src/api/store/customers/query-config.ts @@ -6,6 +6,7 @@ const defaultStoreCustomersFields = [ "last_name", "phone", "metadata", + "has_account", "created_by", "deleted_at", "created_at", diff --git a/packages/medusa/src/api/store/customers/route.ts b/packages/medusa/src/api/store/customers/route.ts index f8533e8a97..64f08495fb 100644 --- a/packages/medusa/src/api/store/customers/route.ts +++ b/packages/medusa/src/api/store/customers/route.ts @@ -1,12 +1,12 @@ +import { MedusaError } from "@medusajs/utils" import { AuthenticatedMedusaRequest, MedusaResponse, } from "../../../types/routing" -import { MedusaError } from "@medusajs/utils" import { createCustomerAccountWorkflow } from "@medusajs/core-flows" -import { refetchCustomer } from "./helpers" import { HttpTypes } from "@medusajs/types" +import { refetchCustomer } from "./helpers" export const POST = async ( req: AuthenticatedMedusaRequest, @@ -21,10 +21,10 @@ export const POST = async ( } const createCustomers = createCustomerAccountWorkflow(req.scope) - const customersData = req.validatedBody + const customerData = req.validatedBody const { result } = await createCustomers.run({ - input: { customersData, authIdentityId: req.auth_context.auth_identity_id }, + input: { customerData, authIdentityId: req.auth_context.auth_identity_id }, }) const customer = await refetchCustomer(