Files
stock-manager/src/use-cases/person.use-cases.ts
T

314 lines
7.1 KiB
TypeScript

import { Prisma, UserStatus } from "@/generated/prisma/client"
import { normalizeEmail } from "@/lib/email"
import prisma from "@/lib/prisma"
import { getPasswordHash } from "@/lib/security"
import type {
CreatePersonFormType,
UpdatePersonFormType,
} from "@/schemas/person.schema"
import type {
UnifiedCreateFormType,
UnifiedUpdateFormType,
} from "@/schemas/user.schema"
import { PersonService } from "@/services/person.service"
import { getUserByEmail } from "@/services/user.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
}
}
export async function createPersonUserUseCase(
input: UnifiedCreateFormType,
): Promise<PersonUseCaseResult> {
const {
firstName,
lastName,
department,
email,
phone,
role,
password,
isActive,
} = input
try {
return await prisma.$transaction(async (tx) => {
// Cross-table email uniqueness: check both Person and User tables
const existingPersonEmail = await PersonService.findByEmail(email, tx)
if (existingPersonEmail) {
return personError({ email: ["Email already exists"] })
}
const existingUserEmail = await getUserByEmail(email, tx)
if (existingUserEmail) {
return personError({ email: ["Email already exists"] })
}
if (role === "NO_USER") {
// Person-only creation — no User record
await PersonService.create(
{
firstName,
lastName,
department,
email,
phone: phone ?? null,
},
tx,
)
return { success: true }
}
// Person + User creation
if (!password) {
return personError({ password: ["Password is required"] })
}
const person = await PersonService.create(
{
firstName,
lastName,
department,
email,
phone: phone ?? null,
},
tx,
)
const userName = `${firstName} ${lastName}`
const hashedPassword = await getPasswordHash(password)
const status = isActive ? UserStatus.ACTIVE : UserStatus.DISABLED
const now = new Date()
const user = await tx.user.create({
data: {
name: userName,
email,
emailNormalized: normalizeEmail(email),
passwordHash: hashedPassword,
role,
status,
...(status === UserStatus.ACTIVE ? { activatedAt: now } : {}),
passwordChangedAt: now,
},
})
await PersonService.update(
person.id,
{ user: { connect: { id: user.id } } },
tx,
)
return { success: true }
})
} catch (error) {
const errors = uniqueErrorFor(error)
if (errors) {
return personError(errors)
}
throw error
}
}
export async function updatePersonUserUseCase(
input: UnifiedUpdateFormType,
): Promise<PersonUseCaseResult> {
const {
id,
firstName,
lastName,
department,
email,
phone,
role,
isActive,
password,
} = input
try {
return await prisma.$transaction(async (tx) => {
const existing = await PersonService.findById(id, tx)
if (!existing) {
return personError({ id: ["Person not found"] })
}
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,
},
tx,
)
// If the person has a linked user, update User fields.
if (existing.userId) {
const userData: Prisma.UserUpdateInput = {}
if (role !== undefined) {
userData.role = role
}
if (isActive !== undefined) {
userData.status = isActive ? UserStatus.ACTIVE : UserStatus.DISABLED
if (isActive) {
userData.activatedAt = new Date()
}
}
if (password && password.length >= 8) {
userData.passwordHash = await getPasswordHash(password)
userData.passwordChangedAt = new Date()
}
if (Object.keys(userData).length > 0) {
await tx.user.update({
where: { id: existing.userId },
data: userData,
})
}
}
return { success: true }
})
} catch (error) {
const errors = uniqueErrorFor(error)
if (errors) {
return personError(errors)
}
throw error
}
}