fix: 归档删除改为软删除
This commit is contained in:
@@ -16,6 +16,7 @@ function createService(bills: Partial<Bill>[] = []) {
|
||||
find: jest.fn().mockResolvedValue(bills),
|
||||
findOne: jest.fn().mockResolvedValue(bills[0] ?? null),
|
||||
save: jest.fn(async (value) => value),
|
||||
update: jest.fn(),
|
||||
createQueryBuilder: jest.fn(() => queryBuilder()),
|
||||
};
|
||||
const manager = {
|
||||
@@ -55,23 +56,26 @@ describe('BillsService state and batch boundaries', () => {
|
||||
expect(billRepo.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects an empty batch delete', async () => {
|
||||
it('rejects an empty batch archive', async () => {
|
||||
const { service, dataSource } = createService();
|
||||
await expect(service.batchRemove([])).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(dataSource.transaction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects a batch delete when some ids do not exist', async () => {
|
||||
it('rejects a batch archive when some ids do not exist', async () => {
|
||||
const { service, dataSource } = createService([{ id: 1, paidAmount: 0, status: 'unpaid' }]);
|
||||
await expect(service.batchRemove([1, 2])).rejects.toBeInstanceOf(NotFoundException);
|
||||
expect(dataSource.transaction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('deletes a bill and its links in one transaction', async () => {
|
||||
const { service, dataSource, manager } = createService([{ id: 1, paidAmount: 0, status: 'unpaid' }]);
|
||||
await expect(service.remove(1)).resolves.toEqual({ message: '账单已删除' });
|
||||
expect(dataSource.transaction).toHaveBeenCalledTimes(1);
|
||||
expect(manager.delete).toHaveBeenCalledTimes(2);
|
||||
expect(manager.update).toHaveBeenCalledTimes(1);
|
||||
it('archives an unpaid bill without deleting rows', async () => {
|
||||
const { service, billRepo, dataSource, manager } = createService([{ id: 1, paidAmount: 0, status: 'unpaid' }]);
|
||||
await expect(service.remove(1)).resolves.toEqual({ message: '账单已归档' });
|
||||
expect(billRepo.save).not.toHaveBeenCalled();
|
||||
expect(billRepo.createQueryBuilder).not.toHaveBeenCalled();
|
||||
expect(billRepo.findOne).toHaveBeenCalledWith({ where: { id: 1 } });
|
||||
expect((billRepo as any).update).toHaveBeenCalledWith(1, expect.objectContaining({ status: 'cancelled' }));
|
||||
expect(dataSource.transaction).not.toHaveBeenCalled();
|
||||
expect(manager.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -187,7 +187,7 @@ export class BillsController {
|
||||
userId: req.user?.id,
|
||||
username: req.user?.username,
|
||||
module: '账单管理',
|
||||
action: '删除账单',
|
||||
action: '归档账单',
|
||||
targetId: id,
|
||||
targetType: 'bill',
|
||||
ipAddress,
|
||||
@@ -205,7 +205,7 @@ export class BillsController {
|
||||
userId: req.user?.id,
|
||||
username: req.user?.username,
|
||||
module: '账单管理',
|
||||
action: '批量删除账单',
|
||||
action: '批量归档账单',
|
||||
detail: `IDs: ${body.ids.join(',')}`,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
|
||||
@@ -71,6 +71,7 @@ export class BillsService {
|
||||
periodStart,
|
||||
periodEnd,
|
||||
})
|
||||
.andWhere('e.status = :status', { status: 'active' })
|
||||
.getMany();
|
||||
|
||||
// 按宿舍分组费用
|
||||
@@ -177,6 +178,7 @@ export class BillsService {
|
||||
periodStart,
|
||||
periodEnd,
|
||||
})
|
||||
.andWhere('pe.status = :status', { status: 'active' })
|
||||
.andWhere('pe.billId IS NULL')
|
||||
.getMany();
|
||||
|
||||
@@ -392,31 +394,42 @@ export class BillsService {
|
||||
async remove(id: number) {
|
||||
const exists = await this.billRepo.findOne({ where: { id } });
|
||||
if (!exists) throw new NotFoundException('账单不存在');
|
||||
if (Number(exists.paidAmount) > 0 || exists.status === 'cancelled') {
|
||||
throw new BadRequestException('已发生资金流水的账单不能删除,请使用取消账单');
|
||||
if (exists.status === 'cancelled') throw new BadRequestException('账单已归档');
|
||||
if (Number(exists.paidAmount) > 0) {
|
||||
throw new BadRequestException('已发生资金流水的账单请使用取消账单并冲正');
|
||||
}
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
await manager.delete(BillItem, { billId: id });
|
||||
await manager.update(PersonalExpense, { billId: id }, { billId: null });
|
||||
await manager.delete(Bill, id);
|
||||
await this.billRepo.update(id, {
|
||||
status: 'cancelled',
|
||||
outstandingAmount: 0,
|
||||
cancelReason: '归档未支付账单',
|
||||
cancelledAt: new Date(),
|
||||
});
|
||||
return { message: '账单已删除' };
|
||||
return { message: '账单已归档' };
|
||||
}
|
||||
|
||||
async batchRemove(ids: number[]) {
|
||||
const uniqueIds = [...new Set(ids || [])];
|
||||
if (uniqueIds.length === 0) throw new BadRequestException('请选择要删除的账单');
|
||||
if (uniqueIds.length === 0) throw new BadRequestException('请选择要归档的账单');
|
||||
const bills = await this.billRepo.find({ where: { id: In(uniqueIds) } });
|
||||
if (bills.length !== uniqueIds.length) throw new NotFoundException('部分账单不存在');
|
||||
if (bills.some((bill) => Number(bill.paidAmount) > 0 || bill.status === 'cancelled')) {
|
||||
throw new BadRequestException('选中账单包含资金流水,不能批量删除');
|
||||
if (bills.some((bill) => Number(bill.paidAmount) > 0)) {
|
||||
throw new BadRequestException('选中账单包含资金流水,请逐条取消并冲正');
|
||||
}
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
await manager.delete(BillItem, { billId: In(uniqueIds) });
|
||||
await manager.update(PersonalExpense, { billId: In(uniqueIds) }, { billId: null });
|
||||
await manager.delete(Bill, uniqueIds);
|
||||
});
|
||||
return { message: `成功删除 ${uniqueIds.length} 条账单` };
|
||||
const targetIds = bills.filter((bill) => bill.status !== 'cancelled').map((bill) => bill.id);
|
||||
if (targetIds.length > 0) {
|
||||
await this.billRepo
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
.set({
|
||||
status: 'cancelled',
|
||||
outstandingAmount: 0,
|
||||
cancelReason: '批量归档未支付账单',
|
||||
cancelledAt: new Date(),
|
||||
})
|
||||
.where('id IN (:...ids)', { ids: targetIds })
|
||||
.execute();
|
||||
}
|
||||
return { message: `成功归档 ${targetIds.length} 条账单`, archived: targetIds.length };
|
||||
}
|
||||
|
||||
private assertStatusMatchesAmounts(bill: Bill, status: string) {
|
||||
|
||||
Reference in New Issue
Block a user