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
This commit is contained in:
2026-07-05 20:46:05 +08:00
parent 5df70a8af0
commit ab4adf1174
8 changed files with 645 additions and 82 deletions

View File

@@ -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) {