- Fix record payment screen fetching invoice directly instead of relying on collection provider - Fix getUnremittedPayments tenant isolation (remittance tenantId filter) - Add invoice status filter and dueDate sort support in controller/service - Allow technicians to create tickets (role decorator fix) - Enhance seed data with 10 more pending-installation clients and installation tickets
147 lines
4.3 KiB
TypeScript
147 lines
4.3 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; 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() }),
|
|
},
|
|
});
|
|
}
|
|
}
|