314 lines
7.1 KiB
TypeScript
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
|
|
}
|
|
}
|