import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { TenantService } from '../../database/tenant.service'; @Injectable() export class PaymentsService { constructor(private tenant: TenantService) {} async findAll() { return this.tenant.query(` SELECT p.*, u.unit_number, i.invoice_number FROM payments p JOIN units u ON u.id = p.unit_id LEFT JOIN invoices i ON i.id = p.invoice_id ORDER BY p.payment_date DESC, p.created_at DESC `); } async findOne(id: string) { const rows = await this.tenant.query(` SELECT p.*, u.unit_number, i.invoice_number FROM payments p JOIN units u ON u.id = p.unit_id LEFT JOIN invoices i ON i.id = p.invoice_id WHERE p.id = $1`, [id]); if (!rows.length) throw new NotFoundException('Payment not found'); return rows[0]; } async create(dto: any, userId: string) { // Validate invoice exists and get details let invoice: any = null; if (dto.invoice_id) { const rows = await this.tenant.query('SELECT * FROM invoices WHERE id = $1', [dto.invoice_id]); if (!rows.length) throw new NotFoundException('Invoice not found'); invoice = rows[0]; if (invoice.status === 'paid') throw new BadRequestException('Invoice is already paid'); if (invoice.status === 'void') throw new BadRequestException('Cannot pay void invoice'); } // Get fiscal period const payDate = new Date(dto.payment_date); let fp = await this.tenant.query( 'SELECT id FROM fiscal_periods WHERE year = $1 AND month = $2', [payDate.getFullYear(), payDate.getMonth() + 1], ); if (!fp.length) { fp = await this.tenant.query( `INSERT INTO fiscal_periods (year, month, status) VALUES ($1, $2, 'open') RETURNING id`, [payDate.getFullYear(), payDate.getMonth() + 1], ); } // Create payment record const payment = await this.tenant.query( `INSERT INTO payments (unit_id, invoice_id, payment_date, amount, payment_method, reference_number, notes, received_by) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`, [dto.unit_id, dto.invoice_id || null, dto.payment_date, dto.amount, dto.payment_method || 'check', dto.reference_number || null, dto.notes || null, userId], ); // Create journal entry: DR Cash, CR Accounts Receivable const cashAccount = await this.tenant.query(`SELECT id FROM accounts WHERE account_number = '1000'`); const arAccount = await this.tenant.query(`SELECT id FROM accounts WHERE account_number = '1200'`); if (cashAccount.length && arAccount.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, 'payment', $3, 'payment', $4, true, NOW(), $5) RETURNING id`, [dto.payment_date, `Payment received - ${dto.reference_number || 'N/A'}`, fp[0].id, payment[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, cashAccount[0].id, dto.amount, arAccount[0].id], ); await this.tenant.query(`UPDATE payments SET journal_entry_id = $1 WHERE id = $2`, [je[0].id, payment[0].id]); } // Update invoice if linked — use explicit cast to avoid PostgreSQL type inference error if (invoice) { const newPaid = parseFloat(invoice.amount_paid) + parseFloat(dto.amount); const invoiceAmt = parseFloat(invoice.amount); const newStatus = newPaid >= invoiceAmt ? 'paid' : 'partial'; await this.tenant.query( `UPDATE invoices SET amount_paid = $1, status = $2::VARCHAR, paid_at = CASE WHEN $3::VARCHAR = 'paid' THEN NOW() ELSE paid_at END, updated_at = NOW() WHERE id = $4`, [newPaid, newStatus, newStatus, invoice.id], ); } return payment[0]; } async update(id: string, dto: any, userId: string) { const existing = await this.findOne(id); const sets: string[] = []; const params: any[] = []; let idx = 1; if (dto.payment_date !== undefined) { sets.push(`payment_date = $${idx++}`); params.push(dto.payment_date); } if (dto.amount !== undefined) { sets.push(`amount = $${idx++}`); params.push(dto.amount); } if (dto.payment_method !== undefined) { sets.push(`payment_method = $${idx++}`); params.push(dto.payment_method); } if (dto.reference_number !== undefined) { sets.push(`reference_number = $${idx++}`); params.push(dto.reference_number); } if (dto.notes !== undefined) { sets.push(`notes = $${idx++}`); params.push(dto.notes); } if (!sets.length) return this.findOne(id); params.push(id); await this.tenant.query( `UPDATE payments SET ${sets.join(', ')} WHERE id = $${idx} RETURNING *`, params, ); // If amount changed and payment is linked to an invoice, recalculate invoice totals if (dto.amount !== undefined && existing.invoice_id) { await this.recalculateInvoice(existing.invoice_id); } return this.findOne(id); } async delete(id: string) { const payment = await this.findOne(id); const invoiceId = payment.invoice_id; // Delete associated journal entry lines and journal entry if (payment.journal_entry_id) { await this.tenant.query('DELETE FROM journal_entry_lines WHERE journal_entry_id = $1', [payment.journal_entry_id]); await this.tenant.query('DELETE FROM journal_entries WHERE id = $1', [payment.journal_entry_id]); } // Delete the payment await this.tenant.query('DELETE FROM payments WHERE id = $1', [id]); // Recalculate invoice totals if payment was linked if (invoiceId) { await this.recalculateInvoice(invoiceId); } return { success: true }; } private async recalculateInvoice(invoiceId: string) { // Sum all remaining payments for this invoice const result = await this.tenant.query( 'SELECT COALESCE(SUM(amount), 0) as total_paid FROM payments WHERE invoice_id = $1', [invoiceId], ); const totalPaid = parseFloat(result[0].total_paid); // Get the invoice amount const inv = await this.tenant.query('SELECT amount FROM invoices WHERE id = $1', [invoiceId]); if (!inv.length) return; const invoiceAmt = parseFloat(inv[0].amount); let newStatus: string; if (totalPaid >= invoiceAmt) { newStatus = 'paid'; } else if (totalPaid > 0) { newStatus = 'partial'; } else { newStatus = 'pending'; } await this.tenant.query( `UPDATE invoices SET amount_paid = $1, status = $2::VARCHAR, paid_at = CASE WHEN $3::VARCHAR = 'paid' THEN NOW() ELSE NULL END, updated_at = NOW() WHERE id = $4`, [totalPaid, newStatus, newStatus, invoiceId], ); } }