fix(c2): plan create 置 active + confirm 不改状态 + change 校验同老人

- R7: create 置 planStatus=active(spec 权威);confirm 仅写确认字段不改状态
- R8: change 校验 oldPlan.elderId === dto.elderId,不匹配拒绝

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-14 22:32:59 +08:00
parent 418dfb9dae
commit 161a1253ee
2 changed files with 31 additions and 14 deletions

View File

@ -136,7 +136,7 @@ describe('PlanService', () => {
elderId: 'e1',
itemCount: 18,
itemCountRule: 'inst_medical/care_1 ≥18 项',
planStatus: 'draft',
planStatus: 'active',
tenantCode: 'T001',
createdBy: 'u1',
}),
@ -208,12 +208,12 @@ describe('PlanService', () => {
);
expect(planCreate).toHaveBeenCalledWith({
data: expect.objectContaining({ careMode: 'home_org', itemCount: 0, planStatus: 'draft' }),
data: expect.objectContaining({ careMode: 'home_org', itemCount: 0, planStatus: 'active' }),
});
});
it('变更留旧版:旧计划转 changed + 新计划 change_from_plan_id', async () => {
const planFindUnique = vi.fn().mockResolvedValue({ id: 'p-old', planStatus: 'active' });
const planFindUnique = vi.fn().mockResolvedValue({ id: 'p-old', elderId: 'e1', planStatus: 'active' });
const planCreate = vi.fn().mockResolvedValue({ id: 'p-new' });
const planUpdate = vi.fn().mockResolvedValue({ id: 'p-old' });
const { service } = makeService({ planFindUnique, planCreate, planUpdate });
@ -233,8 +233,8 @@ describe('PlanService', () => {
});
});
it('确认置 confirm_time + elder/guardian 布尔 + 转 active', async () => {
const planFindUnique = vi.fn().mockResolvedValue({ id: 'p1', planStatus: 'draft' });
it('确认置 confirm_time + elder/guardian 布尔,不改变 plan_status', async () => {
const planFindUnique = vi.fn().mockResolvedValue({ id: 'p1', planStatus: 'active' });
const planUpdate = vi.fn().mockResolvedValue({ id: 'p1' });
const { service } = makeService({ planFindUnique, planUpdate });
@ -242,15 +242,30 @@ describe('PlanService', () => {
expect(planUpdate).toHaveBeenCalledWith({
where: { id: 'p1' },
data: expect.objectContaining({
data: {
confirmElder: true,
confirmGuardian: false,
confirmTime: expect.any(Date),
planStatus: 'active',
}),
},
});
});
it('变更计划 elder 不匹配 → 拒绝,不写新计划', async () => {
const planFindUnique = vi.fn().mockResolvedValue({ id: 'p-old', elderId: 'e-other', planStatus: 'active' });
const planCreate = vi.fn();
const planUpdate = vi.fn();
const { service } = makeService({ planFindUnique, planCreate, planUpdate });
await expect(
runWithRequestContext(ctx, () =>
service.change({ ...baseDto, careMode: 'home_org', items: [], changeFromPlanId: 'p-old' }),
),
).rejects.toMatchObject({ response: { code: ErrorCodes.PLAN_NOT_FOUND } });
expect(planCreate).not.toHaveBeenCalled();
expect(planUpdate).not.toHaveBeenCalled();
});
it('detail 返回计划头 + 计划项', async () => {
const planFindUnique = vi.fn().mockResolvedValue({ id: 'p1', elderId: 'e1', planStatus: 'active' });
const planItemFindMany = vi.fn().mockResolvedValue([{ id: 'pi1', planId: 'p1' }]);

View File

@ -17,7 +17,7 @@ interface ResolvedPlanItem extends PlanItemInputDto {
* 服务计划(svc_plan/svc_plan_item)业务服务。
*
* - 制定计划:校验老人存在 + 每个 svc_id 落本机构 L4 + 属地项数(昆明校验/州市提示);
* 快照 item_count/item_count_rule,plan_status 默认 draft;写 plan_item(svc_name/code 快照)。
* 快照 item_count/item_count_rule,plan_status=active;写 plan_item(svc_name/code 快照)。
* - 变更计划:新建 Plan + change_from_plan_id 指向旧计划,旧计划转 changed,不原地改。
* - 确认:置 confirm_time + elder/guardian 布尔,计划转 active。
*/
@ -25,20 +25,23 @@ interface ResolvedPlanItem extends PlanItemInputDto {
export class PlanService {
constructor(private readonly prisma: PrismaService) {}
/** 制定服务计划(draft)。 */
/** 制定服务计划(active)。 */
create(dto: CreatePlanDto) {
return this.buildPlan(dto, { planStatus: 'draft' });
return this.buildPlan(dto, { planStatus: 'active' });
}
/** 变更服务计划:新建 active 计划并指向旧计划,旧计划转 changed。 */
async change(dto: ChangePlanDto) {
await this.requireExists(dto.changeFromPlanId);
const oldPlan = await this.requireExists(dto.changeFromPlanId);
if (oldPlan.elderId !== dto.elderId) {
throw new BusinessException(ErrorCodes.PLAN_NOT_FOUND, '被变更计划不属于该老人', HttpStatus.BAD_REQUEST);
}
const newPlan = await this.buildPlan(dto, { planStatus: 'active', changeFromPlanId: dto.changeFromPlanId });
await this.prisma.client.plan.update({ where: { id: dto.changeFromPlanId }, data: { planStatus: 'changed' } });
return newPlan;
}
/** 确认计划:置确认时间与老人/家属布尔,计划转 active。 */
/** 确认计划:仅写确认时间与老人/家属布尔(不改变 plan_status)。 */
async confirm(id: string, dto: ConfirmPlanDto) {
await this.requireExists(id);
return this.prisma.client.plan.update({
@ -47,7 +50,6 @@ export class PlanService {
confirmElder: dto.confirmElder,
confirmGuardian: dto.confirmGuardian ?? false,
confirmTime: new Date(),
planStatus: 'active',
},
});
}