import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Deposit } from '../entities/deposit.entity'; import { Student } from '../entities/student.entity'; import { DepositInstallment } from '../entities/deposit-installment.entity'; import { CreateDepositDto, RefundDepositDto, CreateDepositWithInstallmentsDto } from './dto/deposit.dto'; @Injectable() export class DepositsService { constructor( @InjectRepository(Deposit) private repo: Repository, @InjectRepository(DepositInstallment) private installmentRepo: Repository, @InjectRepository(Student) private studentRepo: Repository, ) {} async getStudentLookups() { return this.studentRepo.find({ select: ['id', 'name', 'studentNo'], where: { status: 'active' }, order: { name: 'ASC' }, }); } async findAll(query?: { studentId?: number; status?: string }) { const qb = this.repo .createQueryBuilder('d') .leftJoinAndSelect('d.student', 'student') .leftJoinAndSelect('d.installments', 'installments') .orderBy('d.createdAt', 'DESC'); if (query?.studentId) qb.andWhere('d.studentId = :studentId', { studentId: query.studentId }); if (query?.status) qb.andWhere('d.status = :status', { status: query.status }); return qb.getMany(); } async findOne(id: number) { const deposit = await this.repo.findOne({ where: { id }, relations: ['student', 'installments'] }); if (!deposit) throw new NotFoundException('押金记录不存在'); return deposit; } async create(dto: CreateDepositDto, userId?: number) { const student = await this.studentRepo.findOne({ where: { id: dto.studentId } }); if (!student) throw new NotFoundException('学生不存在'); const deposit = this.repo.create({ studentId: dto.studentId, amount: dto.amount, paidDate: dto.paidDate, notes: dto.notes, status: 'paid', recordedBy: userId, }); if (dto instanceof CreateDepositWithInstallmentsDto && dto.installments?.length) { deposit.installments = dto.installments.map((i) => { const inst = this.installmentRepo.create({ amount: i.amount, dueDate: i.dueDate, status: 'pending', }); return inst; }); } return this.repo.save(deposit); } async addInstallment(depositId: number, amount: number, dueDate: string) { const deposit = await this.repo.findOne({ where: { id: depositId } }); if (!deposit) throw new NotFoundException('押金记录不存在'); const installment = this.installmentRepo.create({ depositId, amount, dueDate, status: 'pending', }); return this.installmentRepo.save(installment); } async updateInstallment(id: number, data: { paidDate?: string; status?: string }) { const installment = await this.installmentRepo.findOne({ where: { id } }); if (!installment) throw new NotFoundException('分期记录不存在'); if (data.paidDate !== undefined) installment.paidDate = data.paidDate; if (data.status !== undefined) installment.status = data.status; return this.installmentRepo.save(installment); } async deleteInstallment(id: number) { const installment = await this.installmentRepo.findOne({ where: { id } }); if (!installment) throw new NotFoundException('分期记录不存在'); await this.installmentRepo.delete(id); return { message: '删除成功' }; } async refund(id: number, dto: RefundDepositDto, userId?: number) { const deposit = await this.repo.findOne({ where: { id } }); if (!deposit) throw new NotFoundException('押金记录不存在'); if (deposit.status !== 'paid') throw new BadRequestException('该押金已处理'); const deduction = dto.deductionAmount || 0; const refundAmount = Number(deposit.amount) - deduction; if (refundAmount < 0) throw new BadRequestException('扣除金额不能大于押金金额'); deposit.refundDate = dto.refundDate; deposit.deductionAmount = deduction; deposit.deductionReason = dto.deductionReason || ''; deposit.refundAmount = refundAmount; deposit.status = deduction > 0 ? (refundAmount > 0 ? 'partial_refund' : 'deducted') : 'refunded'; if (dto.notes) deposit.notes = dto.notes; return this.repo.save(deposit); } // ---- Refund approval flow ---- async requestRefund(id: number) { const deposit = await this.repo.findOne({ where: { id } }); if (!deposit) throw new NotFoundException('押金记录不存在'); if (deposit.status !== 'paid') throw new BadRequestException('该押金已处理'); if (deposit.refundStatus) throw new BadRequestException('已提交退款申请,请等待审批'); deposit.refundStatus = 'pending'; deposit.refundRequestedAt = new Date(); return this.repo.save(deposit); } async approveRefund(id: number, userId: number) { const deposit = await this.repo.findOne({ where: { id } }); if (!deposit) throw new NotFoundException('押金记录不存在'); if (!deposit.refundStatus || deposit.refundStatus === 'refunded') { throw new BadRequestException('未找到待审批的退款申请'); } const transitions: Record = { pending: 'head_teacher_approved', head_teacher_approved: 'finance_approved', finance_approved: 'refunded', }; const nextStatus = transitions[deposit.refundStatus]; if (!nextStatus) throw new BadRequestException(`无效的退款状态: ${deposit.refundStatus}`); deposit.refundStatus = nextStatus; deposit.refundApprovedBy = userId; deposit.refundApprovedAt = new Date(); if (nextStatus === 'refunded') { deposit.status = 'refunded'; deposit.refundDate = new Date().toISOString().slice(0, 10); deposit.refundAmount = Number(deposit.amount) - Number(deposit.deductionAmount || 0); } return this.repo.save(deposit); } async rejectRefund(id: number, reason: string, userId: number) { const deposit = await this.repo.findOne({ where: { id } }); if (!deposit) throw new NotFoundException('押金记录不存在'); if (!deposit.refundStatus || deposit.refundStatus === 'refunded') { throw new BadRequestException('未找到待审批的退款申请'); } deposit.refundStatus = null as unknown as string; deposit.refundApprovedBy = userId; deposit.refundApprovedAt = new Date(); deposit.refundRejectedReason = reason; return this.repo.save(deposit); } async findPendingRefunds() { return this.repo.find({ where: [ { refundStatus: 'pending' }, { refundStatus: 'head_teacher_approved' }, ], relations: ['student', 'installments'], order: { refundRequestedAt: 'DESC' }, }); } async remove(id: number) { const deposit = await this.repo.findOne({ where: { id } }); if (!deposit) throw new NotFoundException('押金记录不存在'); await this.repo.delete(id); return { message: '删除成功' }; } async getStats() { const qb = this.repo .createQueryBuilder('d') .select('d.status', 'status') .addSelect('COUNT(*)', 'count') .addSelect('SUM(d.amount)', 'totalAmount'); qb.groupBy('d.status'); return qb.getRawMany(); } }