import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { TenantService } from '../../database/tenant.service'; @Injectable() export class InvoicesService { constructor(private tenant: TenantService) {} async findAll() { return this.tenant.query(` SELECT i.*, u.unit_number, (i.amount - i.amount_paid) as balance_due FROM invoices i JOIN units u ON u.id = i.unit_id ORDER BY i.invoice_date DESC, i.invoice_number DESC `); } async findOne(id: string) { const rows = await this.tenant.query(` SELECT i.*, u.unit_number FROM invoices i JOIN units u ON u.id = i.unit_id WHERE i.id = $1`, [id]); if (!rows.length) throw new NotFoundException('Invoice not found'); return rows[0]; } async generateBulk(dto: { month: number; year: number }, userId: string) { const units = await this.tenant.query( `SELECT * FROM units WHERE status = 'active' AND monthly_assessment > 0`, ); if (!units.length) throw new BadRequestException('No active units with assessments found'); // Get or create fiscal period let fp = await this.tenant.query( 'SELECT id FROM fiscal_periods WHERE year = $1 AND month = $2', [dto.year, dto.month], ); if (!fp.length) { fp = await this.tenant.query( `INSERT INTO fiscal_periods (year, month, status) VALUES ($1, $2, 'open') RETURNING id`, [dto.year, dto.month], ); } const fiscalPeriodId = fp[0].id; const invoiceDate = new Date(dto.year, dto.month - 1, 1); const dueDate = new Date(dto.year, dto.month - 1, 15); let created = 0; for (const unit of units) { const invNum = `INV-${dto.year}${String(dto.month).padStart(2, '0')}-${unit.unit_number}`; // Check if already generated const existing = await this.tenant.query( 'SELECT id FROM invoices WHERE invoice_number = $1', [invNum], ); if (existing.length) continue; // Create the invoice const inv = await this.tenant.query( `INSERT INTO invoices (invoice_number, unit_id, invoice_date, due_date, invoice_type, description, amount, status) VALUES ($1, $2, $3, $4, 'regular_assessment', $5, $6, 'sent') RETURNING id`, [invNum, unit.id, invoiceDate.toISOString().split('T')[0], dueDate.toISOString().split('T')[0], `Monthly assessment - ${new Date(dto.year, dto.month - 1).toLocaleString('default', { month: 'long', year: 'numeric' })}`, unit.monthly_assessment], ); // Create journal entry: DR Accounts Receivable, CR Assessment Income const arAccount = await this.tenant.query(`SELECT id FROM accounts WHERE account_number = 1200`); const incomeAccount = await this.tenant.query(`SELECT id FROM accounts WHERE account_number = 4000`); if (arAccount.length && incomeAccount.length) { const je = await this.tenant.query( `INSERT INTO journal_entries (entry_date, description, entry_type, fiscal_period_id, source_type, source_id, is_posted, posted_at, created_by) VALUES ($1, $2, 'assessment', $3, 'invoice', $4, true, NOW(), $5) RETURNING id`, [invoiceDate.toISOString().split('T')[0], `Assessment - Unit ${unit.unit_number}`, fiscalPeriodId, inv[0].id, userId], ); await this.tenant.query( `INSERT INTO journal_entry_lines (journal_entry_id, account_id, debit, credit) VALUES ($1, $2, $3, 0), ($1, $4, 0, $3)`, [je[0].id, arAccount[0].id, unit.monthly_assessment, incomeAccount[0].id], ); await this.tenant.query( `UPDATE invoices SET journal_entry_id = $1 WHERE id = $2`, [je[0].id, inv[0].id], ); } created++; } return { created, month: dto.month, year: dto.year }; } async applyLateFees(dto: { grace_period_days: number; late_fee_amount: number }, userId: string) { const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - dto.grace_period_days); const cutoffStr = cutoff.toISOString().split('T')[0]; const overdue = await this.tenant.query(` SELECT i.*, u.unit_number FROM invoices i JOIN units u ON u.id = i.unit_id WHERE i.status IN ('sent', 'partial') AND i.due_date < $1 AND NOT EXISTS ( SELECT 1 FROM invoices lf WHERE lf.unit_id = i.unit_id AND lf.invoice_type = 'late_fee' AND lf.description LIKE '%' || i.invoice_number || '%' ) `, [cutoffStr]); let applied = 0; for (const inv of overdue) { await this.tenant.query(`UPDATE invoices SET status = 'overdue' WHERE id = $1`, [inv.id]); const lfNum = `LF-${inv.invoice_number}`; await this.tenant.query( `INSERT INTO invoices (invoice_number, unit_id, invoice_date, due_date, invoice_type, description, amount, status) VALUES ($1, $2, CURRENT_DATE, CURRENT_DATE + INTERVAL '15 days', 'late_fee', $3, $4, 'sent')`, [lfNum, inv.unit_id, `Late fee for invoice ${inv.invoice_number}`, dto.late_fee_amount], ); applied++; } return { applied }; } }