diff --git a/prisma/migrations/20260603100143_init/migration.sql b/prisma/migrations/20260603100143_init/migration.sql index 0719e0e..9261850 100644 --- a/prisma/migrations/20260603100143_init/migration.sql +++ b/prisma/migrations/20260603100143_init/migration.sql @@ -2,7 +2,7 @@ CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'MANAGER', 'STAFF', 'VIEWER'); -- CreateEnum -CREATE TYPE "RecipientDepartment" AS ENUM ('IT', 'ENGINEERING', 'LOGISTICS', 'TRAFFIC', 'DRIVER', 'ADMINISTRATION', 'SALES', 'OTHER'); +CREATE TYPE "PersonDepartment" AS ENUM ('IT', 'ENGINEERING', 'LOGISTICS', 'TRAFFIC', 'DRIVER', 'ADMINISTRATION', 'SALES', 'OTHER'); -- CreateEnum CREATE TYPE "ItemStatus" AS ENUM ('AVAILABLE', 'ASSIGNED', 'RESERVED', 'IN_REPAIR', 'BROKEN', 'STOLEN', 'DISPOSED'); @@ -26,19 +26,19 @@ CREATE TABLE "User" ( ); -- CreateTable -CREATE TABLE "Recipient" ( +CREATE TABLE "Person" ( "id" TEXT NOT NULL, - "username" TEXT NOT NULL, "firstName" TEXT NOT NULL, "lastName" TEXT NOT NULL, - "department" "RecipientDepartment", + "department" "PersonDepartment", "email" TEXT, "phone" TEXT, + "userId" TEXT, "isActive" BOOLEAN NOT NULL DEFAULT true, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, - CONSTRAINT "Recipient_pkey" PRIMARY KEY ("id") + CONSTRAINT "Person_pkey" PRIMARY KEY ("id") ); -- CreateTable @@ -126,16 +126,16 @@ CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); -- CreateIndex -CREATE UNIQUE INDEX "Recipient_username_key" ON "Recipient"("username"); +CREATE UNIQUE INDEX "Person_email_key" ON "Person"("email"); -- CreateIndex -CREATE UNIQUE INDEX "Recipient_email_key" ON "Recipient"("email"); +CREATE UNIQUE INDEX "Person_userId_key" ON "Person"("userId"); -- CreateIndex -CREATE INDEX "Recipient_lastName_firstName_idx" ON "Recipient"("lastName", "firstName"); +CREATE INDEX "Person_lastName_firstName_idx" ON "Person"("lastName", "firstName"); -- CreateIndex -CREATE INDEX "Recipient_department_idx" ON "Recipient"("department"); +CREATE INDEX "Person_department_idx" ON "Person"("department"); -- CreateIndex CREATE UNIQUE INDEX "Category_name_key" ON "Category"("name"); @@ -191,6 +191,9 @@ CREATE INDEX "Movement_type_idx" ON "Movement"("type"); -- CreateIndex CREATE INDEX "Movement_userId_idx" ON "Movement"("userId"); +-- AddForeignKey +ALTER TABLE "Person" ADD CONSTRAINT "Person_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + -- AddForeignKey ALTER TABLE "Item" ADD CONSTRAINT "Item_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE; @@ -204,7 +207,7 @@ ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_itemId_fkey" FOREIGN KEY ("i ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "Asset"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; @@ -216,10 +219,10 @@ ALTER TABLE "Movement" ADD CONSTRAINT "Movement_itemId_fkey" FOREIGN KEY ("itemI ALTER TABLE "Movement" ADD CONSTRAINT "Movement_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "Asset"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "Movement" ADD CONSTRAINT "Movement_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "Movement" ADD CONSTRAINT "Movement_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Person"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Movement" ADD CONSTRAINT "Movement_assignmentId_fkey" FOREIGN KEY ("assignmentId") REFERENCES "Assignment"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "Movement" ADD CONSTRAINT "Movement_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "Movement" ADD CONSTRAINT "Movement_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cfe97f3..c398668 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -33,9 +33,10 @@ model User { updatedAt DateTime @updatedAt movements Movement[] assignments Assignment[] + person Person? } -enum RecipientDepartment { +enum PersonDepartment { IT ENGINEERING LOGISTICS @@ -46,17 +47,18 @@ enum RecipientDepartment { OTHER } -model Recipient { - id String @id @default(uuid()) - username String @unique +model Person { + id String @id @default(uuid()) firstName String lastName String - department RecipientDepartment? - email String? @unique + department PersonDepartment? + email String? @unique phone String? - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + userId String? @unique + user User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt assignments Assignment[] movements Movement[] @@ -128,11 +130,11 @@ model Assignment { quantity Int? notes String? itemId String? - item Item? @relation(fields: [itemId], references: [id]) + item Item? @relation(fields: [itemId], references: [id], onDelete: SetNull, onUpdate: Cascade) assetId String? @unique - asset Asset? @relation(fields: [assetId], references: [id]) + asset Asset? @relation(fields: [assetId], references: [id], onDelete: SetNull, onUpdate: Cascade) recipientId String? - recipient Recipient? @relation(fields: [recipientId], references: [id], onDelete: Cascade, onUpdate: Cascade) + recipient Person? @relation(fields: [recipientId], references: [id], onDelete: Cascade, onUpdate: Cascade) assignmentDate DateTime @default(now()) returnDate DateTime? createdBy String @@ -163,15 +165,15 @@ model Movement { details String? notes String? itemId String? - item Item? @relation(fields: [itemId], references: [id]) + item Item? @relation(fields: [itemId], references: [id], onDelete: SetNull, onUpdate: Cascade) assetId String? - asset Asset? @relation(fields: [assetId], references: [id]) + asset Asset? @relation(fields: [assetId], references: [id], onDelete: SetNull, onUpdate: Cascade) previousStock Int? newStock Int? recipientId String? - recipient Recipient? @relation(fields: [recipientId], references: [id]) + recipient Person? @relation(fields: [recipientId], references: [id], onDelete: SetNull, onUpdate: Cascade) assignmentId String? - assignment Assignment? @relation(fields: [assignmentId], references: [id]) + assignment Assignment? @relation(fields: [assignmentId], references: [id], onDelete: SetNull, onUpdate: Cascade) userId String user User @relation(fields: [userId], references: [id]) createdAt DateTime @default(now()) @@ -181,4 +183,4 @@ model Movement { @@index([recipientId]) @@index([type]) @@index([userId]) -} +} \ No newline at end of file diff --git a/src/actions/import.actions.ts b/src/actions/import.actions.ts index b78a030..cdad538 100644 --- a/src/actions/import.actions.ts +++ b/src/actions/import.actions.ts @@ -12,14 +12,14 @@ import { getAuthenticatedUserId } from "@/services/auth.service" import { CategoryService } from "@/services/category.service" import { ItemService } from "@/services/item.service" import { MovementService } from "@/services/movement.service" -import { RecipientService } from "@/services/recipient.service" +import { PersonService } from "@/services/person.service" import type { Asset, Assignment, Category, ImportItem, Item, - Recipient, + Person, } from "@/types" export async function importItems(formData: ImportFormType) { @@ -123,7 +123,7 @@ export async function importItems(formData: ImportFormType) { file: [ "Only one of category or categoryId is allowed, you must select one of them", ], - }, + } } } @@ -153,7 +153,6 @@ export async function importItems(formData: ImportFormType) { category, deliveryNote, assigned, - username, firstName, lastName, } = row @@ -178,10 +177,6 @@ export async function importItems(formData: ImportFormType) { importErrors.push(`Row ${index + 2}: Delivery note must be a string`) } - if (username && typeof username !== "string") { - importErrors.push(`Row ${index + 2}: Username must be a string`) - } - if (firstName && typeof firstName !== "string") { importErrors.push(`Row ${index + 2}: First name must be a string`) } @@ -214,7 +209,6 @@ export async function importItems(formData: ImportFormType) { category: row.category?.trim() || "", deliveryNote: row.deliveryNote?.trim() || "", assigned: row.assigned?.trim() === "true", - username: row.username?.trim() || "", firstName: row.firstName?.trim() || "", lastName: row.lastName?.trim() || "", }) @@ -229,7 +223,6 @@ export async function importItems(formData: ImportFormType) { category, deliveryNote, assigned, - username, firstName, lastName, } = item @@ -238,7 +231,7 @@ export async function importItems(formData: ImportFormType) { let newItem: Item | null = null let newAsset: Asset | null = null let newCategory: Category | null = null - let newRecipient: Recipient | null = null + let newPerson: Person | null = null let newAssignment: Assignment | null = null const existingCategory = categoryId @@ -290,14 +283,16 @@ export async function importItems(formData: ImportFormType) { } if (assigned && firstName && lastName) { - const finalUsername = - username || `${firstName.toLowerCase()[0]}${lastName.toLowerCase()}` - const existingRecipient = - await RecipientService.findByUsername(finalUsername) + const existingPerson = firstName + ? await PersonService.findAllPaginated({ + search: firstName, + page: 0, + pageSize: 1, + }) + : null - if (!existingRecipient) { - newRecipient = await RecipientService.create({ - username: finalUsername, + if (!existingPerson || existingPerson.data.length === 0) { + newPerson = await PersonService.create({ firstName, lastName, email: undefined, @@ -305,7 +300,7 @@ export async function importItems(formData: ImportFormType) { department: "OTHER", }) } else { - newRecipient = existingRecipient + newPerson = existingPerson.data[0] } newAssignment = await AssignmentService.create({ @@ -313,7 +308,7 @@ export async function importItems(formData: ImportFormType) { notes: deliveryNote || "", itemId: newItem?.id || "", assetId: newAsset?.id || "", - recipientId: newRecipient?.id || "", + recipientId: newPerson?.id || "", assignmentDate: new Date(), createdBy: userId, }) @@ -324,15 +319,15 @@ export async function importItems(formData: ImportFormType) { quantity: stock || 1, type: assigned ? "ASSIGNMENT" : "IN", itemId: newItem?.id || undefined, - recipientId: newRecipient?.id || undefined, + recipientId: newPerson?.id || undefined, } if (newAssignment?.id) { movementData.assignmentId = newAssignment.id } - if (newRecipient?.id) { - movementData.recipientId = newRecipient.id + if (newPerson?.id) { + movementData.recipientId = newPerson.id } await MovementService.create({ @@ -347,4 +342,4 @@ export async function importItems(formData: ImportFormType) { success: true, message: "Items imported successfully!", } -} +} \ No newline at end of file diff --git a/src/actions/recipient.actions.ts b/src/actions/person.actions.ts similarity index 54% rename from src/actions/recipient.actions.ts rename to src/actions/person.actions.ts index f4698b0..5c5b553 100644 --- a/src/actions/recipient.actions.ts +++ b/src/actions/person.actions.ts @@ -4,22 +4,22 @@ import { revalidatePath } from "next/cache" import { flattenError } from "zod" import { getI18n } from "@/i18n/server" import { - buildCreateRecipientSchema, - buildUpdateRecipientSchema, - type CreateRecipientFormType, - type UpdateRecipientFormType, -} from "@/schemas/recipient.schema" + buildCreatePersonSchema, + buildUpdatePersonSchema, + type CreatePersonFormType, + type UpdatePersonFormType, +} from "@/schemas/person.schema" import { - createRecipientUseCase, - updateRecipientUseCase, -} from "@/use-cases/recipient.use-cases" + createPersonUseCase, + updatePersonUseCase, +} from "@/use-cases/person.use-cases" -import { localizeRecipientFieldErrors } from "./recipient.messages" +import { localizePersonFieldErrors } from "./person.messages" -export async function createNewRecipient(formData: CreateRecipientFormType) { +export async function createNewPerson(formData: CreatePersonFormType) { const { dictionary } = await getI18n() - const copy = dictionary.inventory.recipients - const validatedFields = buildCreateRecipientSchema(copy.schema).safeParse( + const copy = dictionary.inventory.people + const validatedFields = buildCreatePersonSchema(copy.schema).safeParse( formData, ) @@ -31,17 +31,17 @@ export async function createNewRecipient(formData: CreateRecipientFormType) { } try { - const result = await createRecipientUseCase(validatedFields.data) + const result = await createPersonUseCase(validatedFields.data) if (!result.success) { return { ...result, - errors: localizeRecipientFieldErrors(result.errors, copy.actions), + errors: localizePersonFieldErrors(result.errors, copy.actions), message: copy.actions.createFailure, } } - revalidatePath("/recipients") + revalidatePath("/people") return { success: true, @@ -56,10 +56,10 @@ export async function createNewRecipient(formData: CreateRecipientFormType) { } } -export async function updateRecipient(formData: UpdateRecipientFormType) { +export async function updatePerson(formData: UpdatePersonFormType) { const { dictionary } = await getI18n() - const copy = dictionary.inventory.recipients - const validatedFields = buildUpdateRecipientSchema(copy.schema).safeParse( + const copy = dictionary.inventory.people + const validatedFields = buildUpdatePersonSchema(copy.schema).safeParse( formData, ) @@ -71,17 +71,17 @@ export async function updateRecipient(formData: UpdateRecipientFormType) { } try { - const result = await updateRecipientUseCase(validatedFields.data) + const result = await updatePersonUseCase(validatedFields.data) if (!result.success) { return { ...result, - errors: localizeRecipientFieldErrors(result.errors, copy.actions), + errors: localizePersonFieldErrors(result.errors, copy.actions), message: copy.actions.updateFailure, } } - revalidatePath("/recipients") + revalidatePath("/people") return { success: true, @@ -94,4 +94,4 @@ export async function updateRecipient(formData: UpdateRecipientFormType) { message: copy.actions.updateFailure, } } -} +} \ No newline at end of file diff --git a/src/actions/person.messages.ts b/src/actions/person.messages.ts new file mode 100644 index 0000000..a1f5e8f --- /dev/null +++ b/src/actions/person.messages.ts @@ -0,0 +1,38 @@ +import type { Dictionary } from "@/i18n/dictionaries" + +type PersonActionCopy = Dictionary["inventory"]["people"]["actions"] + +type FieldErrors = Record + +const personErrorMessageKeys = { + "Email already exists": "duplicateEmail", +} as const satisfies Record + +function isPersonErrorMessage( + message: string, +): message is keyof typeof personErrorMessageKeys { + return message in personErrorMessageKeys +} + +function localizePersonMessage( + message: string, + copy: PersonActionCopy, +): string { + if (!isPersonErrorMessage(message)) return message + + return copy[personErrorMessageKeys[message]] +} + +export function localizePersonFieldErrors( + errors: FieldErrors | undefined, + copy: PersonActionCopy, +): FieldErrors | undefined { + if (!errors) return undefined + + return Object.fromEntries( + Object.entries(errors).map(([field, messages]) => [ + field, + messages.map((message) => localizePersonMessage(message, copy)), + ]), + ) +} \ No newline at end of file diff --git a/src/actions/recipient.messages.ts b/src/actions/recipient.messages.ts deleted file mode 100644 index 5dc925c..0000000 --- a/src/actions/recipient.messages.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Dictionary } from "@/i18n/dictionaries" - -type RecipientActionCopy = Dictionary["inventory"]["recipients"]["actions"] - -type FieldErrors = Record - -const recipientErrorMessageKeys = { - "Username already exists": "duplicateUsername", - "Email already exists": "duplicateEmail", -} as const satisfies Record - -function isRecipientErrorMessage( - message: string, -): message is keyof typeof recipientErrorMessageKeys { - return message in recipientErrorMessageKeys -} - -function localizeRecipientMessage( - message: string, - copy: RecipientActionCopy, -): string { - if (!isRecipientErrorMessage(message)) return message - - return copy[recipientErrorMessageKeys[message]] -} - -export function localizeRecipientFieldErrors( - errors: FieldErrors | undefined, - copy: RecipientActionCopy, -): FieldErrors | undefined { - if (!errors) return undefined - - return Object.fromEntries( - Object.entries(errors).map(([field, messages]) => [ - field, - messages.map((message) => localizeRecipientMessage(message, copy)), - ]), - ) -} diff --git a/src/i18n/dictionaries/en.ts b/src/i18n/dictionaries/en.ts index dcc5046..4aba4cf 100644 --- a/src/i18n/dictionaries/en.ts +++ b/src/i18n/dictionaries/en.ts @@ -318,6 +318,80 @@ export const en = { idRequired: "Assignment ID is required", }, }, + people: { + list: { + title: "People", + addLabel: "Add Person", + empty: "No people found.", + columns: { + name: "Name", + email: "Email", + phone: "Phone", + department: "Department", + actions: "Actions", + }, + actions: { + view: "View person", + edit: "Edit person", + }, + }, + detail: { + notFound: "Person not found", + labels: { + email: "Email", + phone: "Phone", + department: "Department", + }, + }, + new: { + title: "Add Person", + }, + edit: { + title: "Edit Person", + notFound: "Person not found", + }, + form: { + firstNameLabel: "First Name", + firstNamePlaceholder: "First name", + lastNameLabel: "Last Name", + lastNamePlaceholder: "Last name", + departmentLabel: "Department", + departmentPlaceholder: "Select a department", + emailLabel: "Email", + emailPlaceholder: "Email", + phoneLabel: "Phone", + phonePlaceholder: "Phone", + createSubmit: "Create Person", + updateSubmit: "Update Person", + }, + fallback: { + unknownDepartment: "Unknown department", + }, + departments: { + IT: "IT", + ENGINEERING: "Engineering", + LOGISTICS: "Logistics", + TRAFFIC: "Traffic", + DRIVER: "Driver", + ADMINISTRATION: "Administration", + SALES: "Sales", + OTHER: "Other", + }, + actions: { + createSuccess: "Person created successfully", + createFailure: "Failed to create person", + updateSuccess: "Person updated successfully", + updateFailure: "Failed to update person", + duplicateEmail: "Email already exists", + }, + schema: { + firstNameRequired: "First name is required", + lastNameRequired: "Last name is required", + departmentRequired: "Department is required", + emailInvalid: "Email format is invalid", + idRequired: "ID is required", + }, + }, recipients: { list: { title: "Recipients", @@ -540,4 +614,4 @@ export const en = { }, } -export type Dictionary = typeof en +export type Dictionary = typeof en \ No newline at end of file diff --git a/src/i18n/dictionaries/es.ts b/src/i18n/dictionaries/es.ts index cdfa4b7..fcf1428 100644 --- a/src/i18n/dictionaries/es.ts +++ b/src/i18n/dictionaries/es.ts @@ -323,6 +323,80 @@ export const es = { idRequired: "El ID de asignación es obligatorio", }, }, + people: { + list: { + title: "Personas", + addLabel: "Agregar persona", + empty: "No se encontraron personas.", + columns: { + name: "Nombre", + email: "Correo electrónico", + phone: "Teléfono", + department: "Departamento", + actions: "Acciones", + }, + actions: { + view: "Ver persona", + edit: "Editar persona", + }, + }, + detail: { + notFound: "Persona no encontrada", + labels: { + email: "Correo electrónico", + phone: "Teléfono", + department: "Departamento", + }, + }, + new: { + title: "Agregar persona", + }, + edit: { + title: "Editar persona", + notFound: "Persona no encontrada", + }, + form: { + firstNameLabel: "Nombre", + firstNamePlaceholder: "Nombre", + lastNameLabel: "Apellido", + lastNamePlaceholder: "Apellido", + departmentLabel: "Departamento", + departmentPlaceholder: "Selecciona un departamento", + emailLabel: "Correo electrónico", + emailPlaceholder: "Correo electrónico", + phoneLabel: "Teléfono", + phonePlaceholder: "Teléfono", + createSubmit: "Crear persona", + updateSubmit: "Actualizar persona", + }, + fallback: { + unknownDepartment: "Departamento desconocido", + }, + departments: { + IT: "IT", + ENGINEERING: "Ingeniería", + LOGISTICS: "Logística", + TRAFFIC: "Tráfico", + DRIVER: "Chofer", + ADMINISTRATION: "Administración", + SALES: "Ventas", + OTHER: "Otro", + }, + actions: { + createSuccess: "Persona creada correctamente", + createFailure: "Error al crear la persona", + updateSuccess: "Persona actualizada correctamente", + updateFailure: "Error al actualizar la persona", + duplicateEmail: "El correo electrónico ya existe", + }, + schema: { + firstNameRequired: "El nombre es obligatorio", + lastNameRequired: "El apellido es obligatorio", + departmentRequired: "El departamento es obligatorio", + emailInvalid: "El correo electrónico no es válido", + idRequired: "El ID es obligatorio", + }, + }, recipients: { list: { title: "Destinatarios", @@ -543,4 +617,4 @@ export const es = { }, }, }, -} satisfies Dictionary +} satisfies Dictionary \ No newline at end of file diff --git a/src/lib/constants.ts b/src/lib/constants.ts index c894af7..cdcf8e1 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -8,7 +8,7 @@ export const SIGN_IN_URL = "/login" export const TOKEN_EXPIRATION_SECONDS = 60 * 60 * 2 // 2 hour -export const RECIPIENT_DEPARTMENTS = { +export const PERSON_DEPARTMENTS = { IT: "IT", ENGINEERING: "ENGINEERING", LOGISTICS: "LOGISTICS", diff --git a/src/schemas/person.schema.ts b/src/schemas/person.schema.ts new file mode 100644 index 0000000..fa11940 --- /dev/null +++ b/src/schemas/person.schema.ts @@ -0,0 +1,74 @@ +import { z } from "zod" + +import type { Dictionary } from "@/i18n/dictionaries" + +export type PersonSchemaCopy = Dictionary["inventory"]["people"]["schema"] + +const defaultPersonSchemaCopy: PersonSchemaCopy = { + firstNameRequired: "First name is required", + lastNameRequired: "Last name is required", + departmentRequired: "Department is required", + emailInvalid: "Email format is invalid", + idRequired: "ID is required", +} + +const personDepartments = [ + "IT", + "ENGINEERING", + "TRAFFIC", + "DRIVER", + "LOGISTICS", + "ADMINISTRATION", + "SALES", + "OTHER", +] as const + +function buildPersonBaseSchema(copy: PersonSchemaCopy) { + return z.object({ + id: z.string().optional(), + firstName: z.string().min(1, { + error: copy.firstNameRequired, + }), + lastName: z.string().min(1, { + error: copy.lastNameRequired, + }), + department: z.enum(personDepartments, { + error: copy.departmentRequired, + }), + email: z.string().optional().nullable(), + phone: z.string().optional().nullable(), + userId: z.string().uuid().optional().nullable(), + }) +} + +export const personSchema = buildPersonBaseSchema(defaultPersonSchemaCopy) + +export function buildCreatePersonSchema(copy: PersonSchemaCopy) { + return buildPersonBaseSchema(copy).superRefine((data, ctx) => { + if (data.email && !z.string().email().safeParse(data.email).success) { + ctx.addIssue({ + code: "custom", + message: copy.emailInvalid, + path: ["email"], + }) + } + }) +} + +export const createPersonSchema = buildCreatePersonSchema( + defaultPersonSchemaCopy, +) + +export type CreatePersonFormType = z.infer + +export function buildUpdatePersonSchema(copy: PersonSchemaCopy) { + return buildPersonBaseSchema(copy).extend({ + id: z.string().nonempty(copy.idRequired), + }) +} + +export const updatePersonSchema = buildUpdatePersonSchema( + defaultPersonSchemaCopy, +) + +export type UpdatePersonFormType = z.infer \ No newline at end of file diff --git a/src/schemas/recipient.schema.ts b/src/schemas/recipient.schema.ts deleted file mode 100644 index 166a3f3..0000000 --- a/src/schemas/recipient.schema.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { z } from "zod" - -import type { Dictionary } from "@/i18n/dictionaries" - -export type RecipientSchemaCopy = - Dictionary["inventory"]["recipients"]["schema"] - -const defaultRecipientSchemaCopy: RecipientSchemaCopy = { - usernameRequired: "Username is required", - firstNameRequired: "First name is required", - lastNameRequired: "Last name is required", - departmentRequired: "Department is required", - emailInvalid: "Email format is invalid", - idRequired: "ID is required", -} - -const recipientDepartments = [ - "IT", - "ENGINEERING", - "TRAFFIC", - "DRIVER", - "LOGISTICS", - "ADMINISTRATION", - "SALES", - "OTHER", -] as const - -function buildRecipientBaseSchema(copy: RecipientSchemaCopy) { - return z.object({ - id: z.string().optional(), - username: z.string().min(1, { - error: copy.usernameRequired, - }), - firstName: z.string().min(1, { - error: copy.firstNameRequired, - }), - lastName: z.string().min(1, { - error: copy.lastNameRequired, - }), - department: z.enum(recipientDepartments, { - error: copy.departmentRequired, - }), - email: z.string().optional().nullable(), - phone: z.string().optional().nullable(), - }) -} - -export const recipientSchema = buildRecipientBaseSchema( - defaultRecipientSchemaCopy, -) - -export function buildCreateRecipientSchema(copy: RecipientSchemaCopy) { - return buildRecipientBaseSchema(copy).superRefine((data, ctx) => { - if (data.email && !z.string().email().safeParse(data.email).success) { - ctx.addIssue({ - code: "custom", - message: copy.emailInvalid, - path: ["email"], - }) - } - }) -} - -export const createRecipientSchema = buildCreateRecipientSchema( - defaultRecipientSchemaCopy, -) - -export type CreateRecipientFormType = z.infer - -export function buildUpdateRecipientSchema(copy: RecipientSchemaCopy) { - return buildRecipientBaseSchema(copy).extend({ - id: z.string().nonempty(copy.idRequired), - }) -} - -export const updateRecipientSchema = buildUpdateRecipientSchema( - defaultRecipientSchemaCopy, -) - -export type UpdateRecipientFormType = z.infer diff --git a/src/services/assignment.service.ts b/src/services/assignment.service.ts index 05082a7..4b58902 100644 --- a/src/services/assignment.service.ts +++ b/src/services/assignment.service.ts @@ -81,7 +81,7 @@ export const AssignmentService = { }, }) }, - findAllByRecipient: async ( + findAllByPerson: async ( recipientId: string, ): Promise => { return prisma.assignment.findMany({ @@ -143,4 +143,4 @@ export const AssignmentService = { data, }) }, -} +} \ No newline at end of file diff --git a/src/services/person.service.ts b/src/services/person.service.ts new file mode 100644 index 0000000..b5e99e4 --- /dev/null +++ b/src/services/person.service.ts @@ -0,0 +1,71 @@ +import type { Prisma, Person } from "@/generated/prisma/client" +import { paginate } from "@/lib/paginate" +import prisma from "@/lib/prisma" + +export const PersonService = { + findAll: async (): Promise => { + return prisma.person.findMany({ + orderBy: { + firstName: "asc", + }, + }) + }, + findAllPaginated: async ({ + page = 0, + pageSize, + search, + }: { + page?: number + pageSize?: number + search?: string + }) => { + return paginate({ + model: prisma.person, + page, + pageSize, + where: { + ...(search + ? { + OR: [ + { email: { contains: search, mode: "insensitive" } }, + { firstName: { contains: search, mode: "insensitive" } }, + { lastName: { contains: search, mode: "insensitive" } }, + ], + } + : {}), + }, + }) + }, + findAllPeopleCount: async (): Promise => { + return prisma.person.count() + }, + + findById: async ( + id: string, + db: Prisma.TransactionClient | typeof prisma = prisma, + ): Promise => { + return db.person.findUnique({ where: { id } }) + }, + + findByEmail: async ( + email: string, + db: Prisma.TransactionClient | typeof prisma = prisma, + ): Promise => { + return db.person.findUnique({ where: { email } }) + }, + + create: async ( + data: Prisma.PersonCreateInput, + db: Prisma.TransactionClient | typeof prisma = prisma, + ): Promise => { + return db.person.create({ data }) + }, + + update: async ( + id: string, + data: Prisma.PersonUpdateInput, + db: Prisma.TransactionClient | typeof prisma = prisma, + ): Promise => { + return db.person.update({ where: { id }, data }) + }, +} \ No newline at end of file diff --git a/src/services/recipient.service.ts b/src/services/recipient.service.ts deleted file mode 100644 index c9c104c..0000000 --- a/src/services/recipient.service.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { Prisma, Recipient } from "@/generated/prisma/client" -import { paginate } from "@/lib/paginate" -import prisma from "@/lib/prisma" - -export const RecipientService = { - findAll: async (): Promise => { - return prisma.recipient.findMany({ - orderBy: { - firstName: "asc", - }, - }) - }, - findAllPaginated: async ({ - page = 0, - pageSize, - search, - }: { - page?: number - pageSize?: number - search?: string - }) => { - return paginate({ - model: prisma.recipient, - page, - pageSize, - where: { - ...(search - ? { - OR: [ - { username: { contains: search, mode: "insensitive" } }, - { firstName: { contains: search, mode: "insensitive" } }, - { lastName: { contains: search, mode: "insensitive" } }, - ], - } - : {}), - }, - }) - }, - findAllRecipientsCount: async (): Promise => { - return prisma.recipient.count() - }, - - findById: async ( - id: string, - db: Prisma.TransactionClient | typeof prisma = prisma, - ): Promise => { - return db.recipient.findUnique({ where: { id } }) - }, - - findByUsername: async ( - username: string, - db: Prisma.TransactionClient | typeof prisma = prisma, - ): Promise => { - return db.recipient.findUnique({ where: { username } }) - }, - - findByEmail: async ( - email: string, - db: Prisma.TransactionClient | typeof prisma = prisma, - ): Promise => { - return db.recipient.findUnique({ where: { email } }) - }, - - create: async ( - data: Prisma.RecipientCreateInput, - db: Prisma.TransactionClient | typeof prisma = prisma, - ): Promise => { - return db.recipient.create({ data }) - }, - - update: async ( - id: string, - data: Prisma.RecipientUpdateInput, - db: Prisma.TransactionClient | typeof prisma = prisma, - ): Promise => { - return db.recipient.update({ where: { id }, data }) - }, -} diff --git a/src/types/assignment.ts b/src/types/assignment.ts index 348df42..24ad7b2 100644 --- a/src/types/assignment.ts +++ b/src/types/assignment.ts @@ -2,7 +2,7 @@ import type { Assignment as PrismaAssignment } from "@/generated/prisma/client" import type { Asset } from "./asset" import type { Item } from "./item" -import type { Recipient } from "./recipient" +import type { Person } from "./person" export type Assignment = PrismaAssignment @@ -10,7 +10,7 @@ export type AssignmentSummary = Pick export type AssignmentWithRecipientItemAsset = Assignment & { returnDate: Date | null - recipient: Recipient | null + recipient: Person | null item: Item | null asset: Asset | null -} +} \ No newline at end of file diff --git a/src/types/import.ts b/src/types/import.ts index c7535c7..c4efb13 100644 --- a/src/types/import.ts +++ b/src/types/import.ts @@ -6,7 +6,6 @@ export interface ImportItem { category?: string deliveryNote?: string assigned?: boolean - username?: string firstName?: string lastName?: string } diff --git a/src/types/index.ts b/src/types/index.ts index 93229d9..805b46c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,5 +5,5 @@ export * from "./import" export * from "./item" export * from "./movement" export * from "./paginate" -export * from "./recipient" +export * from "./person" export * from "./user" diff --git a/src/types/person.ts b/src/types/person.ts new file mode 100644 index 0000000..7ff1c8e --- /dev/null +++ b/src/types/person.ts @@ -0,0 +1,3 @@ +import type { Person as PrismaPerson } from "@/generated/prisma/client" + +export type Person = PrismaPerson \ No newline at end of file diff --git a/src/types/recipient.ts b/src/types/recipient.ts deleted file mode 100644 index 76dc990..0000000 --- a/src/types/recipient.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { Recipient as PrismaRecipient } from "@/generated/prisma/client" - -export type Recipient = PrismaRecipient diff --git a/src/use-cases/person.use-cases.ts b/src/use-cases/person.use-cases.ts new file mode 100644 index 0000000..989adf0 --- /dev/null +++ b/src/use-cases/person.use-cases.ts @@ -0,0 +1,127 @@ +import { Prisma } from "@/generated/prisma/client" +import prisma from "@/lib/prisma" +import type { + CreatePersonFormType, + UpdatePersonFormType, +} from "@/schemas/person.schema" +import { PersonService } from "@/services/person.service" + +type FieldErrors = Record + +type PersonUseCaseResult = + | { + success: true + } + | { + success: false + errors: FieldErrors + } + +function personError(errors: FieldErrors): PersonUseCaseResult { + return { + success: false, + errors, + } +} + +function uniqueErrorFor(error: unknown): FieldErrors | null { + if ( + !(error instanceof Prisma.PrismaClientKnownRequestError) || + error.code !== "P2002" + ) { + return null + } + + const target = Array.isArray(error.meta?.target) ? error.meta.target : [] + + if (target.includes("email")) { + return { email: ["Email already exists"] } + } + + return { email: ["Email already exists"] } +} + +export async function createPersonUseCase( + input: CreatePersonFormType, +): Promise { + const { firstName, lastName, department, email, phone, userId } = input + + try { + return await prisma.$transaction(async (tx) => { + if (email) { + const existingPersonEmail = await PersonService.findByEmail(email, tx) + + if (existingPersonEmail) { + return personError({ email: ["Email already exists"] }) + } + } + + await PersonService.create( + { + firstName, + lastName, + department, + email: email || null, + phone: phone || null, + ...(userId ? { user: { connect: { id: userId } } } : {}), + }, + tx, + ) + + return { + success: true, + } + }) + } catch (error) { + const errors = uniqueErrorFor(error) + + if (errors) { + return personError(errors) + } + + throw error + } +} + +export async function updatePersonUseCase( + input: UpdatePersonFormType, +): Promise { + const { id, firstName, lastName, department, email, phone, userId } = input + + try { + return await prisma.$transaction(async (tx) => { + if (email) { + const existingPersonEmail = await PersonService.findByEmail(email, tx) + + if (existingPersonEmail && existingPersonEmail.id !== id) { + return personError({ email: ["Email already exists"] }) + } + } + + await PersonService.update( + id, + { + firstName, + lastName, + department, + email: email || null, + phone: phone || null, + ...(userId ? { user: { connect: { id: userId } } } : { userId: null }), + }, + tx, + ) + + return { + success: true, + } + }) + } catch (error) { + const errors = uniqueErrorFor(error) + + if (errors) { + return personError(errors) + } + + throw error + } +} \ No newline at end of file diff --git a/src/use-cases/recipient.use-cases.ts b/src/use-cases/recipient.use-cases.ts deleted file mode 100644 index 720127a..0000000 --- a/src/use-cases/recipient.use-cases.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { Prisma } from "@/generated/prisma/client" -import prisma from "@/lib/prisma" -import type { - CreateRecipientFormType, - UpdateRecipientFormType, -} from "@/schemas/recipient.schema" -import { RecipientService } from "@/services/recipient.service" - -type FieldErrors = Record - -type RecipientUseCaseResult = - | { - success: true - } - | { - success: false - errors: FieldErrors - } - -function recipientError(errors: FieldErrors): RecipientUseCaseResult { - return { - success: false, - errors, - } -} - -function uniqueErrorFor(error: unknown): FieldErrors | null { - if ( - !(error instanceof Prisma.PrismaClientKnownRequestError) || - error.code !== "P2002" - ) { - return null - } - - const target = Array.isArray(error.meta?.target) ? error.meta.target : [] - - if (target.includes("username")) { - return { username: ["Username already exists"] } - } - - if (target.includes("email")) { - return { email: ["Email already exists"] } - } - - return { username: ["Username already exists"] } -} - -export async function createRecipientUseCase( - input: CreateRecipientFormType, -): Promise { - const { username, firstName, lastName, department, email, phone } = input - - try { - return await prisma.$transaction(async (tx) => { - const existingRecipientUsername = await RecipientService.findByUsername( - username, - tx, - ) - - if (existingRecipientUsername) { - return recipientError({ username: ["Username already exists"] }) - } - - if (email) { - const existingRecipientEmail = await RecipientService.findByEmail( - email, - tx, - ) - - if (existingRecipientEmail) { - return recipientError({ email: ["Email already exists"] }) - } - } - - await RecipientService.create( - { - username: username || (firstName[0] + lastName).toLowerCase(), - firstName, - lastName, - department, - email: email || null, - phone: phone || null, - }, - tx, - ) - - return { - success: true, - } - }) - } catch (error) { - const errors = uniqueErrorFor(error) - - if (errors) { - return recipientError(errors) - } - - throw error - } -} - -export async function updateRecipientUseCase( - input: UpdateRecipientFormType, -): Promise { - const { id, username, firstName, lastName, department, email, phone } = input - - try { - return await prisma.$transaction(async (tx) => { - const existingRecipient = await RecipientService.findByUsername( - username, - tx, - ) - - if (existingRecipient && existingRecipient.id !== id) { - return recipientError({ username: ["Username already exists"] }) - } - - if (email) { - const existingRecipientEmail = await RecipientService.findByEmail( - email, - tx, - ) - - if (existingRecipientEmail && existingRecipientEmail.id !== id) { - return recipientError({ email: ["Email already exists"] }) - } - } - - await RecipientService.update( - id, - { - username, - firstName, - lastName, - department, - email: email || null, - phone: phone || null, - }, - tx, - ) - - return { - success: true, - } - }) - } catch (error) { - const errors = uniqueErrorFor(error) - - if (errors) { - return recipientError(errors) - } - - throw error - } -} diff --git a/tests/integration/helpers/factories.ts b/tests/integration/helpers/factories.ts index e863640..de780cd 100644 --- a/tests/integration/helpers/factories.ts +++ b/tests/integration/helpers/factories.ts @@ -1,6 +1,6 @@ import type { PrismaClient, - RecipientDepartment, + PersonDepartment, UserRole, } from "@/generated/prisma/client" @@ -48,24 +48,22 @@ export async function createTestCategory( }) } -export async function createTestRecipient( +export async function createTestPerson( prisma: PrismaClient, overrides: Partial<{ - username: string firstName: string lastName: string - department: RecipientDepartment + department: PersonDepartment email: string | null phone: string | null }> = {}, ) { const suffix = nextSuffix() - return prisma.recipient.create({ + return prisma.person.create({ data: { - username: overrides.username ?? `test-recipient-${suffix}`, firstName: overrides.firstName ?? "Test", - lastName: overrides.lastName ?? "Recipient", + lastName: overrides.lastName ?? `Person-${suffix}`, department: overrides.department ?? "OTHER", email: overrides.email ?? null, phone: overrides.phone ?? null, @@ -92,4 +90,4 @@ export async function createTestItem( category: { connect: { id: categoryId } }, }, }) -} +} \ No newline at end of file diff --git a/tests/integration/helpers/test-db.ts b/tests/integration/helpers/test-db.ts index 0a3bcec..94ee101 100644 --- a/tests/integration/helpers/test-db.ts +++ b/tests/integration/helpers/test-db.ts @@ -18,7 +18,7 @@ const TABLES_TO_TRUNCATE = [ "Asset", "Item", "Category", - "Recipient", + "Person", "User", ] diff --git a/tests/integration/use-cases/asset.use-cases.test.ts b/tests/integration/use-cases/asset.use-cases.test.ts index 26a8c3c..cd45e46 100644 --- a/tests/integration/use-cases/asset.use-cases.test.ts +++ b/tests/integration/use-cases/asset.use-cases.test.ts @@ -2,7 +2,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest" import type { PrismaClient } from "@/generated/prisma/client" import { createTestItem, - createTestRecipient, + createTestPerson, createTestUser, } from "../helpers/factories" import { @@ -80,7 +80,7 @@ describe("asset use-cases", () => { it("creates an assigned asset with assignment and ASSIGNMENT movement", async () => { const actor = await createTestUser(prisma) - const recipient = await createTestRecipient(prisma) + const recipient = await createTestPerson(prisma) const item = await createTestItem(prisma, { stock: 0 }) const result = await createAssetUseCase({ @@ -133,7 +133,7 @@ describe("asset use-cases", () => { it("moves an available asset to assigned and back to available", async () => { const actor = await createTestUser(prisma) - const recipient = await createTestRecipient(prisma) + const recipient = await createTestPerson(prisma) const item = await createTestItem(prisma, { stock: 0 }) const created = await createAssetUseCase({ @@ -230,7 +230,7 @@ describe("asset use-cases", () => { it("returns an active assignment without restoring stock when an assigned asset moves to a terminal status", async () => { const actor = await createTestUser(prisma) - const recipient = await createTestRecipient(prisma) + const recipient = await createTestPerson(prisma) const item = await createTestItem(prisma, { stock: 0 }) const created = await createAssetUseCase({ diff --git a/tests/integration/use-cases/assignment.use-cases.test.ts b/tests/integration/use-cases/assignment.use-cases.test.ts index 9f2476e..cb04755 100644 --- a/tests/integration/use-cases/assignment.use-cases.test.ts +++ b/tests/integration/use-cases/assignment.use-cases.test.ts @@ -2,7 +2,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest" import type { PrismaClient } from "@/generated/prisma/client" import { createTestItem, - createTestRecipient, + createTestPerson, createTestUser, } from "../helpers/factories" import { @@ -38,7 +38,7 @@ afterAll(async () => { describe("assignment use-cases", () => { it("creates an assignment, decrements stock, and records an ASSIGNMENT movement", async () => { const actor = await createTestUser(prisma) - const recipient = await createTestRecipient(prisma) + const recipient = await createTestPerson(prisma) const item = await createTestItem(prisma, { stock: 5 }) const assignmentDate = new Date("2026-01-01T00:00:00.000Z") @@ -88,7 +88,7 @@ describe("assignment use-cases", () => { it("rejects assignment creation when item stock is insufficient", async () => { const actor = await createTestUser(prisma) - const recipient = await createTestRecipient(prisma) + const recipient = await createTestPerson(prisma) const item = await createTestItem(prisma, { stock: 1 }) const result = await createAssignmentUseCase({ @@ -114,7 +114,7 @@ describe("assignment use-cases", () => { it("returns an assignment, restores stock, closes it, and records a RETURN movement", async () => { const actor = await createTestUser(prisma) - const recipient = await createTestRecipient(prisma) + const recipient = await createTestPerson(prisma) const item = await createTestItem(prisma, { stock: 4 }) const created = await createAssignmentUseCase({ @@ -172,7 +172,7 @@ describe("assignment use-cases", () => { it("rejects returning the same assignment twice", async () => { const actor = await createTestUser(prisma) - const recipient = await createTestRecipient(prisma) + const recipient = await createTestPerson(prisma) const item = await createTestItem(prisma, { stock: 2 }) const created = await createAssignmentUseCase({ diff --git a/tests/integration/use-cases/person.use-cases.test.ts b/tests/integration/use-cases/person.use-cases.test.ts new file mode 100644 index 0000000..f014070 --- /dev/null +++ b/tests/integration/use-cases/person.use-cases.test.ts @@ -0,0 +1,190 @@ +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 createPersonUseCase: typeof import("@/use-cases/person.use-cases").createPersonUseCase +let updatePersonUseCase: typeof import("@/use-cases/person.use-cases").updatePersonUseCase + +beforeAll(async () => { + await startIntegrationTestDatabase() + + const prismaModule = await import("@/lib/prisma") + const personUseCases = await import("@/use-cases/person.use-cases") + + prisma = prismaModule.prisma + createPersonUseCase = personUseCases.createPersonUseCase + updatePersonUseCase = personUseCases.updatePersonUseCase +}) + +beforeEach(async () => { + await resetIntegrationTestDatabase(prisma) +}) + +afterAll(async () => { + await prisma?.$disconnect() + await stopIntegrationTestDatabase() +}) + +describe("person use-cases", () => { + it("creates a person and normalizes empty optional contact fields to null", async () => { + await expect( + createPersonUseCase({ + firstName: "Person", + lastName: "One", + department: "IT", + email: "", + phone: "", + }), + ).resolves.toEqual({ success: true }) + + await expect( + prisma.person.findFirstOrThrow({ + where: { firstName: "Person", lastName: "One" }, + }), + ).resolves.toMatchObject({ + firstName: "Person", + lastName: "One", + department: "IT", + email: null, + phone: null, + userId: null, + }) + }) + + it("creates a person with linked userId", async () => { + const user = await createTestUser(prisma) + + await expect( + createPersonUseCase({ + firstName: "Linked", + lastName: "Person", + department: "ENGINEERING", + email: "linked@example.test", + phone: null, + userId: user.id, + }), + ).resolves.toEqual({ success: true }) + + await expect( + prisma.person.findFirstOrThrow({ + where: { firstName: "Linked" }, + }), + ).resolves.toMatchObject({ + firstName: "Linked", + lastName: "Person", + department: "ENGINEERING", + email: "linked@example.test", + userId: user.id, + }) + }) + + it("rejects duplicate emails on create", async () => { + await createTestPerson(prisma, { + email: "existing@example.test", + }) + + await expect( + createPersonUseCase({ + firstName: "Duplicate", + lastName: "Email", + department: "OTHER", + email: "existing@example.test", + phone: null, + }), + ).resolves.toEqual({ + success: false, + errors: { email: ["Email already exists"] }, + }) + + await expect(prisma.person.count()).resolves.toBe(1) + }) + + it("updates a person and rejects duplicate emails", async () => { + const person = await createTestPerson(prisma, { + email: "person@example.test", + phone: "111111111", + }) + const other = await createTestPerson(prisma, { + email: "other@example.test", + }) + + await expect( + updatePersonUseCase({ + id: person.id, + firstName: "Edited", + lastName: "Person", + department: "ENGINEERING", + email: "edited@example.test", + phone: "222222222", + }), + ).resolves.toEqual({ success: true }) + + await expect( + prisma.person.findUniqueOrThrow({ where: { id: person.id } }), + ).resolves.toMatchObject({ + firstName: "Edited", + lastName: "Person", + department: "ENGINEERING", + email: "edited@example.test", + phone: "222222222", + }) + + await expect( + updatePersonUseCase({ + id: person.id, + firstName: "Edited", + lastName: "Person", + department: "ENGINEERING", + email: other.email, + phone: "222222222", + }), + ).resolves.toEqual({ + success: false, + errors: { email: ["Email already exists"] }, + }) + + await expect( + prisma.person.findUniqueOrThrow({ where: { id: person.id } }), + ).resolves.toMatchObject({ + email: "edited@example.test", + }) + await expect(prisma.person.count()).resolves.toBe(2) + }) + + it("searches by email and name in paginated results", async () => { + await createTestPerson(prisma, { + firstName: "Alice", + lastName: "Smith", + email: "alice@company.com", + }) + await createTestPerson(prisma, { + firstName: "Bob", + lastName: "Jones", + email: "bob@other.com", + }) + + const { PersonService } = await import("@/services/person.service") + + const emailResults = await PersonService.findAllPaginated({ + search: "company", + page: 1, + pageSize: 10, + }) + expect(emailResults.data).toHaveLength(1) + expect(emailResults.data[0].firstName).toBe("Alice") + + const nameResults = await PersonService.findAllPaginated({ + search: "Bob", + page: 1, + pageSize: 10, + }) + expect(nameResults.data).toHaveLength(1) + expect(nameResults.data[0].firstName).toBe("Bob") + }) +}) \ No newline at end of file diff --git a/tests/integration/use-cases/recipient.use-cases.test.ts b/tests/integration/use-cases/recipient.use-cases.test.ts deleted file mode 100644 index ed890ae..0000000 --- a/tests/integration/use-cases/recipient.use-cases.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest" -import type { PrismaClient } from "@/generated/prisma/client" -import { createTestRecipient } from "../helpers/factories" -import { - resetIntegrationTestDatabase, - startIntegrationTestDatabase, - stopIntegrationTestDatabase, -} from "../helpers/test-db" - -let prisma: PrismaClient -let createRecipientUseCase: typeof import("@/use-cases/recipient.use-cases").createRecipientUseCase -let updateRecipientUseCase: typeof import("@/use-cases/recipient.use-cases").updateRecipientUseCase - -beforeAll(async () => { - await startIntegrationTestDatabase() - - const prismaModule = await import("@/lib/prisma") - const recipientUseCases = await import("@/use-cases/recipient.use-cases") - - prisma = prismaModule.prisma - createRecipientUseCase = recipientUseCases.createRecipientUseCase - updateRecipientUseCase = recipientUseCases.updateRecipientUseCase -}) - -beforeEach(async () => { - await resetIntegrationTestDatabase(prisma) -}) - -afterAll(async () => { - await prisma?.$disconnect() - await stopIntegrationTestDatabase() -}) - -describe("recipient use-cases", () => { - it("creates a recipient and normalizes empty optional contact fields to null", async () => { - await expect( - createRecipientUseCase({ - username: "recipient-one", - firstName: "Recipient", - lastName: "One", - department: "IT", - email: "", - phone: "", - }), - ).resolves.toEqual({ success: true }) - - await expect( - prisma.recipient.findUniqueOrThrow({ - where: { username: "recipient-one" }, - }), - ).resolves.toMatchObject({ - username: "recipient-one", - firstName: "Recipient", - lastName: "One", - department: "IT", - email: null, - phone: null, - }) - }) - - it("rejects duplicate usernames and duplicate emails on create", async () => { - await createTestRecipient(prisma, { - username: "existing-recipient", - email: "existing-recipient@example.test", - }) - - await expect( - createRecipientUseCase({ - username: "existing-recipient", - firstName: "Duplicate", - lastName: "Username", - department: "OTHER", - email: "unique-recipient@example.test", - phone: null, - }), - ).resolves.toEqual({ - success: false, - errors: { username: ["Username already exists"] }, - }) - - await expect( - createRecipientUseCase({ - username: "unique-recipient", - firstName: "Duplicate", - lastName: "Email", - department: "OTHER", - email: "existing-recipient@example.test", - phone: null, - }), - ).resolves.toEqual({ - success: false, - errors: { email: ["Email already exists"] }, - }) - - await expect(prisma.recipient.count()).resolves.toBe(1) - }) - - it("updates a recipient and rejects duplicate usernames or emails", async () => { - const recipient = await createTestRecipient(prisma, { - username: "editable-recipient", - email: "editable-recipient@example.test", - phone: "111111111", - }) - const other = await createTestRecipient(prisma, { - username: "other-recipient", - email: "other-recipient@example.test", - }) - - await expect( - updateRecipientUseCase({ - id: recipient.id, - username: "edited-recipient", - firstName: "Edited", - lastName: "Recipient", - department: "ENGINEERING", - email: "edited-recipient@example.test", - phone: "222222222", - }), - ).resolves.toEqual({ success: true }) - - await expect( - prisma.recipient.findUniqueOrThrow({ where: { id: recipient.id } }), - ).resolves.toMatchObject({ - username: "edited-recipient", - firstName: "Edited", - lastName: "Recipient", - department: "ENGINEERING", - email: "edited-recipient@example.test", - phone: "222222222", - }) - - await expect( - updateRecipientUseCase({ - id: recipient.id, - username: other.username, - firstName: "Edited", - lastName: "Recipient", - department: "ENGINEERING", - email: "new-recipient@example.test", - phone: "222222222", - }), - ).resolves.toEqual({ - success: false, - errors: { username: ["Username already exists"] }, - }) - - await expect( - updateRecipientUseCase({ - id: recipient.id, - username: "edited-recipient", - firstName: "Edited", - lastName: "Recipient", - department: "ENGINEERING", - email: other.email, - phone: "222222222", - }), - ).resolves.toEqual({ - success: false, - errors: { email: ["Email already exists"] }, - }) - - await expect( - prisma.recipient.findUniqueOrThrow({ where: { id: recipient.id } }), - ).resolves.toMatchObject({ - username: "edited-recipient", - email: "edited-recipient@example.test", - }) - await expect(prisma.recipient.count()).resolves.toBe(2) - }) -})