154 lines
5.6 KiB
TypeScript
154 lines
5.6 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 } from './dto/deposit.dto';
|
|
|
|
const money = (value: number | string | null | undefined) => Number(Number(value || 0).toFixed(2));
|
|
|
|
@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 amount = money(dto.amount);
|
|
if (!Number.isFinite(dto.amount) || Math.abs(dto.amount * 100 - Math.round(dto.amount * 100)) > 1e-8) {
|
|
throw new BadRequestException('收取金额最多保留两位小数');
|
|
}
|
|
if (amount <= 0) throw new BadRequestException('收取金额必须大于0');
|
|
|
|
const existing = await this.repo.findOne({ where: { studentId: dto.studentId } });
|
|
if (existing) {
|
|
existing.amount = money(Number(existing.amount || 0) + amount);
|
|
existing.paidDate = dto.paidDate;
|
|
existing.status = 'paid';
|
|
existing.recordedBy = userId ?? null;
|
|
existing.refundDate = null as unknown as string;
|
|
existing.refundAmount = null as unknown as number;
|
|
existing.refundedBy = null;
|
|
existing.refundedAt = null;
|
|
if (dto.notes) existing.notes = dto.notes;
|
|
return this.repo.save(existing);
|
|
}
|
|
|
|
return this.repo.save(
|
|
this.repo.create({
|
|
studentId: dto.studentId,
|
|
amount,
|
|
paidDate: dto.paidDate,
|
|
notes: dto.notes,
|
|
status: 'paid',
|
|
recordedBy: userId,
|
|
}),
|
|
);
|
|
}
|
|
|
|
async addInstallment(depositId: number, amount: number, dueDate: string) {
|
|
const normalizedAmount = money(amount);
|
|
if (!Number.isFinite(amount) || Math.abs(amount * 100 - Math.round(amount * 100)) > 1e-8) {
|
|
throw new BadRequestException('分期金额最多保留两位小数');
|
|
}
|
|
if (normalizedAmount <= 0) throw new BadRequestException('分期金额必须大于0');
|
|
const deposit = await this.repo.findOne({ where: { id: depositId } });
|
|
if (!deposit) throw new NotFoundException('押金记录不存在');
|
|
|
|
const installment = this.installmentRepo.create({
|
|
depositId,
|
|
amount: normalizedAmount,
|
|
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' || Number(deposit.amount) <= 0) {
|
|
throw new BadRequestException('该学生当前没有可退押金');
|
|
}
|
|
|
|
const refundAmount = money(deposit.amount);
|
|
|
|
deposit.refundDate = dto.refundDate;
|
|
deposit.refundAmount = refundAmount;
|
|
deposit.amount = 0;
|
|
deposit.status = 'refunded';
|
|
if (dto.notes) deposit.notes = dto.notes;
|
|
deposit.refundedBy = userId ?? null;
|
|
deposit.refundedAt = new Date();
|
|
|
|
return this.repo.save(deposit);
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|