import { eq, and, count, sum, sql, type Column } from 'drizzle-orm' import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js' import { drawerSessions, drawerAdjustments, transactions } from '../db/schema/pos.js' import { ConflictError, NotFoundError } from '../lib/errors.js' import type { DrawerOpenInput, DrawerCloseInput, DrawerAdjustmentInput, PaginationInput } from '@lunarfront/shared/schemas' import { withPagination, withSort, paginatedResponse } from '../utils/pagination.js' export const DrawerService = { async open(db: PostgresJsDatabase, input: DrawerOpenInput, openedBy: string) { // Ensure no other open session at this location if (input.locationId) { const existing = await this.getOpen(db, input.locationId) if (existing) { throw new ConflictError('A drawer session is already open at this location') } } const [session] = await db .insert(drawerSessions) .values({ locationId: input.locationId, registerId: input.registerId, openedBy, openingBalance: input.openingBalance.toString(), }) .returning() return session }, async close(db: PostgresJsDatabase, sessionId: string, input: DrawerCloseInput, closedBy: string) { const session = await this.getById(db, sessionId) if (!session) throw new NotFoundError('Drawer session') if (session.status === 'closed') throw new ConflictError('Drawer session is already closed') // Calculate expected balance from cash transactions in this drawer session // Net cash kept = total + rounding_adjustment (change is already accounted for) const [cashTotals] = await db .select({ total: sum(transactions.total), rounding: sum(transactions.roundingAdjustment), }) .from(transactions) .where( and( eq(transactions.drawerSessionId, sessionId), eq(transactions.status, 'completed'), eq(transactions.paymentMethod, 'cash') ) ) // Calculate net drawer adjustments (cash_in adds, cash_out subtracts) const [adjTotals] = await db .select({ cashIn: sql`coalesce(sum(case when ${drawerAdjustments.type} = 'cash_in' then ${drawerAdjustments.amount} else 0 end), 0)`, cashOut: sql`coalesce(sum(case when ${drawerAdjustments.type} = 'cash_out' then ${drawerAdjustments.amount} else 0 end), 0)`, }) .from(drawerAdjustments) .where(eq(drawerAdjustments.drawerSessionId, sessionId)) const salesCashIn = parseFloat(cashTotals?.total ?? '0') + parseFloat(cashTotals?.rounding ?? '0') const adjCashIn = parseFloat(adjTotals?.cashIn ?? '0') const adjCashOut = parseFloat(adjTotals?.cashOut ?? '0') const openingBalance = parseFloat(session.openingBalance) const expectedBalance = openingBalance + salesCashIn + adjCashIn - adjCashOut const closingBalance = input.closingBalance const overShort = closingBalance - expectedBalance const [updated] = await db .update(drawerSessions) .set({ closedBy, closingBalance: closingBalance.toString(), expectedBalance: expectedBalance.toString(), overShort: overShort.toString(), denominations: input.denominations, notes: input.notes, status: 'closed', closedAt: new Date(), }) .where(eq(drawerSessions.id, sessionId)) .returning() return updated }, async getOpen(db: PostgresJsDatabase, locationId: string) { const [session] = await db .select() .from(drawerSessions) .where( and( eq(drawerSessions.locationId, locationId), eq(drawerSessions.status, 'open') ) ) .limit(1) return session ?? null }, async getById(db: PostgresJsDatabase, id: string) { const [session] = await db .select() .from(drawerSessions) .where(eq(drawerSessions.id, id)) .limit(1) return session ?? null }, async addAdjustment(db: PostgresJsDatabase, sessionId: string, input: DrawerAdjustmentInput, createdBy: string, _approvedBy?: string) { const session = await this.getById(db, sessionId) if (!session) throw new NotFoundError('Drawer session') if (session.status === 'closed') throw new ConflictError('Cannot adjust a closed drawer') const [adjustment] = await db .insert(drawerAdjustments) .values({ drawerSessionId: sessionId, type: input.type, amount: input.amount.toString(), reason: input.reason, createdBy, }) .returning() return adjustment }, async getAdjustments(db: PostgresJsDatabase, sessionId: string) { return db .select() .from(drawerAdjustments) .where(eq(drawerAdjustments.drawerSessionId, sessionId)) }, async list(db: PostgresJsDatabase, params: PaginationInput) { const sortableColumns: Record = { opened_at: drawerSessions.openedAt, closed_at: drawerSessions.closedAt, status: drawerSessions.status, } let query = db.select().from(drawerSessions).$dynamic() query = withSort(query, params.sort, params.order, sortableColumns, drawerSessions.openedAt) query = withPagination(query, params.page, params.limit) const [data, [{ total }]] = await Promise.all([ query, db.select({ total: count() }).from(drawerSessions), ]) return paginatedResponse(data, total, params.page, params.limit) }, }