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', elderId: 'e1',
itemCount: 18, itemCount: 18,
itemCountRule: 'inst_medical/care_1 ≥18 项', itemCountRule: 'inst_medical/care_1 ≥18 项',
planStatus: 'draft', planStatus: 'active',
tenantCode: 'T001', tenantCode: 'T001',
createdBy: 'u1', createdBy: 'u1',
}), }),
@ -208,12 +208,12 @@ describe('PlanService', () => {
); );
expect(planCreate).toHaveBeenCalledWith({ 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 () => { 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 planCreate = vi.fn().mockResolvedValue({ id: 'p-new' });
const planUpdate = vi.fn().mockResolvedValue({ id: 'p-old' }); const planUpdate = vi.fn().mockResolvedValue({ id: 'p-old' });
const { service } = makeService({ planFindUnique, planCreate, planUpdate }); const { service } = makeService({ planFindUnique, planCreate, planUpdate });
@ -233,8 +233,8 @@ describe('PlanService', () => {
}); });
}); });
it('确认置 confirm_time + elder/guardian 布尔 + 转 active', async () => { it('确认置 confirm_time + elder/guardian 布尔,不改变 plan_status', async () => {
const planFindUnique = vi.fn().mockResolvedValue({ id: 'p1', planStatus: 'draft' }); const planFindUnique = vi.fn().mockResolvedValue({ id: 'p1', planStatus: 'active' });
const planUpdate = vi.fn().mockResolvedValue({ id: 'p1' }); const planUpdate = vi.fn().mockResolvedValue({ id: 'p1' });
const { service } = makeService({ planFindUnique, planUpdate }); const { service } = makeService({ planFindUnique, planUpdate });
@ -242,15 +242,30 @@ describe('PlanService', () => {
expect(planUpdate).toHaveBeenCalledWith({ expect(planUpdate).toHaveBeenCalledWith({
where: { id: 'p1' }, where: { id: 'p1' },
data: expect.objectContaining({ data: {
confirmElder: true, confirmElder: true,
confirmGuardian: false, confirmGuardian: false,
confirmTime: expect.any(Date), 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 () => { it('detail 返回计划头 + 计划项', async () => {
const planFindUnique = vi.fn().mockResolvedValue({ id: 'p1', elderId: 'e1', planStatus: 'active' }); const planFindUnique = vi.fn().mockResolvedValue({ id: 'p1', elderId: 'e1', planStatus: 'active' });
const planItemFindMany = vi.fn().mockResolvedValue([{ id: 'pi1', planId: 'p1' }]); 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_plan/svc_plan_item)业务服务。
* *
* - 制定计划:校验老人存在 + 每个 svc_id 落本机构 L4 + 属地项数(昆明校验/州市提示); * - 制定计划:校验老人存在 + 每个 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,不原地改。 * - 变更计划:新建 Plan + change_from_plan_id 指向旧计划,旧计划转 changed,不原地改。
* - 确认:置 confirm_time + elder/guardian 布尔,计划转 active。 * - 确认:置 confirm_time + elder/guardian 布尔,计划转 active。
*/ */
@ -25,20 +25,23 @@ interface ResolvedPlanItem extends PlanItemInputDto {
export class PlanService { export class PlanService {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) {}
/** 制定服务计划(draft)。 */ /** 制定服务计划(active)。 */
create(dto: CreatePlanDto) { create(dto: CreatePlanDto) {
return this.buildPlan(dto, { planStatus: 'draft' }); return this.buildPlan(dto, { planStatus: 'active' });
} }
/** 变更服务计划:新建 active 计划并指向旧计划,旧计划转 changed。 */ /** 变更服务计划:新建 active 计划并指向旧计划,旧计划转 changed。 */
async change(dto: ChangePlanDto) { 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 }); const newPlan = await this.buildPlan(dto, { planStatus: 'active', changeFromPlanId: dto.changeFromPlanId });
await this.prisma.client.plan.update({ where: { id: dto.changeFromPlanId }, data: { planStatus: 'changed' } }); await this.prisma.client.plan.update({ where: { id: dto.changeFromPlanId }, data: { planStatus: 'changed' } });
return newPlan; return newPlan;
} }
/** 确认计划:置确认时间与老人/家属布尔,计划转 active。 */ /** 确认计划:仅写确认时间与老人/家属布尔(不改变 plan_status)。 */
async confirm(id: string, dto: ConfirmPlanDto) { async confirm(id: string, dto: ConfirmPlanDto) {
await this.requireExists(id); await this.requireExists(id);
return this.prisma.client.plan.update({ return this.prisma.client.plan.update({
@ -47,7 +50,6 @@ export class PlanService {
confirmElder: dto.confirmElder, confirmElder: dto.confirmElder,
confirmGuardian: dto.confirmGuardian ?? false, confirmGuardian: dto.confirmGuardian ?? false,
confirmTime: new Date(), confirmTime: new Date(),
planStatus: 'active',
}, },
}); });
} }