fix(c1): elder 十态校验/不可逆态守卫/待遇止>起 + statusLog 单测
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1308675f80
commit
0cbdb7f825
@ -2,6 +2,23 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsDateString, IsIn, IsInt, IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
import { PageQuery } from '../common/page.dto.js';
|
||||
|
||||
/** 十态待遇状态机(唯一来源,见 docs/superpowers/specs C1 设计 §4)。 */
|
||||
export const TREAT_STATUSES = [
|
||||
'pending_file',
|
||||
'waiting_period',
|
||||
'active',
|
||||
'hosp_pause',
|
||||
'review_waiting',
|
||||
'transferring',
|
||||
'dead',
|
||||
'long_away',
|
||||
'benefit_expired',
|
||||
'terminated',
|
||||
] as const;
|
||||
|
||||
/** 不可逆态:处于这些状态时禁止再变更。 */
|
||||
export const IRREVERSIBLE_TREAT_STATUSES: readonly string[] = ['dead', 'terminated'];
|
||||
|
||||
/**
|
||||
* 老人档案(elder)DTO。字段字典以 shared/data-model 为准(ltc_elder)。
|
||||
* - 不含 tenantCode/createdBy/createdAt/updatedAt/deletedAt(系统字段,服务端注入);
|
||||
@ -147,9 +164,10 @@ export class ListElderQuery extends PageQuery {
|
||||
}
|
||||
|
||||
export class ChangeStatusDto {
|
||||
@ApiProperty({ description: '目标待遇状态(十态之一)' })
|
||||
@ApiProperty({ description: '目标待遇状态(十态之一)', enum: TREAT_STATUSES })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsIn(TREAT_STATUSES)
|
||||
toStatus!: string;
|
||||
|
||||
@ApiProperty({ description: '变更原因(必填)' })
|
||||
|
||||
@ -41,6 +41,7 @@ function makeService(
|
||||
elderUpdate?: ReturnType<typeof vi.fn>;
|
||||
userRoleFindMany?: ReturnType<typeof vi.fn>;
|
||||
statusLogCreate?: ReturnType<typeof vi.fn>;
|
||||
statusLogFindMany?: ReturnType<typeof vi.fn>;
|
||||
auditRecord?: ReturnType<typeof vi.fn>;
|
||||
} = {},
|
||||
) {
|
||||
@ -52,6 +53,7 @@ function makeService(
|
||||
const elderUpdate = overrides.elderUpdate ?? vi.fn().mockResolvedValue({ id: 'e1' });
|
||||
const userRoleFindMany = overrides.userRoleFindMany ?? vi.fn().mockResolvedValue([]);
|
||||
const statusLogCreate = overrides.statusLogCreate ?? vi.fn().mockResolvedValue({ id: 'sl1' });
|
||||
const statusLogFindMany = overrides.statusLogFindMany ?? vi.fn().mockResolvedValue([]);
|
||||
const auditRecord = overrides.auditRecord ?? vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
const prisma = {
|
||||
@ -64,7 +66,7 @@ function makeService(
|
||||
create: elderCreate,
|
||||
update: elderUpdate,
|
||||
},
|
||||
elderStatusLog: { create: statusLogCreate, findMany: vi.fn().mockResolvedValue([]) },
|
||||
elderStatusLog: { create: statusLogCreate, findMany: statusLogFindMany },
|
||||
userRole: { findMany: userRoleFindMany },
|
||||
},
|
||||
};
|
||||
@ -88,6 +90,7 @@ function makeService(
|
||||
elderUpdate,
|
||||
userRoleFindMany,
|
||||
statusLogCreate,
|
||||
statusLogFindMany,
|
||||
auditRecord,
|
||||
storage,
|
||||
};
|
||||
@ -185,6 +188,44 @@ describe('ElderService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('changeStatus 对不可逆态(dead/terminated)拒绝变更', async () => {
|
||||
const elderFindUnique = vi.fn().mockResolvedValue({ id: 'e1', treatStatus: 'dead', name: '张三' });
|
||||
const { service, statusLogCreate } = makeService({ elderFindUnique });
|
||||
|
||||
await expect(
|
||||
runWithRequestContext(ctx, () => service.changeStatus('e1', { toStatus: 'active', changeReason: '恢复' })),
|
||||
).rejects.toThrowError('当前待遇状态不可再变更');
|
||||
|
||||
expect(statusLogCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('changeStatus 后 statusLog 返回写入的日志', async () => {
|
||||
const elderFindUnique = vi.fn().mockResolvedValue({ id: 'e1', treatStatus: 'waiting_period', name: '张三' });
|
||||
const statusLogFindMany = vi.fn().mockResolvedValue([
|
||||
{ id: 'sl1', elderId: 'e1', fromStatus: 'waiting_period', toStatus: 'active', changeReason: '待遇生效', operatorId: 'u1' },
|
||||
]);
|
||||
const { service, statusLogCreate } = makeService({ elderFindUnique, statusLogFindMany });
|
||||
|
||||
await runWithRequestContext(ctx, () => service.changeStatus('e1', { toStatus: 'active', changeReason: '待遇生效' }));
|
||||
const logs = await runWithRequestContext(ctx, () => service.statusLog('e1'));
|
||||
|
||||
expect(statusLogCreate).toHaveBeenCalled();
|
||||
expect(logs).toHaveLength(1);
|
||||
expect(logs[0]).toMatchObject({ fromStatus: 'waiting_period', toStatus: 'active', operatorId: 'u1' });
|
||||
});
|
||||
|
||||
it('create 待遇止早于起 → 抛 400', async () => {
|
||||
const { service, elderCreate } = makeService();
|
||||
|
||||
await expect(
|
||||
runWithRequestContext(ctx, () =>
|
||||
service.createElder({ ...baseDto, benefitStart: '2025-01-01', benefitEnd: '2024-01-01' }),
|
||||
),
|
||||
).rejects.toThrowError('待遇有效期止须晚于待遇有效期起');
|
||||
|
||||
expect(elderCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('list 返回掩码 id_card_no', async () => {
|
||||
const elderFindMany = vi.fn().mockResolvedValue([
|
||||
{ id: 'e1', idCardNo: VALID_ID_CARD, name: '张三', treatStatus: 'active' },
|
||||
|
||||
@ -6,7 +6,7 @@ import { BusinessException } from '../common/business-exception.js';
|
||||
import { ErrorCodes } from '../common/error-codes.js';
|
||||
import { StorageService } from '../storage/storage.service.js';
|
||||
import { AuditLogsService } from '../audit-logs/audit-logs.service.js';
|
||||
import { ChangeStatusDto, CreateElderDto, ListElderQuery } from './dto.js';
|
||||
import { ChangeStatusDto, CreateElderDto, IRREVERSIBLE_TREAT_STATUSES, ListElderQuery } from './dto.js';
|
||||
|
||||
/**
|
||||
* 身份证掩码:18 位 → 前4 + 10个* + 后4;15 位 → 前3 + 8个* + 后4;其余原样。
|
||||
@ -87,6 +87,7 @@ export class ElderService extends BaseCrudService {
|
||||
async createElder(dto: CreateElderDto) {
|
||||
const idCardNo = dto.idCardNo.trim();
|
||||
const birthDate = deriveBirthDate(idCardNo);
|
||||
this.assertBenefitRange(dto.benefitStart, dto.benefitEnd);
|
||||
await this.assertIdCardUnique(idCardNo);
|
||||
if ((dto.assessLevel || dto.careLevel) && !dto.assessEvidenceFile) {
|
||||
throw new BusinessException(
|
||||
@ -135,6 +136,9 @@ export class ElderService extends BaseCrudService {
|
||||
);
|
||||
}
|
||||
const elder = await this.requireExists(id);
|
||||
if (IRREVERSIBLE_TREAT_STATUSES.includes(elder.treatStatus)) {
|
||||
throw new BadRequestException('当前待遇状态不可再变更');
|
||||
}
|
||||
const ctx = getRequestContext();
|
||||
const fromStatus = elder.treatStatus;
|
||||
await this.update(id, {
|
||||
@ -193,6 +197,13 @@ export class ElderService extends BaseCrudService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 待遇有效期须「止 > 起」。 */
|
||||
private assertBenefitRange(benefitStart?: string, benefitEnd?: string) {
|
||||
if (benefitStart && benefitEnd && benefitEnd <= benefitStart) {
|
||||
throw new BadRequestException('待遇有效期止须晚于待遇有效期起');
|
||||
}
|
||||
}
|
||||
|
||||
private async requireExists(id: string): Promise<Record<string, any>> {
|
||||
const existing = await this.findById(id);
|
||||
if (!existing) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user