207 lines
7.3 KiB
TypeScript
207 lines
7.3 KiB
TypeScript
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<Deposit>,
|
|
@InjectRepository(DepositInstallment)
|
|
private installmentRepo: Repository<DepositInstallment>,
|
|
@InjectRepository(Student)
|
|
private studentRepo: Repository<Student>,
|
|
) {}
|
|
|
|
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<string, string> = {
|
|
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();
|
|
}
|
|
}
|