From ab4adf1174597db86406efe47d9b8807e7fd6056 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Sun, 5 Jul 2026 20:46:05 +0800 Subject: [PATCH] P2-14: Deposit installment tracking + refund approval flow - Add DepositInstallment entity (id, depositId, amount, dueDate, paidDate, status, createdAt) - Add installments OneToMany relation to Deposit entity with cascade+eager - Add refund approval fields: refundStatus, refundRequestedAt, refundApprovedBy, refundApprovedAt - Add installment DTOs (CreateInstallmentDto, UpdateInstallmentDto) - Add refund approval DTOs (ApproveRefundDto, CreateDepositWithInstallmentsDto) - Service: add/create/update/delete installments, requestRefund, approveRefund, findPendingRefunds - Controller: GET deposits/:id, GET pending-refunds, POST :id/installments, PUT installments/:id, DELETE installments/:id, POST :id/request-refund, PUT :id/approve-refund - Frontend: detail modal with installment list, refund request button, pending refunds tab with approve actions --- apps/admin/src/pages/Deposits/index.tsx | 421 +++++++++++++++--- .../src/deposits/deposits.controller.ts | 78 +++- apps/server/src/deposits/deposits.module.ts | 3 +- apps/server/src/deposits/deposits.service.ts | 135 +++++- apps/server/src/deposits/dto/deposit.dto.ts | 34 +- .../entities/deposit-installment.entity.ts | 37 ++ apps/server/src/entities/deposit.entity.ts | 18 + apps/server/src/entities/index.ts | 1 + 8 files changed, 645 insertions(+), 82 deletions(-) create mode 100644 apps/server/src/entities/deposit-installment.entity.ts diff --git a/apps/admin/src/pages/Deposits/index.tsx b/apps/admin/src/pages/Deposits/index.tsx index 1992b2d..92989e7 100644 --- a/apps/admin/src/pages/Deposits/index.tsx +++ b/apps/admin/src/pages/Deposits/index.tsx @@ -12,8 +12,11 @@ import { message, Tag, Popconfirm, + Tabs, + List, + Card, } from 'antd'; -import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'; +import { PlusOutlined, DeleteOutlined, DollarOutlined, CheckOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import api from '../../api'; import PermissionButton from '../../components/PermissionButton'; @@ -25,16 +28,34 @@ const statusMap: Record = { deducted: { text: '已全扣', color: 'red' }, }; +const refundStatusMap: Record = { + pending: { text: '待班主任审批', color: 'orange' }, + head_teacher_approved: { text: '待财务审批', color: 'blue' }, + finance_approved: { text: '已退款', color: 'green' }, + refunded: { text: '已退款', color: 'green' }, +}; + +const installmentStatusMap: Record = { + pending: { text: '待缴', color: 'orange' }, + paid: { text: '已缴', color: 'green' }, +}; + const DepositsPage: React.FC = () => { const [data, setData] = useState([]); const [students, setStudents] = useState([]); const [loading, setLoading] = useState(false); const [createModal, setCreateModal] = useState(false); const [refundModal, setRefundModal] = useState(null); + const [detailModal, setDetailModal] = useState(null); + const [installmentModal, setInstallmentModal] = useState(null); + const [pendingRefunds, setPendingRefunds] = useState([]); + const [pendingLoading, setPendingLoading] = useState(false); const [createForm] = Form.useForm(); const [refundForm] = Form.useForm(); + const [installmentForm] = Form.useForm(); const [searchText, setSearchText] = useState(''); const [filterStatus, setFilterStatus] = useState(undefined); + const [activeTab, setActiveTab] = useState('all'); const fetchData = async () => { setLoading(true); @@ -48,6 +69,17 @@ const DepositsPage: React.FC = () => { setLoading(false); }; + const fetchPendingRefunds = async () => { + setPendingLoading(true); + try { + const res = await api.get('/deposits/pending-refunds'); + setPendingRefunds(res || []); + } catch (e) { + console.error(e); + } + setPendingLoading(false); + }; + useEffect(() => { fetchData(); }, []); @@ -99,6 +131,67 @@ const DepositsPage: React.FC = () => { } }; + const handleRequestRefund = async (record: any) => { + try { + await api.post(`/deposits/${record.id}/request-refund`); + message.success('退款申请已提交'); + fetchData(); + } catch (e: any) { + message.error(e?.message || '操作失败'); + } + }; + + const handleApproveRefund = async (record: any) => { + try { + await api.put(`/deposits/${record.id}/approve-refund`); + message.success('审批通过'); + fetchPendingRefunds(); + fetchData(); + } catch (e: any) { + message.error(e?.message || '操作失败'); + } + }; + + const handleAddInstallment = async () => { + if (installmentModal == null) return; + const values = await installmentForm.validateFields(); + try { + await api.post(`/deposits/${installmentModal}/installments`, { + amount: values.amount, + dueDate: values.dueDate.format('YYYY-MM-DD'), + }); + message.success('分期已添加'); + setInstallmentModal(null); + installmentForm.resetFields(); + fetchData(); + } catch (e: any) { + message.error(e?.message || '操作失败'); + } + }; + + const handlePayInstallment = async (installmentId: number) => { + try { + await api.put(`/deposits/installments/${installmentId}`, { + paidDate: dayjs().format('YYYY-MM-DD'), + status: 'paid', + }); + message.success('分期已标记为已缴'); + fetchData(); + } catch (e: any) { + message.error(e?.message || '操作失败'); + } + }; + + const handleDeleteInstallment = async (installmentId: number) => { + try { + await api.delete(`/deposits/installments/${installmentId}`); + message.success('分期已删除'); + fetchData(); + } catch (e: any) { + message.error(e?.message || '操作失败'); + } + }; + const columns = [ { title: '学生', render: (_: any, r: any) => r.student?.name || '-' }, { title: '押金金额', dataIndex: 'amount', render: (v: number) => `¥${Number(v).toFixed(2)}` }, @@ -108,6 +201,12 @@ const DepositsPage: React.FC = () => { dataIndex: 'status', render: (s: string) => {statusMap[s]?.text || s}, }, + { + title: '退款审批', + dataIndex: 'refundStatus', + render: (s: string) => + s ? {refundStatusMap[s]?.text || s} : '-', + }, { title: '退还金额', dataIndex: 'refundAmount', @@ -123,21 +222,39 @@ const DepositsPage: React.FC = () => { { title: '备注', dataIndex: 'notes', render: (v: any) => v || '-' }, { title: '操作', - width: 160, + width: 240, render: (_: any, record: any) => ( - {record.status === 'paid' && ( - { - setRefundModal(record); - refundForm.setFieldsValue({ refundDate: dayjs(), deductionAmount: 0 }); - }} - > - 退还 - + { + setDetailModal(record); + }} + > + 详情 + + {record.status === 'paid' && !record.refundStatus && ( + <> + { + setRefundModal(record); + refundForm.setFieldsValue({ refundDate: dayjs(), deductionAmount: 0 }); + }} + > + 退还 + + handleRequestRefund(record)} + > + 申请退款 + + )} { }, ]; + const pendingColumns = [ + { title: '学生', render: (_: any, r: any) => r.student?.name || '-' }, + { title: '押金金额', dataIndex: 'amount', render: (v: number) => `¥${Number(v).toFixed(2)}` }, + { title: '缴纳日期', dataIndex: 'paidDate' }, + { + title: '审批状态', + dataIndex: 'refundStatus', + render: (s: string) => {refundStatusMap[s]?.text || s}, + }, + { + title: '申请时间', + dataIndex: 'refundRequestedAt', + render: (v: any) => (v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-'), + }, + { + title: '操作', + render: (_: any, record: any) => ( + } + onClick={() => handleApproveRefund(record)} + > + 审批通过 + + ), + }, + ]; + return (
-
{ + setActiveTab(key); + if (key === 'pending') fetchPendingRefunds(); }} - > - - setSearchText(v)} - onChange={(e) => { - if (!e.target.value) setSearchText(''); - }} - /> - setFilterStatus(v)} + options={[ + { value: 'paid', label: '已缴' }, + { value: 'refunded', label: '已全退' }, + { value: 'partial_refund', label: '部分退还' }, + { value: 'deducted', label: '已全扣' }, + ]} + /> + + } + onClick={() => { + createForm.resetFields(); + createForm.setFieldsValue({ amount: 500, paidDate: dayjs() }); + setCreateModal(true); + }} + > + 收取押金 + +
+ `共 ${total} 条` }} + /> + + ), + }, + { + key: 'pending', + label: '待审批退款', + children: ( +
`共 ${total} 条` }} + /> + ), + }, + ]} /> + {/* Create Modal */} { + {/* Refund Modal */} { + + {/* Detail Modal */} + setDetailModal(null)} + footer={null} + width={640} + > + {detailModal && ( +
+ +

押金金额: ¥{Number(detailModal.amount).toFixed(2)}

+

缴纳日期: {detailModal.paidDate}

+

+ 状态:{' '} + + {statusMap[detailModal.status]?.text || detailModal.status} + +

+ {detailModal.refundStatus && ( +

+ 退款审批:{' '} + + {refundStatusMap[detailModal.refundStatus]?.text || detailModal.refundStatus} + +

+ )} + {detailModal.notes &&

备注: {detailModal.notes}

} +
+ + {/* Installments Section */} +
+

分期记录

+ } + onClick={() => { + setInstallmentModal(detailModal.id); + installmentForm.resetFields(); + }} + > + 添加分期 + +
+ {detailModal.installments?.length > 0 ? ( + ( + } + onClick={() => handlePayInstallment(item.id)} + > + 标记已缴 + + ), + + handleDeleteInstallment(item.id)} + > +
+ )} +
+ + {/* Add Installment Modal */} + setInstallmentModal(null)} + okText="确认" + > +
+ + + + + + + +
); }; diff --git a/apps/server/src/deposits/deposits.controller.ts b/apps/server/src/deposits/deposits.controller.ts index be65b0e..3e086da 100644 --- a/apps/server/src/deposits/deposits.controller.ts +++ b/apps/server/src/deposits/deposits.controller.ts @@ -11,7 +11,7 @@ import { Request, } from '@nestjs/common'; import { DepositsService } from './deposits.service'; -import { CreateDepositDto, RefundDepositDto } from './dto/deposit.dto'; +import { CreateDepositDto, RefundDepositDto, CreateDepositWithInstallmentsDto } from './dto/deposit.dto'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { OperationLogsService } from '../operation-logs/operation-logs.service'; import { extractRequestInfo } from '../common/request-utils'; @@ -34,15 +34,27 @@ export class DepositsController { }); } + @Get('pending-refunds') + @RequirePermission('deposit:edit') + findPendingRefunds() { + return this.service.findPendingRefunds(); + } + @Get('stats') @RequirePermission('deposit:view') getStats() { return this.service.getStats(); } + @Get(':id') + @RequirePermission('deposit:view') + findOne(@Param('id') id: string) { + return this.service.findOne(+id); + } + @Post() @RequirePermission('deposit:create') - async create(@Body() dto: CreateDepositDto, @Request() req: any) { + async create(@Body() dto: CreateDepositDto | CreateDepositWithInstallmentsDto, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.create(dto, req.user?.id); await this.logService.log({ @@ -59,6 +71,30 @@ export class DepositsController { return result; } + @Post(':id/installments') + @RequirePermission('deposit:edit') + async addInstallment( + @Param('id') id: string, + @Body() body: { amount: number; dueDate: string }, + ) { + return this.service.addInstallment(+id, body.amount, body.dueDate); + } + + @Put('installments/:installmentId') + @RequirePermission('deposit:edit') + async updateInstallment( + @Param('installmentId') installmentId: string, + @Body() body: { paidDate?: string; status?: string }, + ) { + return this.service.updateInstallment(+installmentId, body); + } + + @Delete('installments/:installmentId') + @RequirePermission('deposit:delete') + async deleteInstallment(@Param('installmentId') installmentId: string) { + return this.service.deleteInstallment(+installmentId); + } + @Put(':id/refund') @RequirePermission('deposit:edit') async refund(@Param('id') id: string, @Body() dto: RefundDepositDto, @Request() req: any) { @@ -78,6 +114,44 @@ export class DepositsController { return result; } + @Post(':id/request-refund') + @RequirePermission('deposit:edit') + async requestRefund(@Param('id') id: string, @Request() req: any) { + const { ipAddress, userAgent } = extractRequestInfo(req); + const result = await this.service.requestRefund(+id); + await this.logService.log({ + userId: req.user?.id, + username: req.user?.username, + module: '押金管理', + action: '申请退款', + targetId: +id, + targetType: 'deposit', + detail: '提交退款申请', + ipAddress, + userAgent, + }); + return result; + } + + @Put(':id/approve-refund') + @RequirePermission('deposit:approve') + async approveRefund(@Param('id') id: string, @Request() req: any) { + const { ipAddress, userAgent } = extractRequestInfo(req); + const result = await this.service.approveRefund(+id, req.user?.id); + await this.logService.log({ + userId: req.user?.id, + username: req.user?.username, + module: '押金管理', + action: '审批退款', + targetId: +id, + targetType: 'deposit', + detail: `审批通过 → ${result.refundStatus}`, + ipAddress, + userAgent, + }); + return result; + } + @Delete(':id') @RequirePermission('deposit:delete') async remove(@Param('id') id: string, @Request() req: any) { diff --git a/apps/server/src/deposits/deposits.module.ts b/apps/server/src/deposits/deposits.module.ts index 12f83bb..bcb16b6 100644 --- a/apps/server/src/deposits/deposits.module.ts +++ b/apps/server/src/deposits/deposits.module.ts @@ -1,12 +1,13 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Deposit } from '../entities/deposit.entity'; +import { DepositInstallment } from '../entities/deposit-installment.entity'; import { DepositsService } from './deposits.service'; import { DepositsController } from './deposits.controller'; import { OperationLogsModule } from '../operation-logs/operation-logs.module'; @Module({ - imports: [TypeOrmModule.forFeature([Deposit]), OperationLogsModule], + imports: [TypeOrmModule.forFeature([Deposit, DepositInstallment]), OperationLogsModule], controllers: [DepositsController], providers: [DepositsService], exports: [DepositsService], diff --git a/apps/server/src/deposits/deposits.service.ts b/apps/server/src/deposits/deposits.service.ts index 541c187..9fa9ea7 100644 --- a/apps/server/src/deposits/deposits.service.ts +++ b/apps/server/src/deposits/deposits.service.ts @@ -2,33 +2,84 @@ import { Injectable, NotFoundException, BadRequestException } from '@nestjs/comm import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Deposit } from '../entities/deposit.entity'; -import { CreateDepositDto, RefundDepositDto } from './dto/deposit.dto'; +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) {} + constructor( + @InjectRepository(Deposit) private repo: Repository, + @InjectRepository(DepositInstallment) + private installmentRepo: Repository, + ) {} 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) { - return this.repo.save( - this.repo.create({ - studentId: dto.studentId, - amount: dto.amount, - paidDate: dto.paidDate, - notes: dto.notes, - status: 'paid', - recordedBy: userId, - }), - ); + 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) => + this.installmentRepo.create({ + amount: i.amount, + dueDate: i.dueDate, + status: 'pending', + }), + ); + } + + 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) { @@ -48,9 +99,69 @@ export class DepositsService { deduction > 0 ? (refundAmount > 0 ? 'partial_refund' : 'deducted') : 'refunded'; if (dto.notes) deposit.notes = dto.notes; + // Clear refund approval flow if direct refund + deposit.refundStatus = null; + deposit.refundRequestedAt = null; + deposit.refundApprovedBy = null; + deposit.refundApprovedAt = null; + 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 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('押金记录不存在'); diff --git a/apps/server/src/deposits/dto/deposit.dto.ts b/apps/server/src/deposits/dto/deposit.dto.ts index 74ea313..2dd48b9 100644 --- a/apps/server/src/deposits/dto/deposit.dto.ts +++ b/apps/server/src/deposits/dto/deposit.dto.ts @@ -1,4 +1,5 @@ -import { IsInt, IsNumber, IsString, IsOptional } from 'class-validator'; +import { IsInt, IsNumber, IsString, IsOptional, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; export class CreateDepositDto { @IsInt() @@ -15,6 +16,25 @@ export class CreateDepositDto { notes?: string; } +export class CreateInstallmentDto { + @IsNumber() + amount: number; + + @IsString() + dueDate: string; +} + +export class UpdateInstallmentDto { + @IsOptional() + @IsString() + paidDate?: string; + + @IsOptional() + @IsString() + status?: string; +} + + export class RefundDepositDto { @IsString() refundDate: string; @@ -31,3 +51,15 @@ export class RefundDepositDto { @IsString() notes?: string; } +export class CreateDepositWithInstallmentsDto extends CreateDepositDto { + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => CreateInstallmentDto) + installments?: CreateInstallmentDto[]; +} + +export class ApproveRefundDto { + @IsOptional() + @IsString() + notes?: string; +} diff --git a/apps/server/src/entities/deposit-installment.entity.ts b/apps/server/src/entities/deposit-installment.entity.ts new file mode 100644 index 0000000..20bd67a --- /dev/null +++ b/apps/server/src/entities/deposit-installment.entity.ts @@ -0,0 +1,37 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Deposit } from './deposit.entity'; + +@Entity('deposit_installments') +export class DepositInstallment { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'deposit_id' }) + depositId: number; + + @Column({ type: 'decimal', precision: 10, scale: 2 }) + amount: number; + + @Column({ name: 'due_date', type: 'date' }) + dueDate: string; + + @Column({ name: 'paid_date', type: 'date', nullable: true }) + paidDate: string; + + @Column({ type: 'varchar', length: 20, default: 'pending' }) + status: string; // pending | paid + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @ManyToOne(() => Deposit, (d) => d.installments, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'deposit_id' }) + deposit: Deposit; +} diff --git a/apps/server/src/entities/deposit.entity.ts b/apps/server/src/entities/deposit.entity.ts index 8cad970..509bf72 100644 --- a/apps/server/src/entities/deposit.entity.ts +++ b/apps/server/src/entities/deposit.entity.ts @@ -4,9 +4,11 @@ import { Column, CreateDateColumn, ManyToOne, + OneToMany, JoinColumn, } from 'typeorm'; import { Student } from './student.entity'; +import { DepositInstallment } from './deposit-installment.entity'; @Entity('deposits') export class Deposit { @@ -44,10 +46,26 @@ export class Deposit { @Column({ name: 'recorded_by', nullable: true }) recordedBy: number; + @Column({ name: 'refund_status', length: 30, nullable: true }) + refundStatus: string | null; // pending | head_teacher_approved | finance_approved | refunded + + @Column({ name: 'refund_requested_at', nullable: true }) + refundRequestedAt: Date | null; + + @Column({ name: 'refund_approved_by', type: 'integer', nullable: true }) + refundApprovedBy: number | null; + + @Column({ name: 'refund_approved_at', nullable: true }) + refundApprovedAt: Date | null; + + @OneToMany(() => DepositInstallment, (i) => i.deposit, { cascade: true, eager: true }) + installments: DepositInstallment[]; + @CreateDateColumn({ name: 'created_at' }) createdAt: Date; @ManyToOne(() => Student, { eager: true }) @JoinColumn({ name: 'student_id' }) student: Student; + } diff --git a/apps/server/src/entities/index.ts b/apps/server/src/entities/index.ts index 7d287c6..61c4e66 100644 --- a/apps/server/src/entities/index.ts +++ b/apps/server/src/entities/index.ts @@ -8,6 +8,7 @@ export { BillItem } from './bill-item.entity'; export { User } from './user.entity'; export { OperationLog } from './operation-log.entity'; export { Deposit } from './deposit.entity'; +export { DepositInstallment } from './deposit-installment.entity'; export { Classroom } from './classroom.entity'; export { Tenant } from './tenant.entity'; export { ClassroomRental } from './classroom-rental.entity';