initial: standalone repo from monorepo split
This commit is contained in:
136
src/invoice/invoice.service.ts
Normal file
136
src/invoice/invoice.service.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
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 } },
|
||||
_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 } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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() }),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user