import { eq, and, count, inArray, isNull, isNotNull, gte, lte, type Column, type SQL } from 'drizzle-orm' import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js' import { repairTickets, repairLineItems, repairBatches, repairServiceTemplates, repairNotes, } from '../db/schema/repairs.js' import type { RepairTicketCreateInput, RepairTicketUpdateInput, RepairLineItemCreateInput, RepairLineItemUpdateInput, RepairBatchCreateInput, RepairBatchUpdateInput, RepairNoteCreateInput, RepairServiceTemplateCreateInput, RepairServiceTemplateUpdateInput, PaginationInput, } from '@lunarfront/shared/schemas' import { withPagination, withSort, buildSearchCondition, paginatedResponse, } from '../utils/pagination.js' async function generateUniqueNumber( db: PostgresJsDatabase, table: typeof repairTickets | typeof repairBatches, column: typeof repairTickets.ticketNumber | typeof repairBatches.batchNumber, ): Promise { for (let attempt = 0; attempt < 10; attempt++) { const num = String(Math.floor(100000 + Math.random() * 900000)) const [existing] = await db .select({ id: table.id }) .from(table) .where(eq(column, num)) .limit(1) if (!existing) return num } return String(Math.floor(10000000 + Math.random() * 90000000)) } export const RepairTicketService = { async create(db: PostgresJsDatabase, input: RepairTicketCreateInput) { const ticketNumber = await generateUniqueNumber( db, repairTickets, repairTickets.ticketNumber, ) const [ticket] = await db .insert(repairTickets) .values({ ticketNumber, customerName: input.customerName, customerPhone: input.customerPhone, accountId: input.accountId, locationId: input.locationId, repairBatchId: input.repairBatchId, inventoryUnitId: input.inventoryUnitId, itemDescription: input.itemDescription, serialNumber: input.serialNumber, conditionIn: input.conditionIn, conditionInNotes: input.conditionInNotes, problemDescription: input.problemDescription, technicianNotes: input.technicianNotes, assignedTechnicianId: input.assignedTechnicianId, estimatedCost: input.estimatedCost?.toString(), promisedDate: input.promisedDate ? new Date(input.promisedDate) : undefined, }) .returning() return ticket }, async getById(db: PostgresJsDatabase, id: string) { const [ticket] = await db .select() .from(repairTickets) .where(eq(repairTickets.id, id)) .limit(1) return ticket ?? null }, async list(db: PostgresJsDatabase, params: PaginationInput, filters?: { status?: string[] conditionIn?: string[] isBatch?: boolean batchNumber?: string intakeDateFrom?: string intakeDateTo?: string promisedDateFrom?: string promisedDateTo?: string completedDateFrom?: string completedDateTo?: string }) { const conditions: SQL[] = [] if (params.q) { const search = buildSearchCondition(params.q, [ repairTickets.ticketNumber, repairTickets.customerName, repairTickets.customerPhone, repairTickets.itemDescription, repairTickets.serialNumber, ]) if (search) conditions.push(search) } if (filters?.status?.length) { conditions.push(inArray(repairTickets.status, filters.status as any)) } if (filters?.conditionIn?.length) { conditions.push(inArray(repairTickets.conditionIn, filters.conditionIn as any)) } if (filters?.isBatch === true) { conditions.push(isNotNull(repairTickets.repairBatchId)) } else if (filters?.isBatch === false) { conditions.push(isNull(repairTickets.repairBatchId)) } if (filters?.intakeDateFrom) conditions.push(gte(repairTickets.intakeDate, new Date(filters.intakeDateFrom))) if (filters?.intakeDateTo) conditions.push(lte(repairTickets.intakeDate, new Date(filters.intakeDateTo))) if (filters?.promisedDateFrom) conditions.push(gte(repairTickets.promisedDate, new Date(filters.promisedDateFrom))) if (filters?.promisedDateTo) conditions.push(lte(repairTickets.promisedDate, new Date(filters.promisedDateTo))) if (filters?.completedDateFrom) conditions.push(gte(repairTickets.completedDate, new Date(filters.completedDateFrom))) if (filters?.completedDateTo) conditions.push(lte(repairTickets.completedDate, new Date(filters.completedDateTo))) const where = conditions.length > 0 ? and(...conditions) : undefined const sortableColumns: Record = { ticket_number: repairTickets.ticketNumber, customer_name: repairTickets.customerName, status: repairTickets.status, intake_date: repairTickets.intakeDate, promised_date: repairTickets.promisedDate, created_at: repairTickets.createdAt, } let query = db.select().from(repairTickets).where(where).$dynamic() query = withSort(query, params.sort, params.order, sortableColumns, repairTickets.createdAt) query = withPagination(query, params.page, params.limit) const [data, [{ total }]] = await Promise.all([ query, db.select({ total: count() }).from(repairTickets).where(where), ]) return paginatedResponse(data, total, params.page, params.limit) }, async listByBatch(db: PostgresJsDatabase, batchId: string, params: PaginationInput) { const baseWhere = eq(repairTickets.repairBatchId, batchId) const searchCondition = params.q ? buildSearchCondition(params.q, [repairTickets.ticketNumber, repairTickets.customerName, repairTickets.itemDescription]) : undefined const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere const sortableColumns: Record = { ticket_number: repairTickets.ticketNumber, customer_name: repairTickets.customerName, status: repairTickets.status, created_at: repairTickets.createdAt, } let query = db.select().from(repairTickets).where(where).$dynamic() query = withSort(query, params.sort, params.order, sortableColumns, repairTickets.createdAt) query = withPagination(query, params.page, params.limit) const [data, [{ total }]] = await Promise.all([ query, db.select({ total: count() }).from(repairTickets).where(where), ]) return paginatedResponse(data, total, params.page, params.limit) }, async listReadyForPickup(db: PostgresJsDatabase, params: PaginationInput) { const baseWhere = eq(repairTickets.status, 'ready') const searchCondition = params.q ? buildSearchCondition(params.q, [repairTickets.ticketNumber, repairTickets.customerName, repairTickets.customerPhone]) : undefined const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere let query = db.select().from(repairTickets).where(where).$dynamic() query = withSort(query, params.sort, params.order, { ticket_number: repairTickets.ticketNumber, customer_name: repairTickets.customerName, created_at: repairTickets.createdAt }, repairTickets.createdAt) query = withPagination(query, params.page, params.limit) const [data, [{ total }]] = await Promise.all([ query, db.select({ total: count() }).from(repairTickets).where(where), ]) return paginatedResponse(data, total, params.page, params.limit) }, async update(db: PostgresJsDatabase, id: string, input: RepairTicketUpdateInput) { const values: Record = { ...input, updatedAt: new Date() } if (input.estimatedCost !== undefined) values.estimatedCost = input.estimatedCost.toString() if (input.promisedDate !== undefined) values.promisedDate = input.promisedDate ? new Date(input.promisedDate) : null const [ticket] = await db .update(repairTickets) .set(values) .where(eq(repairTickets.id, id)) .returning() return ticket ?? null }, async updateStatus(db: PostgresJsDatabase, id: string, status: string) { const updates: Record = { status, updatedAt: new Date() } if (status === 'ready' || status === 'picked_up' || status === 'delivered') { updates.completedDate = new Date() } const [ticket] = await db .update(repairTickets) .set(updates) .where(eq(repairTickets.id, id)) .returning() return ticket ?? null }, async delete(db: PostgresJsDatabase, id: string) { // Soft-cancel: set status to cancelled rather than hard delete const [ticket] = await db .update(repairTickets) .set({ status: 'cancelled', updatedAt: new Date() }) .where(eq(repairTickets.id, id)) .returning() return ticket ?? null }, } export const RepairLineItemService = { async create(db: PostgresJsDatabase, input: RepairLineItemCreateInput) { const [item] = await db .insert(repairLineItems) .values({ repairTicketId: input.repairTicketId, itemType: input.itemType, description: input.description, productId: input.productId, qty: input.qty?.toString(), unitPrice: input.unitPrice?.toString(), totalPrice: input.totalPrice?.toString(), cost: input.cost?.toString(), technicianId: input.technicianId, }) .returning() return item }, async listByTicket(db: PostgresJsDatabase, ticketId: string, params: PaginationInput) { const where = eq(repairLineItems.repairTicketId, ticketId) const sortableColumns: Record = { item_type: repairLineItems.itemType, created_at: repairLineItems.createdAt, } let query = db.select().from(repairLineItems).where(where).$dynamic() query = withSort(query, params.sort, params.order, sortableColumns, repairLineItems.createdAt) query = withPagination(query, params.page, params.limit) const [data, [{ total }]] = await Promise.all([ query, db.select({ total: count() }).from(repairLineItems).where(where), ]) return paginatedResponse(data, total, params.page, params.limit) }, async update(db: PostgresJsDatabase, id: string, input: RepairLineItemUpdateInput) { const values: Record = { ...input } if (input.qty !== undefined) values.qty = input.qty.toString() if (input.unitPrice !== undefined) values.unitPrice = input.unitPrice.toString() if (input.totalPrice !== undefined) values.totalPrice = input.totalPrice.toString() if (input.cost !== undefined) values.cost = input.cost.toString() const [item] = await db .update(repairLineItems) .set(values) .where(eq(repairLineItems.id, id)) .returning() return item ?? null }, async delete(db: PostgresJsDatabase, id: string) { const [item] = await db .delete(repairLineItems) .where(eq(repairLineItems.id, id)) .returning() return item ?? null }, } export const RepairBatchService = { async create(db: PostgresJsDatabase, input: RepairBatchCreateInput) { const batchNumber = await generateUniqueNumber( db, repairBatches, repairBatches.batchNumber, ) const [batch] = await db .insert(repairBatches) .values({ batchNumber, accountId: input.accountId, locationId: input.locationId, contactName: input.contactName, contactPhone: input.contactPhone, contactEmail: input.contactEmail, pickupDate: input.pickupDate ? new Date(input.pickupDate) : undefined, dueDate: input.dueDate ? new Date(input.dueDate) : undefined, itemCount: input.itemCount, notes: input.notes, }) .returning() return batch }, async getById(db: PostgresJsDatabase, id: string) { const [batch] = await db .select() .from(repairBatches) .where(eq(repairBatches.id, id)) .limit(1) return batch ?? null }, async list(db: PostgresJsDatabase, params: PaginationInput) { const searchCondition = params.q ? buildSearchCondition(params.q, [repairBatches.batchNumber, repairBatches.contactName, repairBatches.contactEmail]) : undefined const sortableColumns: Record = { batch_number: repairBatches.batchNumber, status: repairBatches.status, due_date: repairBatches.dueDate, created_at: repairBatches.createdAt, } let query = db.select().from(repairBatches).where(searchCondition).$dynamic() query = withSort(query, params.sort, params.order, sortableColumns, repairBatches.createdAt) query = withPagination(query, params.page, params.limit) const [data, [{ total }]] = await Promise.all([ query, db.select({ total: count() }).from(repairBatches).where(searchCondition), ]) return paginatedResponse(data, total, params.page, params.limit) }, async update(db: PostgresJsDatabase, id: string, input: RepairBatchUpdateInput) { const values: Record = { ...input, updatedAt: new Date() } if (input.pickupDate !== undefined) values.pickupDate = input.pickupDate ? new Date(input.pickupDate) : null if (input.dueDate !== undefined) values.dueDate = input.dueDate ? new Date(input.dueDate) : null const [batch] = await db .update(repairBatches) .set(values) .where(eq(repairBatches.id, id)) .returning() return batch ?? null }, async updateStatus(db: PostgresJsDatabase, id: string, status: string) { const updates: Record = { status, updatedAt: new Date() } if (status === 'completed') updates.completedDate = new Date() if (status === 'delivered') updates.deliveredDate = new Date() const [batch] = await db .update(repairBatches) .set(updates) .where(eq(repairBatches.id, id)) .returning() return batch ?? null }, async approve(db: PostgresJsDatabase, id: string, approvedBy: string) { const [batch] = await db .update(repairBatches) .set({ approvalStatus: 'approved', approvedBy, approvedAt: new Date(), updatedAt: new Date(), }) .where(eq(repairBatches.id, id)) .returning() return batch ?? null }, async reject(db: PostgresJsDatabase, id: string) { const [batch] = await db .update(repairBatches) .set({ approvalStatus: 'rejected', updatedAt: new Date(), }) .where(eq(repairBatches.id, id)) .returning() return batch ?? null }, } export const RepairServiceTemplateService = { async create(db: PostgresJsDatabase, input: RepairServiceTemplateCreateInput) { const [template] = await db .insert(repairServiceTemplates) .values({ name: input.name, itemCategory: input.itemCategory, size: input.size, description: input.description, itemType: input.itemType, defaultPrice: input.defaultPrice?.toString(), defaultCost: input.defaultCost?.toString(), sortOrder: input.sortOrder, }) .returning() return template }, async list(db: PostgresJsDatabase, params: PaginationInput) { const baseWhere = eq(repairServiceTemplates.isActive, true) const searchCondition = params.q ? buildSearchCondition(params.q, [repairServiceTemplates.name, repairServiceTemplates.itemCategory, repairServiceTemplates.size, repairServiceTemplates.description]) : undefined const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere const sortableColumns: Record = { name: repairServiceTemplates.name, item_category: repairServiceTemplates.itemCategory, default_price: repairServiceTemplates.defaultPrice, sort_order: repairServiceTemplates.sortOrder, created_at: repairServiceTemplates.createdAt, } let query = db.select().from(repairServiceTemplates).where(where).$dynamic() query = withSort(query, params.sort, params.order, sortableColumns, repairServiceTemplates.sortOrder) query = withPagination(query, params.page, params.limit) const [data, [{ total }]] = await Promise.all([ query, db.select({ total: count() }).from(repairServiceTemplates).where(where), ]) return paginatedResponse(data, total, params.page, params.limit) }, async update(db: PostgresJsDatabase, id: string, input: RepairServiceTemplateUpdateInput) { const values: Record = { ...input, updatedAt: new Date() } if (input.defaultPrice !== undefined) values.defaultPrice = input.defaultPrice.toString() if (input.defaultCost !== undefined) values.defaultCost = input.defaultCost.toString() const [template] = await db .update(repairServiceTemplates) .set(values) .where(eq(repairServiceTemplates.id, id)) .returning() return template ?? null }, async delete(db: PostgresJsDatabase, id: string) { const [template] = await db .update(repairServiceTemplates) .set({ isActive: false, updatedAt: new Date() }) .where(eq(repairServiceTemplates.id, id)) .returning() return template ?? null }, } export const RepairNoteService = { async create(db: PostgresJsDatabase, ticketId: string, authorId: string, authorName: string, ticketStatus: string, input: RepairNoteCreateInput) { const [note] = await db .insert(repairNotes) .values({ repairTicketId: ticketId, authorId, authorName, content: input.content, visibility: input.visibility, ticketStatus: ticketStatus as any, }) .returning() return note }, async listByTicket(db: PostgresJsDatabase, ticketId: string, params: PaginationInput) { const baseWhere = eq(repairNotes.repairTicketId, ticketId) const searchCondition = params.q ? buildSearchCondition(params.q, [repairNotes.content, repairNotes.authorName]) : undefined const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere const sortableColumns: Record = { created_at: repairNotes.createdAt, author_name: repairNotes.authorName, } let query = db.select().from(repairNotes).where(where).$dynamic() query = withSort(query, params.sort, params.order, sortableColumns, repairNotes.createdAt) query = withPagination(query, params.page, params.limit) const [data, [{ total }]] = await Promise.all([ query, db.select({ total: count() }).from(repairNotes).where(where), ]) return paginatedResponse(data, total, params.page, params.limit) }, async delete(db: PostgresJsDatabase, id: string) { const [note] = await db .delete(repairNotes) .where(eq(repairNotes.id, id)) .returning() return note ?? null }, }