import { Injectable, NotFoundException, BadRequestException, } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { paginationArgs, paginatedResult } from '../common/dto/pagination.dto'; @Injectable() export class InvoiceService { constructor(private readonly prisma: PrismaService) {} async findAll(tenantId: string, filters?: { clientId?: string; status?: string; sort?: string; page?: number; limit?: number }) { const { skip, take, page, limit } = paginationArgs({ page: filters?.page, limit: filters?.limit }); const db = this.prisma.forTenant(tenantId); const statusFilter = filters?.status ? (filters.status.includes(',') ? { in: filters.status.split(',') } : filters.status) : undefined; const where = { ...(filters?.clientId && { clientId: filters.clientId }), ...(statusFilter && { status: statusFilter }), }; const orderBy = filters?.sort === 'dueDate:asc' ? { dueDate: 'asc' as const } : { createdAt: 'desc' as const }; const [items, total] = await Promise.all([ db.invoice.findMany({ where, skip, take, include: { client: { select: { id: true, firstName: true, lastName: true, accountNumber: true, phone: true, latitude: true, longitude: true } }, _count: { select: { payments: true } }, }, orderBy, }), db.invoice.count({ where }), ]); return paginatedResult(items, total, page, limit); } async findById(tenantId: string, id: string) { const db = this.prisma.forTenant(tenantId); const invoice = await db.invoice.findFirst({ where: { id }, include: { client: true, payments: { include: { collectedBy: { select: { id: true, firstName: true, lastName: true } }, }, orderBy: { createdAt: 'desc' }, }, }, }); if (!invoice) { throw new NotFoundException('Invoice not found'); } return invoice; } async generateForClient(tenantId: string, clientId: string) { const subscription = await this.prisma.subscription.findFirst({ where: { clientId, tenantId, status: 'active' }, include: { plan: true, client: true }, }); if (!subscription) { throw new BadRequestException('No active subscription for this client'); } const invoiceCount = await this.prisma.invoice.count({ where: { tenantId } }); const now = new Date(); const dueDate = new Date(now); dueDate.setDate(dueDate.getDate() + (subscription.plan.billingCycle || 30)); return this.prisma.invoice.create({ data: { tenantId, clientId, number: `INV-${String(invoiceCount + 1).padStart(6, '0')}`, amount: subscription.plan.price, balance: subscription.plan.price, status: 'sent', dueDate, periodStart: now, periodEnd: dueDate, }, include: { client: { select: { id: true, firstName: true, lastName: true, accountNumber: true, phone: true, latitude: true, longitude: true } }, }, }); } async voidInvoice(tenantId: string, id: string) { const db = this.prisma.forTenant(tenantId); const invoice = await db.invoice.findFirst({ where: { id } }); if (!invoice) { throw new NotFoundException('Invoice not found'); } if (invoice.status === 'paid') { throw new BadRequestException('Cannot void a paid invoice'); } return this.prisma.invoice.update({ where: { id }, data: { status: 'void' }, }); } async applyPayment(tenantId: string, invoiceId: string, amount: number) { const invoice = await this.prisma.invoice.findFirst({ where: { id: invoiceId, tenantId }, }); if (!invoice) { throw new NotFoundException('Invoice not found'); } const newBalance = Number(invoice.balance) - amount; let newStatus = invoice.status; if (newBalance <= 0) { newStatus = 'paid'; } else if (newBalance < Number(invoice.amount)) { newStatus = 'partial'; } return this.prisma.invoice.update({ where: { id: invoiceId }, data: { balance: Math.max(0, newBalance), status: newStatus, ...(newStatus === 'paid' && { paidAt: new Date() }), }, }); } }