import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { TenantService } from '../../database/tenant.service'; const DEFAULT_DUE_MONTHS: Record = { monthly: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], quarterly: [1, 4, 7, 10], annual: [1], }; @Injectable() export class AssessmentGroupsService { constructor(private tenant: TenantService) {} async findAll() { return this.tenant.query(` SELECT ag.*, (SELECT COUNT(*) FROM units u WHERE u.assessment_group_id = ag.id) as actual_unit_count, CASE ag.frequency WHEN 'quarterly' THEN ag.regular_assessment / 3 WHEN 'annual' THEN ag.regular_assessment / 12 ELSE ag.regular_assessment END * ag.unit_count as monthly_operating_income, CASE ag.frequency WHEN 'quarterly' THEN ag.special_assessment / 3 WHEN 'annual' THEN ag.special_assessment / 12 ELSE ag.special_assessment END * ag.unit_count as monthly_reserve_income, (CASE ag.frequency WHEN 'quarterly' THEN (ag.regular_assessment + ag.special_assessment) / 3 WHEN 'annual' THEN (ag.regular_assessment + ag.special_assessment) / 12 ELSE ag.regular_assessment + ag.special_assessment END) * ag.unit_count as total_monthly_income FROM assessment_groups ag ORDER BY ag.name `); } async findOne(id: string) { const rows = await this.tenant.query('SELECT * FROM assessment_groups WHERE id = $1', [id]); if (!rows.length) throw new NotFoundException('Assessment group not found'); return rows[0]; } async getDefault() { const rows = await this.tenant.query( 'SELECT * FROM assessment_groups WHERE is_default = true AND is_active = true LIMIT 1', ); return rows.length ? rows[0] : null; } private validateDueMonths(frequency: string, dueMonths: number[]) { if (!dueMonths || !dueMonths.length) { throw new BadRequestException('Due months are required'); } // Validate all values are 1-12 if (dueMonths.some((m) => m < 1 || m > 12 || !Number.isInteger(m))) { throw new BadRequestException('Due months must be integers between 1 and 12'); } switch (frequency) { case 'monthly': if (dueMonths.length !== 12) { throw new BadRequestException('Monthly frequency must include all 12 months'); } break; case 'quarterly': if (dueMonths.length !== 4) { throw new BadRequestException('Quarterly frequency must have exactly 4 due months'); } break; case 'annual': if (dueMonths.length !== 1) { throw new BadRequestException('Annual frequency must have exactly 1 due month'); } break; } } async create(dto: any) { const existingGroups = await this.tenant.query('SELECT COUNT(*) as cnt FROM assessment_groups'); const isFirstGroup = parseInt(existingGroups[0].cnt) === 0; const shouldBeDefault = dto.isDefault || isFirstGroup; if (shouldBeDefault) { await this.tenant.query('UPDATE assessment_groups SET is_default = false WHERE is_default = true'); } const frequency = dto.frequency || 'monthly'; const dueMonths = dto.dueMonths || DEFAULT_DUE_MONTHS[frequency] || DEFAULT_DUE_MONTHS.monthly; const dueDay = Math.min(Math.max(dto.dueDay || 1, 1), 28); this.validateDueMonths(frequency, dueMonths); const rows = await this.tenant.query( `INSERT INTO assessment_groups (name, description, regular_assessment, special_assessment, unit_count, frequency, due_months, due_day, is_default) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *`, [dto.name, dto.description || null, dto.regularAssessment || 0, dto.specialAssessment || 0, dto.unitCount || 0, frequency, dueMonths, dueDay, shouldBeDefault], ); return rows[0]; } async update(id: string, dto: any) { const existing = await this.findOne(id); if (dto.isDefault === true) { await this.tenant.query('UPDATE assessment_groups SET is_default = false WHERE is_default = true'); } const sets: string[] = []; const params: any[] = []; let idx = 1; if (dto.name !== undefined) { sets.push(`name = $${idx++}`); params.push(dto.name); } if (dto.description !== undefined) { sets.push(`description = $${idx++}`); params.push(dto.description); } if (dto.regularAssessment !== undefined) { sets.push(`regular_assessment = $${idx++}`); params.push(dto.regularAssessment); } if (dto.specialAssessment !== undefined) { sets.push(`special_assessment = $${idx++}`); params.push(dto.specialAssessment); } if (dto.unitCount !== undefined) { sets.push(`unit_count = $${idx++}`); params.push(dto.unitCount); } if (dto.isActive !== undefined) { sets.push(`is_active = $${idx++}`); params.push(dto.isActive); } if (dto.frequency !== undefined) { sets.push(`frequency = $${idx++}`); params.push(dto.frequency); } if (dto.isDefault !== undefined) { sets.push(`is_default = $${idx++}`); params.push(dto.isDefault); } // Handle due_months: if frequency changed and no explicit dueMonths, auto-populate defaults const effectiveFrequency = dto.frequency || existing.frequency; if (dto.dueMonths !== undefined) { this.validateDueMonths(effectiveFrequency, dto.dueMonths); sets.push(`due_months = $${idx++}`); params.push(dto.dueMonths); } else if (dto.frequency !== undefined && dto.frequency !== existing.frequency) { // Frequency changed, auto-populate due_months const newDueMonths = DEFAULT_DUE_MONTHS[dto.frequency] || DEFAULT_DUE_MONTHS.monthly; sets.push(`due_months = $${idx++}`); params.push(newDueMonths); } if (dto.dueDay !== undefined) { sets.push(`due_day = $${idx++}`); params.push(Math.min(Math.max(dto.dueDay, 1), 28)); } if (!sets.length) return this.findOne(id); sets.push('updated_at = NOW()'); params.push(id); const rows = await this.tenant.query( `UPDATE assessment_groups SET ${sets.join(', ')} WHERE id = $${idx} RETURNING *`, params, ); return rows[0]; } async setDefault(id: string) { await this.findOne(id); await this.tenant.query('UPDATE assessment_groups SET is_default = false WHERE is_default = true'); await this.tenant.query( 'UPDATE assessment_groups SET is_default = true, updated_at = NOW() WHERE id = $1', [id], ); return this.findOne(id); } async getSummary() { const rows = await this.tenant.query(` SELECT COUNT(*) as group_count, COALESCE(SUM( CASE frequency WHEN 'quarterly' THEN regular_assessment / 3 WHEN 'annual' THEN regular_assessment / 12 ELSE regular_assessment END * unit_count ), 0) as total_monthly_operating, COALESCE(SUM( CASE frequency WHEN 'quarterly' THEN special_assessment / 3 WHEN 'annual' THEN special_assessment / 12 ELSE special_assessment END * unit_count ), 0) as total_monthly_reserve, COALESCE(SUM( CASE frequency WHEN 'quarterly' THEN (regular_assessment + special_assessment) / 3 WHEN 'annual' THEN (regular_assessment + special_assessment) / 12 ELSE regular_assessment + special_assessment END * unit_count ), 0) as total_monthly_income, COALESCE(SUM(unit_count), 0) as total_units FROM assessment_groups WHERE is_active = true `); return rows[0]; } }