refactor: rename Recipient to Person, remove username, add userId FK

This commit is contained in:
2026-06-16 10:04:24 +02:00
parent befe1f3f82
commit d67f31cf54
27 changed files with 751 additions and 628 deletions
@@ -2,7 +2,7 @@
CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'MANAGER', 'STAFF', 'VIEWER'); CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'MANAGER', 'STAFF', 'VIEWER');
-- CreateEnum -- 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 -- CreateEnum
CREATE TYPE "ItemStatus" AS ENUM ('AVAILABLE', 'ASSIGNED', 'RESERVED', 'IN_REPAIR', 'BROKEN', 'STOLEN', 'DISPOSED'); CREATE TYPE "ItemStatus" AS ENUM ('AVAILABLE', 'ASSIGNED', 'RESERVED', 'IN_REPAIR', 'BROKEN', 'STOLEN', 'DISPOSED');
@@ -26,19 +26,19 @@ CREATE TABLE "User" (
); );
-- CreateTable -- CreateTable
CREATE TABLE "Recipient" ( CREATE TABLE "Person" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"username" TEXT NOT NULL,
"firstName" TEXT NOT NULL, "firstName" TEXT NOT NULL,
"lastName" TEXT NOT NULL, "lastName" TEXT NOT NULL,
"department" "RecipientDepartment", "department" "PersonDepartment",
"email" TEXT, "email" TEXT,
"phone" TEXT, "phone" TEXT,
"userId" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true, "isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Recipient_pkey" PRIMARY KEY ("id") CONSTRAINT "Person_pkey" PRIMARY KEY ("id")
); );
-- CreateTable -- CreateTable
@@ -126,16 +126,16 @@ CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "Recipient_username_key" ON "Recipient"("username"); CREATE UNIQUE INDEX "Person_email_key" ON "Person"("email");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "Recipient_email_key" ON "Recipient"("email"); CREATE UNIQUE INDEX "Person_userId_key" ON "Person"("userId");
-- CreateIndex -- CreateIndex
CREATE INDEX "Recipient_lastName_firstName_idx" ON "Recipient"("lastName", "firstName"); CREATE INDEX "Person_lastName_firstName_idx" ON "Person"("lastName", "firstName");
-- CreateIndex -- CreateIndex
CREATE INDEX "Recipient_department_idx" ON "Recipient"("department"); CREATE INDEX "Person_department_idx" ON "Person"("department");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "Category_name_key" ON "Category"("name"); CREATE UNIQUE INDEX "Category_name_key" ON "Category"("name");
@@ -191,6 +191,9 @@ CREATE INDEX "Movement_type_idx" ON "Movement"("type");
-- CreateIndex -- CreateIndex
CREATE INDEX "Movement_userId_idx" ON "Movement"("userId"); 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 -- AddForeignKey
ALTER TABLE "Item" ADD CONSTRAINT "Item_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 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; ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "Asset"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey -- 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 -- AddForeignKey
ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -216,7 +219,7 @@ 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; ALTER TABLE "Movement" ADD CONSTRAINT "Movement_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "Asset"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey -- 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 -- AddForeignKey
ALTER TABLE "Movement" ADD CONSTRAINT "Movement_assignmentId_fkey" FOREIGN KEY ("assignmentId") REFERENCES "Assignment"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "Movement" ADD CONSTRAINT "Movement_assignmentId_fkey" FOREIGN KEY ("assignmentId") REFERENCES "Assignment"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+18 -16
View File
@@ -33,9 +33,10 @@ model User {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
movements Movement[] movements Movement[]
assignments Assignment[] assignments Assignment[]
person Person?
} }
enum RecipientDepartment { enum PersonDepartment {
IT IT
ENGINEERING ENGINEERING
LOGISTICS LOGISTICS
@@ -46,17 +47,18 @@ enum RecipientDepartment {
OTHER OTHER
} }
model Recipient { model Person {
id String @id @default(uuid()) id String @id @default(uuid())
username String @unique
firstName String firstName String
lastName String lastName String
department RecipientDepartment? department PersonDepartment?
email String? @unique email String? @unique
phone String? phone String?
isActive Boolean @default(true) userId String? @unique
createdAt DateTime @default(now()) user User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade)
updatedAt DateTime @updatedAt isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
assignments Assignment[] assignments Assignment[]
movements Movement[] movements Movement[]
@@ -128,11 +130,11 @@ model Assignment {
quantity Int? quantity Int?
notes String? notes String?
itemId String? itemId String?
item Item? @relation(fields: [itemId], references: [id]) item Item? @relation(fields: [itemId], references: [id], onDelete: SetNull, onUpdate: Cascade)
assetId String? @unique assetId String? @unique
asset Asset? @relation(fields: [assetId], references: [id]) asset Asset? @relation(fields: [assetId], references: [id], onDelete: SetNull, onUpdate: Cascade)
recipientId String? 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()) assignmentDate DateTime @default(now())
returnDate DateTime? returnDate DateTime?
createdBy String createdBy String
@@ -163,15 +165,15 @@ model Movement {
details String? details String?
notes String? notes String?
itemId String? itemId String?
item Item? @relation(fields: [itemId], references: [id]) item Item? @relation(fields: [itemId], references: [id], onDelete: SetNull, onUpdate: Cascade)
assetId String? assetId String?
asset Asset? @relation(fields: [assetId], references: [id]) asset Asset? @relation(fields: [assetId], references: [id], onDelete: SetNull, onUpdate: Cascade)
previousStock Int? previousStock Int?
newStock Int? newStock Int?
recipientId String? recipientId String?
recipient Recipient? @relation(fields: [recipientId], references: [id]) recipient Person? @relation(fields: [recipientId], references: [id], onDelete: SetNull, onUpdate: Cascade)
assignmentId String? assignmentId String?
assignment Assignment? @relation(fields: [assignmentId], references: [id]) assignment Assignment? @relation(fields: [assignmentId], references: [id], onDelete: SetNull, onUpdate: Cascade)
userId String userId String
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
+18 -23
View File
@@ -12,14 +12,14 @@ import { getAuthenticatedUserId } from "@/services/auth.service"
import { CategoryService } from "@/services/category.service" import { CategoryService } from "@/services/category.service"
import { ItemService } from "@/services/item.service" import { ItemService } from "@/services/item.service"
import { MovementService } from "@/services/movement.service" import { MovementService } from "@/services/movement.service"
import { RecipientService } from "@/services/recipient.service" import { PersonService } from "@/services/person.service"
import type { import type {
Asset, Asset,
Assignment, Assignment,
Category, Category,
ImportItem, ImportItem,
Item, Item,
Recipient, Person,
} from "@/types" } from "@/types"
export async function importItems(formData: ImportFormType) { export async function importItems(formData: ImportFormType) {
@@ -123,7 +123,7 @@ export async function importItems(formData: ImportFormType) {
file: [ file: [
"Only one of category or categoryId is allowed, you must select one of them", "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, category,
deliveryNote, deliveryNote,
assigned, assigned,
username,
firstName, firstName,
lastName, lastName,
} = row } = row
@@ -178,10 +177,6 @@ export async function importItems(formData: ImportFormType) {
importErrors.push(`Row ${index + 2}: Delivery note must be a string`) 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") { if (firstName && typeof firstName !== "string") {
importErrors.push(`Row ${index + 2}: First name must be a 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() || "", category: row.category?.trim() || "",
deliveryNote: row.deliveryNote?.trim() || "", deliveryNote: row.deliveryNote?.trim() || "",
assigned: row.assigned?.trim() === "true", assigned: row.assigned?.trim() === "true",
username: row.username?.trim() || "",
firstName: row.firstName?.trim() || "", firstName: row.firstName?.trim() || "",
lastName: row.lastName?.trim() || "", lastName: row.lastName?.trim() || "",
}) })
@@ -229,7 +223,6 @@ export async function importItems(formData: ImportFormType) {
category, category,
deliveryNote, deliveryNote,
assigned, assigned,
username,
firstName, firstName,
lastName, lastName,
} = item } = item
@@ -238,7 +231,7 @@ export async function importItems(formData: ImportFormType) {
let newItem: Item | null = null let newItem: Item | null = null
let newAsset: Asset | null = null let newAsset: Asset | null = null
let newCategory: Category | null = null let newCategory: Category | null = null
let newRecipient: Recipient | null = null let newPerson: Person | null = null
let newAssignment: Assignment | null = null let newAssignment: Assignment | null = null
const existingCategory = categoryId const existingCategory = categoryId
@@ -290,14 +283,16 @@ export async function importItems(formData: ImportFormType) {
} }
if (assigned && firstName && lastName) { if (assigned && firstName && lastName) {
const finalUsername = const existingPerson = firstName
username || `${firstName.toLowerCase()[0]}${lastName.toLowerCase()}` ? await PersonService.findAllPaginated({
const existingRecipient = search: firstName,
await RecipientService.findByUsername(finalUsername) page: 0,
pageSize: 1,
})
: null
if (!existingRecipient) { if (!existingPerson || existingPerson.data.length === 0) {
newRecipient = await RecipientService.create({ newPerson = await PersonService.create({
username: finalUsername,
firstName, firstName,
lastName, lastName,
email: undefined, email: undefined,
@@ -305,7 +300,7 @@ export async function importItems(formData: ImportFormType) {
department: "OTHER", department: "OTHER",
}) })
} else { } else {
newRecipient = existingRecipient newPerson = existingPerson.data[0]
} }
newAssignment = await AssignmentService.create({ newAssignment = await AssignmentService.create({
@@ -313,7 +308,7 @@ export async function importItems(formData: ImportFormType) {
notes: deliveryNote || "", notes: deliveryNote || "",
itemId: newItem?.id || "", itemId: newItem?.id || "",
assetId: newAsset?.id || "", assetId: newAsset?.id || "",
recipientId: newRecipient?.id || "", recipientId: newPerson?.id || "",
assignmentDate: new Date(), assignmentDate: new Date(),
createdBy: userId, createdBy: userId,
}) })
@@ -324,15 +319,15 @@ export async function importItems(formData: ImportFormType) {
quantity: stock || 1, quantity: stock || 1,
type: assigned ? "ASSIGNMENT" : "IN", type: assigned ? "ASSIGNMENT" : "IN",
itemId: newItem?.id || undefined, itemId: newItem?.id || undefined,
recipientId: newRecipient?.id || undefined, recipientId: newPerson?.id || undefined,
} }
if (newAssignment?.id) { if (newAssignment?.id) {
movementData.assignmentId = newAssignment.id movementData.assignmentId = newAssignment.id
} }
if (newRecipient?.id) { if (newPerson?.id) {
movementData.recipientId = newRecipient.id movementData.recipientId = newPerson.id
} }
await MovementService.create({ await MovementService.create({
@@ -4,22 +4,22 @@ import { revalidatePath } from "next/cache"
import { flattenError } from "zod" import { flattenError } from "zod"
import { getI18n } from "@/i18n/server" import { getI18n } from "@/i18n/server"
import { import {
buildCreateRecipientSchema, buildCreatePersonSchema,
buildUpdateRecipientSchema, buildUpdatePersonSchema,
type CreateRecipientFormType, type CreatePersonFormType,
type UpdateRecipientFormType, type UpdatePersonFormType,
} from "@/schemas/recipient.schema" } from "@/schemas/person.schema"
import { import {
createRecipientUseCase, createPersonUseCase,
updateRecipientUseCase, updatePersonUseCase,
} from "@/use-cases/recipient.use-cases" } 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 { dictionary } = await getI18n()
const copy = dictionary.inventory.recipients const copy = dictionary.inventory.people
const validatedFields = buildCreateRecipientSchema(copy.schema).safeParse( const validatedFields = buildCreatePersonSchema(copy.schema).safeParse(
formData, formData,
) )
@@ -31,17 +31,17 @@ export async function createNewRecipient(formData: CreateRecipientFormType) {
} }
try { try {
const result = await createRecipientUseCase(validatedFields.data) const result = await createPersonUseCase(validatedFields.data)
if (!result.success) { if (!result.success) {
return { return {
...result, ...result,
errors: localizeRecipientFieldErrors(result.errors, copy.actions), errors: localizePersonFieldErrors(result.errors, copy.actions),
message: copy.actions.createFailure, message: copy.actions.createFailure,
} }
} }
revalidatePath("/recipients") revalidatePath("/people")
return { return {
success: true, 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 { dictionary } = await getI18n()
const copy = dictionary.inventory.recipients const copy = dictionary.inventory.people
const validatedFields = buildUpdateRecipientSchema(copy.schema).safeParse( const validatedFields = buildUpdatePersonSchema(copy.schema).safeParse(
formData, formData,
) )
@@ -71,17 +71,17 @@ export async function updateRecipient(formData: UpdateRecipientFormType) {
} }
try { try {
const result = await updateRecipientUseCase(validatedFields.data) const result = await updatePersonUseCase(validatedFields.data)
if (!result.success) { if (!result.success) {
return { return {
...result, ...result,
errors: localizeRecipientFieldErrors(result.errors, copy.actions), errors: localizePersonFieldErrors(result.errors, copy.actions),
message: copy.actions.updateFailure, message: copy.actions.updateFailure,
} }
} }
revalidatePath("/recipients") revalidatePath("/people")
return { return {
success: true, success: true,
+38
View File
@@ -0,0 +1,38 @@
import type { Dictionary } from "@/i18n/dictionaries"
type PersonActionCopy = Dictionary["inventory"]["people"]["actions"]
type FieldErrors = Record<string, string[]>
const personErrorMessageKeys = {
"Email already exists": "duplicateEmail",
} as const satisfies Record<string, keyof PersonActionCopy>
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)),
]),
)
}
-39
View File
@@ -1,39 +0,0 @@
import type { Dictionary } from "@/i18n/dictionaries"
type RecipientActionCopy = Dictionary["inventory"]["recipients"]["actions"]
type FieldErrors = Record<string, string[]>
const recipientErrorMessageKeys = {
"Username already exists": "duplicateUsername",
"Email already exists": "duplicateEmail",
} as const satisfies Record<string, keyof RecipientActionCopy>
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)),
]),
)
}
+74
View File
@@ -318,6 +318,80 @@ export const en = {
idRequired: "Assignment ID is required", 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: { recipients: {
list: { list: {
title: "Recipients", title: "Recipients",
+74
View File
@@ -323,6 +323,80 @@ export const es = {
idRequired: "El ID de asignación es obligatorio", 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: { recipients: {
list: { list: {
title: "Destinatarios", title: "Destinatarios",
+1 -1
View File
@@ -8,7 +8,7 @@ export const SIGN_IN_URL = "/login"
export const TOKEN_EXPIRATION_SECONDS = 60 * 60 * 2 // 2 hour export const TOKEN_EXPIRATION_SECONDS = 60 * 60 * 2 // 2 hour
export const RECIPIENT_DEPARTMENTS = { export const PERSON_DEPARTMENTS = {
IT: "IT", IT: "IT",
ENGINEERING: "ENGINEERING", ENGINEERING: "ENGINEERING",
LOGISTICS: "LOGISTICS", LOGISTICS: "LOGISTICS",
+74
View File
@@ -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<typeof createPersonSchema>
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<typeof updatePersonSchema>
-80
View File
@@ -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<typeof createRecipientSchema>
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<typeof updateRecipientSchema>
+1 -1
View File
@@ -81,7 +81,7 @@ export const AssignmentService = {
}, },
}) })
}, },
findAllByRecipient: async ( findAllByPerson: async (
recipientId: string, recipientId: string,
): Promise<AssignmentWithRecipientItemAsset[]> => { ): Promise<AssignmentWithRecipientItemAsset[]> => {
return prisma.assignment.findMany({ return prisma.assignment.findMany({
+71
View File
@@ -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<Person[]> => {
return prisma.person.findMany({
orderBy: {
firstName: "asc",
},
})
},
findAllPaginated: async ({
page = 0,
pageSize,
search,
}: {
page?: number
pageSize?: number
search?: string
}) => {
return paginate<Person>({
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<number> => {
return prisma.person.count()
},
findById: async (
id: string,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Person | null> => {
return db.person.findUnique({ where: { id } })
},
findByEmail: async (
email: string,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Person | null> => {
return db.person.findUnique({ where: { email } })
},
create: async (
data: Prisma.PersonCreateInput,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Person> => {
return db.person.create({ data })
},
update: async (
id: string,
data: Prisma.PersonUpdateInput,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Person> => {
return db.person.update({ where: { id }, data })
},
}
-78
View File
@@ -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<Recipient[]> => {
return prisma.recipient.findMany({
orderBy: {
firstName: "asc",
},
})
},
findAllPaginated: async ({
page = 0,
pageSize,
search,
}: {
page?: number
pageSize?: number
search?: string
}) => {
return paginate<Recipient>({
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<number> => {
return prisma.recipient.count()
},
findById: async (
id: string,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Recipient | null> => {
return db.recipient.findUnique({ where: { id } })
},
findByUsername: async (
username: string,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Recipient | null> => {
return db.recipient.findUnique({ where: { username } })
},
findByEmail: async (
email: string,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Recipient | null> => {
return db.recipient.findUnique({ where: { email } })
},
create: async (
data: Prisma.RecipientCreateInput,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Recipient> => {
return db.recipient.create({ data })
},
update: async (
id: string,
data: Prisma.RecipientUpdateInput,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Recipient> => {
return db.recipient.update({ where: { id }, data })
},
}
+2 -2
View File
@@ -2,7 +2,7 @@ import type { Assignment as PrismaAssignment } from "@/generated/prisma/client"
import type { Asset } from "./asset" import type { Asset } from "./asset"
import type { Item } from "./item" import type { Item } from "./item"
import type { Recipient } from "./recipient" import type { Person } from "./person"
export type Assignment = PrismaAssignment export type Assignment = PrismaAssignment
@@ -10,7 +10,7 @@ export type AssignmentSummary = Pick<Assignment, "id" | "quantity">
export type AssignmentWithRecipientItemAsset = Assignment & { export type AssignmentWithRecipientItemAsset = Assignment & {
returnDate: Date | null returnDate: Date | null
recipient: Recipient | null recipient: Person | null
item: Item | null item: Item | null
asset: Asset | null asset: Asset | null
} }
-1
View File
@@ -6,7 +6,6 @@ export interface ImportItem {
category?: string category?: string
deliveryNote?: string deliveryNote?: string
assigned?: boolean assigned?: boolean
username?: string
firstName?: string firstName?: string
lastName?: string lastName?: string
} }
+1 -1
View File
@@ -5,5 +5,5 @@ export * from "./import"
export * from "./item" export * from "./item"
export * from "./movement" export * from "./movement"
export * from "./paginate" export * from "./paginate"
export * from "./recipient" export * from "./person"
export * from "./user" export * from "./user"
+3
View File
@@ -0,0 +1,3 @@
import type { Person as PrismaPerson } from "@/generated/prisma/client"
export type Person = PrismaPerson
-3
View File
@@ -1,3 +0,0 @@
import type { Recipient as PrismaRecipient } from "@/generated/prisma/client"
export type Recipient = PrismaRecipient
+127
View File
@@ -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<string, string[]>
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<PersonUseCaseResult> {
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<PersonUseCaseResult> {
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
}
}
-155
View File
@@ -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<string, string[]>
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<RecipientUseCaseResult> {
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<RecipientUseCaseResult> {
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
}
}
+5 -7
View File
@@ -1,6 +1,6 @@
import type { import type {
PrismaClient, PrismaClient,
RecipientDepartment, PersonDepartment,
UserRole, UserRole,
} from "@/generated/prisma/client" } from "@/generated/prisma/client"
@@ -48,24 +48,22 @@ export async function createTestCategory(
}) })
} }
export async function createTestRecipient( export async function createTestPerson(
prisma: PrismaClient, prisma: PrismaClient,
overrides: Partial<{ overrides: Partial<{
username: string
firstName: string firstName: string
lastName: string lastName: string
department: RecipientDepartment department: PersonDepartment
email: string | null email: string | null
phone: string | null phone: string | null
}> = {}, }> = {},
) { ) {
const suffix = nextSuffix() const suffix = nextSuffix()
return prisma.recipient.create({ return prisma.person.create({
data: { data: {
username: overrides.username ?? `test-recipient-${suffix}`,
firstName: overrides.firstName ?? "Test", firstName: overrides.firstName ?? "Test",
lastName: overrides.lastName ?? "Recipient", lastName: overrides.lastName ?? `Person-${suffix}`,
department: overrides.department ?? "OTHER", department: overrides.department ?? "OTHER",
email: overrides.email ?? null, email: overrides.email ?? null,
phone: overrides.phone ?? null, phone: overrides.phone ?? null,
+1 -1
View File
@@ -18,7 +18,7 @@ const TABLES_TO_TRUNCATE = [
"Asset", "Asset",
"Item", "Item",
"Category", "Category",
"Recipient", "Person",
"User", "User",
] ]
@@ -2,7 +2,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"
import type { PrismaClient } from "@/generated/prisma/client" import type { PrismaClient } from "@/generated/prisma/client"
import { import {
createTestItem, createTestItem,
createTestRecipient, createTestPerson,
createTestUser, createTestUser,
} from "../helpers/factories" } from "../helpers/factories"
import { import {
@@ -80,7 +80,7 @@ describe("asset use-cases", () => {
it("creates an assigned asset with assignment and ASSIGNMENT movement", async () => { it("creates an assigned asset with assignment and ASSIGNMENT movement", async () => {
const actor = await createTestUser(prisma) const actor = await createTestUser(prisma)
const recipient = await createTestRecipient(prisma) const recipient = await createTestPerson(prisma)
const item = await createTestItem(prisma, { stock: 0 }) const item = await createTestItem(prisma, { stock: 0 })
const result = await createAssetUseCase({ const result = await createAssetUseCase({
@@ -133,7 +133,7 @@ describe("asset use-cases", () => {
it("moves an available asset to assigned and back to available", async () => { it("moves an available asset to assigned and back to available", async () => {
const actor = await createTestUser(prisma) const actor = await createTestUser(prisma)
const recipient = await createTestRecipient(prisma) const recipient = await createTestPerson(prisma)
const item = await createTestItem(prisma, { stock: 0 }) const item = await createTestItem(prisma, { stock: 0 })
const created = await createAssetUseCase({ 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 () => { it("returns an active assignment without restoring stock when an assigned asset moves to a terminal status", async () => {
const actor = await createTestUser(prisma) const actor = await createTestUser(prisma)
const recipient = await createTestRecipient(prisma) const recipient = await createTestPerson(prisma)
const item = await createTestItem(prisma, { stock: 0 }) const item = await createTestItem(prisma, { stock: 0 })
const created = await createAssetUseCase({ const created = await createAssetUseCase({
@@ -2,7 +2,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"
import type { PrismaClient } from "@/generated/prisma/client" import type { PrismaClient } from "@/generated/prisma/client"
import { import {
createTestItem, createTestItem,
createTestRecipient, createTestPerson,
createTestUser, createTestUser,
} from "../helpers/factories" } from "../helpers/factories"
import { import {
@@ -38,7 +38,7 @@ afterAll(async () => {
describe("assignment use-cases", () => { describe("assignment use-cases", () => {
it("creates an assignment, decrements stock, and records an ASSIGNMENT movement", async () => { it("creates an assignment, decrements stock, and records an ASSIGNMENT movement", async () => {
const actor = await createTestUser(prisma) const actor = await createTestUser(prisma)
const recipient = await createTestRecipient(prisma) const recipient = await createTestPerson(prisma)
const item = await createTestItem(prisma, { stock: 5 }) const item = await createTestItem(prisma, { stock: 5 })
const assignmentDate = new Date("2026-01-01T00:00:00.000Z") 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 () => { it("rejects assignment creation when item stock is insufficient", async () => {
const actor = await createTestUser(prisma) const actor = await createTestUser(prisma)
const recipient = await createTestRecipient(prisma) const recipient = await createTestPerson(prisma)
const item = await createTestItem(prisma, { stock: 1 }) const item = await createTestItem(prisma, { stock: 1 })
const result = await createAssignmentUseCase({ 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 () => { it("returns an assignment, restores stock, closes it, and records a RETURN movement", async () => {
const actor = await createTestUser(prisma) const actor = await createTestUser(prisma)
const recipient = await createTestRecipient(prisma) const recipient = await createTestPerson(prisma)
const item = await createTestItem(prisma, { stock: 4 }) const item = await createTestItem(prisma, { stock: 4 })
const created = await createAssignmentUseCase({ const created = await createAssignmentUseCase({
@@ -172,7 +172,7 @@ describe("assignment use-cases", () => {
it("rejects returning the same assignment twice", async () => { it("rejects returning the same assignment twice", async () => {
const actor = await createTestUser(prisma) const actor = await createTestUser(prisma)
const recipient = await createTestRecipient(prisma) const recipient = await createTestPerson(prisma)
const item = await createTestItem(prisma, { stock: 2 }) const item = await createTestItem(prisma, { stock: 2 })
const created = await createAssignmentUseCase({ const created = await createAssignmentUseCase({
@@ -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")
})
})
@@ -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)
})
})