From e5717461cfa17b023f19faf34bedc9cc70653c61 Mon Sep 17 00:00:00 2001 From: Asis Ferrer Date: Tue, 16 Jun 2026 21:21:17 +0200 Subject: [PATCH] feat: add unified Person+User creation backend --- src/actions/person.actions.ts | 52 ++++ src/actions/user.messages.ts | 64 +++++ src/i18n/dictionaries/en.ts | 1 + src/i18n/dictionaries/es.ts | 1 + src/schemas/person.schema.ts | 2 +- src/schemas/user.schema.ts | 44 +++ src/use-cases/person.use-cases.ts | 82 ++++++ .../use-cases/person-user.use-cases.test.ts | 261 +++++++++++++++++ tests/unit/app/users/user.copy.test.ts | 1 + .../unit/i18n/admin-users-dictionary.test.ts | 2 + .../schemas/unified-create.schema.test.ts | 263 ++++++++++++++++++ 11 files changed, 772 insertions(+), 1 deletion(-) create mode 100644 tests/integration/use-cases/person-user.use-cases.test.ts create mode 100644 tests/unit/schemas/unified-create.schema.test.ts diff --git a/src/actions/person.actions.ts b/src/actions/person.actions.ts index 6b074c1..f3ca6ff 100644 --- a/src/actions/person.actions.ts +++ b/src/actions/person.actions.ts @@ -9,12 +9,20 @@ import { type CreatePersonFormType, type UpdatePersonFormType, } from "@/schemas/person.schema" +import { + buildUnifiedCreateSchema, + type UnifiedCreateFormType, +} from "@/schemas/user.schema" import { createPersonUseCase, + createPersonUserUseCase, updatePersonUseCase, } from "@/use-cases/person.use-cases" import { localizePersonFieldErrors } from "./person.messages" +import { localizeUnifiedCreateFieldErrors } from "./user.messages" + +const PERSON_USER_PATH = "/admin/users" export async function createNewPerson(formData: CreatePersonFormType) { const { dictionary } = await getI18n() @@ -56,6 +64,50 @@ export async function createNewPerson(formData: CreatePersonFormType) { } } +export async function createPersonUserAction( + formData: UnifiedCreateFormType, +) { + const { dictionary } = await getI18n() + const userCopy = dictionary.admin.users + const schemaCopy = { + ...userCopy.schema, + ...dictionary.inventory.people.schema, + } + const validatedFields = buildUnifiedCreateSchema(schemaCopy).safeParse( + formData, + ) + + if (!validatedFields.success) { + return { + success: false, + errors: flattenError(validatedFields.error).fieldErrors, + } + } + + try { + const result = await createPersonUserUseCase(validatedFields.data) + + if (!result.success) { + return { + ...result, + errors: localizeUnifiedCreateFieldErrors( + result.errors, + userCopy.actions, + schemaCopy, + ), + message: userCopy.actions.createFailure, + } + } + + revalidatePath(PERSON_USER_PATH) + + return { success: true, message: userCopy.actions.createSuccess } + } catch (error) { + console.error("Database error:", error) + return { success: false, message: userCopy.actions.createFailure } + } +} + export async function updatePerson(formData: UpdatePersonFormType) { const { dictionary } = await getI18n() const copy = dictionary.inventory.people diff --git a/src/actions/user.messages.ts b/src/actions/user.messages.ts index 22306dd..5e3c52b 100644 --- a/src/actions/user.messages.ts +++ b/src/actions/user.messages.ts @@ -1,4 +1,5 @@ import type { Dictionary } from "@/i18n/dictionaries" +import type { UnifiedSchemaCopy } from "@/schemas/user.schema" type UserActionCopy = Dictionary["admin"]["users"]["actions"] @@ -37,3 +38,66 @@ export function localizeUserFieldErrors( ]), ) } + +type UnifiedCreateActionCopy = Dictionary["admin"]["users"]["actions"] + +const unifiedCreateErrorMessageKeys = { + "Email already exists": "duplicateEmail", +} as const satisfies Record + +function isUnifiedCreateErrorMessage( + message: string, +): message is keyof typeof unifiedCreateErrorMessageKeys { + return message in unifiedCreateErrorMessageKeys +} + +function localizeUnifiedCreateMessage( + message: string, + copy: UnifiedCreateActionCopy, +): string { + if (!isUnifiedCreateErrorMessage(message)) return message + + return copy[unifiedCreateErrorMessageKeys[message]] +} + +export function localizeUnifiedCreateFieldErrors( + errors: FieldErrors | undefined, + copy: UnifiedCreateActionCopy, + schemaCopy: UnifiedSchemaCopy, +): FieldErrors | undefined { + if (!errors) return undefined + + return Object.fromEntries( + Object.entries(errors).map(([field, messages]) => [ + field, + messages.map((message) => { + // Schema-level validation messages come from schemaCopy + if ( + field === "firstName" && + message === schemaCopy.firstNameRequired + ) + return message + if ( + field === "lastName" && + message === schemaCopy.lastNameRequired + ) + return message + if ( + field === "department" && + message === schemaCopy.departmentRequired + ) + return message + if (field === "email" && message === schemaCopy.emailInvalid) + return message + if ( + field === "password" && + message === schemaCopy.passwordMinLength + ) + return message + + // Action-level messages (like "Email already exists") come from action copy + return localizeUnifiedCreateMessage(message, copy) + }), + ]), + ) +} diff --git a/src/i18n/dictionaries/en.ts b/src/i18n/dictionaries/en.ts index 70f41d9..db6a883 100644 --- a/src/i18n/dictionaries/en.ts +++ b/src/i18n/dictionaries/en.ts @@ -473,6 +473,7 @@ export const en = { MANAGER: "Manager", STAFF: "Staff", VIEWER: "Viewer", + NO_USER: "No user account", }, status: { active: "Active", diff --git a/src/i18n/dictionaries/es.ts b/src/i18n/dictionaries/es.ts index cc94d2d..8ea6c26 100644 --- a/src/i18n/dictionaries/es.ts +++ b/src/i18n/dictionaries/es.ts @@ -478,6 +478,7 @@ export const es = { MANAGER: "Gerente", STAFF: "Personal", VIEWER: "Visor", + NO_USER: "Sin cuenta de usuario", }, status: { active: "Activo", diff --git a/src/schemas/person.schema.ts b/src/schemas/person.schema.ts index 4bd5c36..6b7b48b 100644 --- a/src/schemas/person.schema.ts +++ b/src/schemas/person.schema.ts @@ -13,7 +13,7 @@ const defaultPersonSchemaCopy: PersonSchemaCopy = { userIdInvalid: "User ID must be a valid UUID", } -const personDepartments = [ +export const personDepartments = [ "IT", "ENGINEERING", "TRAFFIC", diff --git a/src/schemas/user.schema.ts b/src/schemas/user.schema.ts index 205ee8a..1954aa2 100644 --- a/src/schemas/user.schema.ts +++ b/src/schemas/user.schema.ts @@ -1,9 +1,13 @@ import { z } from "zod" import type { Dictionary } from "@/i18n/dictionaries" +import { personDepartments } from "@/schemas/person.schema" export type UserSchemaCopy = Dictionary["admin"]["users"]["schema"] +export type UnifiedSchemaCopy = Dictionary["admin"]["users"]["schema"] & + Dictionary["inventory"]["people"]["schema"] + export const defaultUserSchemaCopy: UserSchemaCopy = { nameRequired: "Name is required", emailInvalid: "Invalid email", @@ -67,3 +71,43 @@ export type CreateUserFormType = z.infer export type UpdateUserFormType = z.infer export type SetUserActiveFormType = z.infer export type ResetUserPasswordFormType = z.infer + +export const unifiedFormRoleSchema = z.enum([ + "ADMIN", + "MANAGER", + "STAFF", + "VIEWER", + "NO_USER", +]) + +export function buildUnifiedCreateSchema(copy: UnifiedSchemaCopy) { + return z + .object({ + firstName: z.string().trim().min(1, { error: copy.firstNameRequired }), + lastName: z.string().trim().min(1, { error: copy.lastNameRequired }), + department: z.enum(personDepartments, { + error: copy.departmentRequired, + }), + email: z.email({ error: copy.emailInvalid }), + phone: z.string().optional().nullable(), + role: unifiedFormRoleSchema, + password: z.string().optional(), + isActive: z.boolean(), + }) + .superRefine((data, ctx) => { + if ( + data.role !== "NO_USER" && + (!data.password || data.password.length < 8) + ) { + ctx.addIssue({ + code: "custom", + message: copy.passwordMinLength, + path: ["password"], + }) + } + }) +} + +export type UnifiedCreateFormType = z.infer< + ReturnType +> diff --git a/src/use-cases/person.use-cases.ts b/src/use-cases/person.use-cases.ts index 6bf30ce..db07684 100644 --- a/src/use-cases/person.use-cases.ts +++ b/src/use-cases/person.use-cases.ts @@ -1,9 +1,12 @@ import { Prisma } from "@/generated/prisma/client" import prisma from "@/lib/prisma" +import { getPasswordHash } from "@/lib/security" import type { CreatePersonFormType, UpdatePersonFormType, } from "@/schemas/person.schema" +import type { UnifiedCreateFormType } from "@/schemas/user.schema" +import { getUserByEmail } from "@/services/user.service" import { PersonService } from "@/services/person.service" type FieldErrors = Record @@ -127,3 +130,82 @@ export async function updatePersonUseCase( throw error } } + +export async function createPersonUserUseCase( + input: UnifiedCreateFormType, +): Promise { + const { firstName, lastName, department, email, phone, role, password, isActive } = + input + + try { + return await prisma.$transaction(async (tx) => { + // Cross-table email uniqueness: check both Person and User tables + const existingPersonEmail = await PersonService.findByEmail(email, tx) + if (existingPersonEmail) { + return personError({ email: ["Email already exists"] }) + } + + const existingUserEmail = await getUserByEmail(email, tx) + if (existingUserEmail) { + return personError({ email: ["Email already exists"] }) + } + + if (role === "NO_USER") { + // Person-only creation — no User record + await PersonService.create( + { + firstName, + lastName, + department, + email, + phone: phone ?? null, + }, + tx, + ) + + return { success: true } + } + + // Person + User creation + const person = await PersonService.create( + { + firstName, + lastName, + department, + email, + phone: phone ?? null, + }, + tx, + ) + + const userName = `${firstName} ${lastName}` + const hashedPassword = await getPasswordHash(password!) + + const user = await tx.user.create({ + data: { + name: userName, + email, + password: hashedPassword, + role, + isActive, + }, + }) + + await PersonService.update( + person.id, + { user: { connect: { id: user.id } } }, + tx, + ) + + return { success: true } + }) + } catch (error) { + const errors = uniqueErrorFor(error) + + if (errors) { + return personError(errors) + } + + throw error + } +} diff --git a/tests/integration/use-cases/person-user.use-cases.test.ts b/tests/integration/use-cases/person-user.use-cases.test.ts new file mode 100644 index 0000000..c896aba --- /dev/null +++ b/tests/integration/use-cases/person-user.use-cases.test.ts @@ -0,0 +1,261 @@ +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest" +import type { PrismaClient } from "@/generated/prisma/client" +import { createTestPerson, createTestUser } from "../helpers/factories" +import { + resetIntegrationTestDatabase, + startIntegrationTestDatabase, + stopIntegrationTestDatabase, +} from "../helpers/test-db" + +let prisma: PrismaClient +let createPersonUserUseCase: typeof import("@/use-cases/person.use-cases").createPersonUserUseCase + +beforeAll(async () => { + await startIntegrationTestDatabase() + + const prismaModule = await import("@/lib/prisma") + const personUseCases = await import("@/use-cases/person.use-cases") + + prisma = prismaModule.prisma + createPersonUserUseCase = personUseCases.createPersonUserUseCase +}) + +beforeEach(async () => { + await resetIntegrationTestDatabase(prisma) +}) + +afterAll(async () => { + await prisma?.$disconnect() + await stopIntegrationTestDatabase() +}) + +describe("createPersonUserUseCase", () => { + describe("NO_USER role (person-only creation)", () => { + it("creates a Person without a User record when role is NO_USER", async () => { + const result = await createPersonUserUseCase({ + firstName: "John", + lastName: "Doe", + department: "IT", + email: "john@example.test", + phone: null, + role: "NO_USER", + isActive: true, + }) + + expect(result).toEqual({ success: true }) + + const person = await prisma.person.findFirstOrThrow({ + where: { firstName: "John", lastName: "Doe" }, + }) + expect(person).toMatchObject({ + firstName: "John", + lastName: "Doe", + department: "IT", + email: "john@example.test", + phone: null, + userId: null, + }) + + // No User record created + await expect( + prisma.user.findUnique({ where: { email: "john@example.test" } }), + ).resolves.toBeNull() + }) + + it("creates a Person with null email when not providing email and role is NO_USER", async () => { + const result = await createPersonUserUseCase({ + firstName: "Jane", + lastName: "Smith", + department: "ENGINEERING", + email: "jane-noemail@example.test", + phone: "555-1234", + role: "NO_USER", + isActive: true, + }) + + expect(result).toEqual({ success: true }) + + const person = await prisma.person.findFirstOrThrow({ + where: { firstName: "Jane", lastName: "Smith" }, + }) + expect(person.phone).toBe("555-1234") + }) + }) + + describe("real role (person + user creation)", () => { + it("creates Person and User with linked userId when role is ADMIN", async () => { + const result = await createPersonUserUseCase({ + firstName: "Admin", + lastName: "User", + department: "IT", + email: "admin@example.test", + phone: null, + role: "ADMIN", + password: "secure-password", + isActive: true, + }) + + expect(result).toEqual({ success: true }) + + const person = await prisma.person.findFirstOrThrow({ + where: { firstName: "Admin", lastName: "User" }, + }) + expect(person).toMatchObject({ + firstName: "Admin", + lastName: "User", + department: "IT", + email: "admin@example.test", + }) + + // User record should exist with derived name + expect(person.userId).not.toBeNull() + + const user = await prisma.user.findUniqueOrThrow({ + where: { id: person.userId! }, + }) + expect(user).toMatchObject({ + name: "Admin User", + email: "admin@example.test", + role: "ADMIN", + isActive: true, + }) + }) + + it("creates Person and User for all real roles (MANAGER, STAFF, VIEWER)", async () => { + const roles = ["MANAGER", "STAFF", "VIEWER"] as const + + for (const role of roles) { + const suffix = role.toLowerCase() + const result = await createPersonUserUseCase({ + firstName: "Person", + lastName: suffix, + department: "IT", + email: `${suffix}@example.test`, + phone: null, + role, + password: "secure-password", + isActive: true, + }) + + expect(result).toEqual({ success: true }) + + const person = await prisma.person.findFirstOrThrow({ + where: { lastName: suffix }, + }) + expect(person.userId).not.toBeNull() + + const user = await prisma.user.findUniqueOrThrow({ + where: { id: person.userId! }, + }) + expect(user.role).toBe(role) + expect(user.name).toBe(`Person ${suffix}`) + } + }) + + it("derives User.name from firstName + lastName", async () => { + await createPersonUserUseCase({ + firstName: "Maria", + lastName: "Garcia", + department: "SALES", + email: "maria@example.test", + phone: null, + role: "STAFF", + password: "secure-password", + isActive: true, + }) + + const user = await prisma.user.findUniqueOrThrow({ + where: { email: "maria@example.test" }, + }) + expect(user.name).toBe("Maria Garcia") + }) + + it("hashes the password when creating a User", async () => { + await createPersonUserUseCase({ + firstName: "Hash", + lastName: "Test", + department: "IT", + email: "hash-test@example.test", + phone: null, + role: "STAFF", + password: "plaintext-password", + isActive: true, + }) + + const user = await prisma.user.findUniqueOrThrow({ + where: { email: "hash-test@example.test" }, + }) + expect(user.password).not.toBe("plaintext-password") + + const { verifyPassword } = await import("@/lib/security") + await expect( + verifyPassword("plaintext-password", user.password), + ).resolves.toBe(true) + }) + }) + + describe("cross-table email uniqueness", () => { + it("rejects submission when email already exists in Person table", async () => { + await createTestPerson(prisma, { email: "existing-person@example.test" }) + + const result = await createPersonUserUseCase({ + firstName: "Duplicate", + lastName: "Person", + department: "IT", + email: "existing-person@example.test", + phone: null, + role: "NO_USER", + isActive: true, + }) + + expect(result).toEqual({ + success: false, + errors: { email: ["Email already exists"] }, + }) + + await expect(prisma.person.count()).resolves.toBe(1) + }) + + it("rejects submission when email already exists in User table", async () => { + await createTestUser(prisma, { email: "existing-user@example.test" }) + + const result = await createPersonUserUseCase({ + firstName: "Duplicate", + lastName: "User", + department: "IT", + email: "existing-user@example.test", + phone: null, + role: "STAFF", + password: "secure-password", + isActive: true, + }) + + expect(result).toEqual({ + success: false, + errors: { email: ["Email already exists"] }, + }) + + // No new Person or User was created + await expect(prisma.person.count()).resolves.toBe(0) + await expect(prisma.user.count()).resolves.toBe(1) + }) + + it("accepts submission when email is unique across both tables", async () => { + // Create a Person and a User with different emails + await createTestPerson(prisma, { email: "person@example.test" }) + await createTestUser(prisma, { email: "user@example.test" }) + + const result = await createPersonUserUseCase({ + firstName: "New", + lastName: "Person", + department: "IT", + email: "new@example.test", + phone: null, + role: "NO_USER", + isActive: true, + }) + + expect(result).toEqual({ success: true }) + }) + }) +}) \ No newline at end of file diff --git a/tests/unit/app/users/user.copy.test.ts b/tests/unit/app/users/user.copy.test.ts index 0fcbdd2..f491247 100644 --- a/tests/unit/app/users/user.copy.test.ts +++ b/tests/unit/app/users/user.copy.test.ts @@ -8,6 +8,7 @@ describe("user copy helpers", () => { MANAGER: "Gerente", STAFF: "Personal", VIEWER: "Visor", + NO_USER: "Sin cuenta de usuario", } const fallbackCopy = { diff --git a/tests/unit/i18n/admin-users-dictionary.test.ts b/tests/unit/i18n/admin-users-dictionary.test.ts index df2f240..b0c227f 100644 --- a/tests/unit/i18n/admin-users-dictionary.test.ts +++ b/tests/unit/i18n/admin-users-dictionary.test.ts @@ -50,6 +50,7 @@ describe("admin users dictionary", () => { MANAGER: "Manager", STAFF: "Staff", VIEWER: "Viewer", + NO_USER: "No user account", }) expect(users.status).toEqual({ @@ -133,6 +134,7 @@ describe("admin users dictionary", () => { MANAGER: "Gerente", STAFF: "Personal", VIEWER: "Visor", + NO_USER: "Sin cuenta de usuario", }) expect(users.status).toEqual({ diff --git a/tests/unit/schemas/unified-create.schema.test.ts b/tests/unit/schemas/unified-create.schema.test.ts new file mode 100644 index 0000000..a9cfa47 --- /dev/null +++ b/tests/unit/schemas/unified-create.schema.test.ts @@ -0,0 +1,263 @@ +import { describe, expect, it } from "vitest" + +import { + buildUnifiedCreateSchema, + unifiedFormRoleSchema, + type UnifiedSchemaCopy, +} from "@/schemas/user.schema" + +const enCopy: UnifiedSchemaCopy = { + firstNameRequired: "First name is required", + lastNameRequired: "Last name is required", + departmentRequired: "Department is required", + emailInvalid: "Invalid email", + passwordMinLength: "Password must be at least 8 characters", + nameRequired: "Name is required", + userIdRequired: "User id is required", + idRequired: "ID is required", + userIdInvalid: "User ID must be a valid UUID", +} + +const esCopy: UnifiedSchemaCopy = { + firstNameRequired: "El nombre es obligatorio", + lastNameRequired: "El apellido es obligatorio", + departmentRequired: "El departamento es obligatorio", + emailInvalid: "Correo electrónico no válido", + passwordMinLength: "La contraseña debe tener al menos 8 caracteres", + nameRequired: "El nombre es obligatorio", + userIdRequired: "El ID de usuario es obligatorio", + idRequired: "El ID es obligatorio", + userIdInvalid: "El ID de usuario debe ser un UUID válido", +} + +const validPersonOnlyData = { + firstName: "John", + lastName: "Doe", + department: "IT", + email: "john@example.test", + phone: null, + role: "NO_USER" as const, + password: undefined, + isActive: true, +} + +const validPersonWithUserData = { + firstName: "Jane", + lastName: "Smith", + department: "ENGINEERING", + email: "jane@example.test", + phone: "1234567890", + role: "ADMIN" as const, + password: "securepassword", + isActive: true, +} + +describe("unifiedFormRoleSchema", () => { + it("accepts all standard roles plus NO_USER", () => { + const roles = ["ADMIN", "MANAGER", "STAFF", "VIEWER", "NO_USER"] + for (const role of roles) { + expect(unifiedFormRoleSchema.safeParse(role).success).toBe(true) + } + }) + + it("rejects invalid roles", () => { + const result = unifiedFormRoleSchema.safeParse("SUPER_ADMIN") + expect(result.success).toBe(false) + }) +}) + +describe("buildUnifiedCreateSchema", () => { + describe("with NO_USER role (person-only creation)", () => { + it("accepts valid data without password when role is NO_USER", () => { + const schema = buildUnifiedCreateSchema(enCopy) + const result = schema.safeParse(validPersonOnlyData) + + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.firstName).toBe("John") + expect(result.data.lastName).toBe("Doe") + expect(result.data.role).toBe("NO_USER") + expect(result.data.password).toBeUndefined() + } + }) + + it("accepts valid data with empty string password when role is NO_USER", () => { + const schema = buildUnifiedCreateSchema(enCopy) + const result = schema.safeParse({ + ...validPersonOnlyData, + password: "", + }) + + expect(result.success).toBe(true) + }) + + it("uses localized error messages for required fields", () => { + const schema = buildUnifiedCreateSchema(esCopy) + const result = schema.safeParse({ + firstName: "", + lastName: "", + department: "", + email: "not-an-email", + role: "NO_USER", + phone: null, + isActive: true, + }) + + expect(result.success).toBe(false) + if (!result.success) { + const errors = result.error.flatten().fieldErrors + expect(errors.firstName).toContain(esCopy.firstNameRequired) + expect(errors.lastName).toContain(esCopy.lastNameRequired) + expect(errors.department).toContain(esCopy.departmentRequired) + expect(errors.email).toContain(esCopy.emailInvalid) + } + }) + }) + + describe("with a real User role (person + user creation)", () => { + it("accepts valid data with password when role is ADMIN", () => { + const schema = buildUnifiedCreateSchema(enCopy) + const result = schema.safeParse(validPersonWithUserData) + + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.firstName).toBe("Jane") + expect(result.data.role).toBe("ADMIN") + expect(result.data.password).toBe("securepassword") + } + }) + + it("accepts valid data with password for all real roles", () => { + const schema = buildUnifiedCreateSchema(enCopy) + for (const role of ["ADMIN", "MANAGER", "STAFF", "VIEWER"] as const) { + const result = schema.safeParse({ + ...validPersonWithUserData, + role, + }) + expect(result.success).toBe(true) + } + }) + + it("rejects short password when role is not NO_USER", () => { + const schema = buildUnifiedCreateSchema(enCopy) + const result = schema.safeParse({ + ...validPersonWithUserData, + password: "short", + }) + + expect(result.success).toBe(false) + if (!result.success) { + const errors = result.error.flatten().fieldErrors + expect(errors.password).toContain(enCopy.passwordMinLength) + } + }) + + it("rejects missing password when role is not NO_USER", () => { + const schema = buildUnifiedCreateSchema(enCopy) + const result = schema.safeParse({ + ...validPersonWithUserData, + password: undefined, + }) + + expect(result.success).toBe(false) + if (!result.success) { + const errors = result.error.flatten().fieldErrors + expect(errors.password).toContain(enCopy.passwordMinLength) + } + }) + + it("rejects empty string password when role is not NO_USER", () => { + const schema = buildUnifiedCreateSchema(enCopy) + const result = schema.safeParse({ + ...validPersonWithUserData, + password: "", + }) + + expect(result.success).toBe(false) + if (!result.success) { + const errors = result.error.flatten().fieldErrors + expect(errors.password).toContain(enCopy.passwordMinLength) + } + }) + + it("uses localized password error message", () => { + const schema = buildUnifiedCreateSchema(esCopy) + const result = schema.safeParse({ + firstName: "Jane", + lastName: "Smith", + department: "ENGINEERING", + email: "jane@example.test", + role: "ADMIN", + password: "corta", + phone: null, + isActive: true, + }) + + expect(result.success).toBe(false) + if (!result.success) { + const errors = result.error.flatten().fieldErrors + expect(errors.password).toContain(esCopy.passwordMinLength) + } + }) + }) + + describe("email validation", () => { + it("rejects invalid email format", () => { + const schema = buildUnifiedCreateSchema(enCopy) + const result = schema.safeParse({ + ...validPersonOnlyData, + email: "not-an-email", + }) + + expect(result.success).toBe(false) + if (!result.success) { + expect(result.error.flatten().fieldErrors.email).toContain( + enCopy.emailInvalid, + ) + } + }) + + it("accepts valid email", () => { + const schema = buildUnifiedCreateSchema(enCopy) + const result = schema.safeParse({ + ...validPersonOnlyData, + email: "valid@example.com", + }) + + expect(result.success).toBe(true) + }) + }) + + describe("department validation", () => { + it("rejects invalid department", () => { + const schema = buildUnifiedCreateSchema(enCopy) + const result = schema.safeParse({ + ...validPersonOnlyData, + department: "INVALID_DEPT", + }) + + expect(result.success).toBe(false) + }) + + it("accepts valid departments", () => { + const schema = buildUnifiedCreateSchema(enCopy) + const validDepartments = [ + "IT", + "ENGINEERING", + "TRAFFIC", + "DRIVER", + "LOGISTICS", + "ADMINISTRATION", + "SALES", + "OTHER", + ] + for (const dept of validDepartments) { + const result = schema.safeParse({ + ...validPersonOnlyData, + department: dept, + }) + expect(result.success).toBe(true) + } + }) + }) +}) \ No newline at end of file