import { randomInt } from "node:crypto"; import type { Prisma } from "@prisma/client"; import { CASES_PAGE_SIZE } from "@/lib/constants"; import type { CaseTypeValue } from "@/lib/constants"; import { trimOrUndefined } from "@/lib/utils"; import { db } from "@/server/db"; import { AppError, isPrismaUniqueConstraintError } from "@/server/errors"; import type { CaseCreateInput, CaseListQueryInput, CaseUpdateInput, } from "@/server/validation/case"; export type CaseListFilters = { search?: string; caseType?: CaseTypeValue; page: number; }; export function normalizeCaseListFilters(input: CaseListQueryInput): CaseListFilters { return { search: trimOrUndefined(input.caseSearch), caseType: input.caseType, page: 1, }; } function buildCaseWhere( ownerId: string, customerId: string, filters?: CaseListFilters, ): Prisma.CaseWhereInput { const clauses: Prisma.CaseWhereInput[] = [ { ownerId, customerId, }, ]; if (filters?.search) { clauses.push({ OR: [ { caseName: { contains: filters.search, mode: "insensitive", }, }, { opponentFullName: { contains: filters.search, mode: "insensitive", }, }, { subType: { contains: filters.search, mode: "insensitive", }, }, ], }); } if (filters?.caseType) { clauses.push({ type: filters.caseType, }); } return { AND: clauses, }; } export async function listCasesForCustomer( ownerId: string, customerId: string, filters: CaseListFilters, ) { const where = buildCaseWhere(ownerId, customerId, filters); const skip = (filters.page - 1) * CASES_PAGE_SIZE; const [totalCount, items] = await db.$transaction([ db.case.count({ where }), db.case.findMany({ where, include: { customer: true, }, orderBy: [{ status: "asc" }, { updatedAt: "desc" }], skip, take: CASES_PAGE_SIZE, }), ]); return { items, totalCount, totalPages: Math.max(1, Math.ceil(totalCount / CASES_PAGE_SIZE)), page: filters.page, pageSize: CASES_PAGE_SIZE, }; } async function generateCasePublicId(ownerId: string) { for (let attempt = 0; attempt < 20; attempt += 1) { const publicId = String(randomInt(0, 1_000_000)).padStart(6, "0"); const existingCase = await db.case.findFirst({ where: { ownerId, publicId, }, select: { id: true, }, }); if (!existingCase) { return publicId; } } throw new AppError("تعذر توليد رقم قضية جديد، حاول تاني.", 500); } export async function createCaseForCustomer( ownerId: string, customerId: string, input: CaseCreateInput, ) { const customer = await db.customer.findFirst({ where: { id: customerId, ownerId, }, }); if (!customer) { throw new AppError("العميل ده مش موجود.", 404); } for (let attempt = 0; attempt < 5; attempt += 1) { try { return await db.case.create({ data: { ownerId, customerId, caseName: input.caseName.trim(), publicId: await generateCasePublicId(ownerId), type: input.type, subType: trimOrUndefined(input.subType), opponentFullName: input.opponentFullName.trim(), description: trimOrUndefined(input.description), status: "OPEN", }, include: { customer: true, }, }); } catch (error) { if (isPrismaUniqueConstraintError(error, "publicId")) { continue; } throw error; } } throw new AppError("تعذر إنشاء القضية دلوقتي، حاول تاني.", 500); } export async function getCaseByIdForOwner( ownerId: string, customerId: string, caseId: string, ) { return db.case.findFirst({ where: { id: caseId, ownerId, customerId, }, include: { customer: true, }, }); } export async function requireCaseByIdForOwner( ownerId: string, customerId: string, caseId: string, ) { const caseItem = await getCaseByIdForOwner(ownerId, customerId, caseId); if (!caseItem) { throw new AppError("القضية دي مش موجودة.", 404); } return caseItem; } export async function updateCaseForOwner( ownerId: string, customerId: string, caseId: string, input: CaseUpdateInput, ) { await requireCaseByIdForOwner(ownerId, customerId, caseId); if (input.action === "close") { return db.case.update({ where: { id: caseId, }, data: { status: "CLOSED", closedAt: new Date(), }, include: { customer: true, }, }); } if (input.action === "reopen") { return db.case.update({ where: { id: caseId, }, data: { status: "OPEN", closedAt: null, }, include: { customer: true, }, }); } return db.case.update({ where: { id: caseId, }, data: { caseName: input.caseName.trim(), type: input.type, subType: trimOrUndefined(input.subType), opponentFullName: input.opponentFullName.trim(), description: trimOrUndefined(input.description), }, include: { customer: true, }, }); }