Files
fiberops-api/src/invoice/invoice.service.ts
2026-05-04 12:34:11 +08:00

137 lines
4.0 KiB
TypeScript

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; page?: number; limit?: number }) {
const { skip, take, page, limit } = paginationArgs({ page: filters?.page, limit: filters?.limit });
const db = this.prisma.forTenant(tenantId);
const where = {
...(filters?.clientId && { clientId: filters.clientId }),
...(filters?.status && { status: filters.status }),
};
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: { createdAt: 'desc' },
}),
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() }),
},
});
}
}