/** * TicketCategoryService — Admin-configurable ticket category CRUD. * * ARCHITECTURE: * - Categories are tenant-scoped and admin-configurable * - Default ISP categories are seeded at tenant creation (see tenant.ts) * - Deactivated categories (isActive=false) cannot be used for new tickets * - Soft-deactivation preserves category history on existing tickets */ // eslint-disable-next-line @typescript-eslint/no-explicit-any type TenantPrismaClient = any; // --------------------------------------------------------------------------- // Input/output types // --------------------------------------------------------------------------- export interface CreateCategoryInput { name: string; description?: string; } export interface UpdateCategoryInput { name?: string; description?: string; isActive?: boolean; } // --------------------------------------------------------------------------- // createCategory // --------------------------------------------------------------------------- /** * Create a new ticket category for this tenant. * * @throws Error if name is blank or already exists in tenant */ export async function createCategory( tenantPrisma: TenantPrismaClient, tenantId: string, input: CreateCategoryInput ) { const { name, description } = input; if (!name || !name.trim()) { throw new Error("Category name is required"); } try { return await tenantPrisma.ticketCategory.create({ data: { tenantId, name: name.trim(), description: description?.trim() ?? null, }, }); } catch (err) { const message = err instanceof Error ? err.message : String(err); if (message.includes("Unique constraint") || message.includes("P2002")) { throw new Error(`Category name "${name.trim()}" already exists`); } throw err; } } // --------------------------------------------------------------------------- // updateCategory // --------------------------------------------------------------------------- /** * Update a category's name, description, or isActive flag. * Used by admins to rename or deactivate categories. * * @throws Error if category not found */ export async function updateCategory( tenantPrisma: TenantPrismaClient, categoryId: string, input: UpdateCategoryInput ) { const { name, description, isActive } = input; const data: Record = {}; if (name !== undefined) data.name = name.trim(); if (description !== undefined) data.description = description.trim() || null; if (isActive !== undefined) data.isActive = isActive; try { return await tenantPrisma.ticketCategory.update({ where: { id: categoryId }, data, }); } catch (err) { const message = err instanceof Error ? err.message : String(err); if (message.includes("Record to update not found") || message.includes("P2025")) { throw new Error(`Category not found: ${categoryId}`); } throw err; } } // --------------------------------------------------------------------------- // listCategories // --------------------------------------------------------------------------- /** * List all categories for this tenant. * * @param options.activeOnly - If true, only return isActive=true categories (default: false) */ export async function listCategories( tenantPrisma: TenantPrismaClient, options: { activeOnly?: boolean } = {} ) { const { activeOnly = false } = options; return tenantPrisma.ticketCategory.findMany({ where: activeOnly ? { isActive: true } : undefined, orderBy: { name: "asc" }, }); }