fix(c5): 终审 R6-R9 溯源/null过滤/操作人/权限
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5cba51abb8
commit
9d1a3b2374
@ -115,6 +115,35 @@ describe('AgencyService', () => {
|
|||||||
});
|
});
|
||||||
expect(update).not.toHaveBeenCalled();
|
expect(update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('R7:dto={qcThreshold:null} 过滤 null,创建用默认 24 而非 null', async () => {
|
||||||
|
const findUnique = vi.fn().mockResolvedValue(null);
|
||||||
|
const create = vi.fn().mockResolvedValue({ code: 'T001' });
|
||||||
|
const update = vi.fn();
|
||||||
|
const prisma = { client: { agency: { findUnique, create, update } } };
|
||||||
|
const service = new AgencyService(prisma as unknown as PrismaService);
|
||||||
|
|
||||||
|
await runWithRequestContext(ctx, () => service.update({ qcThreshold: null } as never));
|
||||||
|
|
||||||
|
expect(create).toHaveBeenCalledWith({
|
||||||
|
data: {
|
||||||
|
code: 'T001',
|
||||||
|
orgName: '',
|
||||||
|
orgType: '',
|
||||||
|
creditCode: '',
|
||||||
|
locCode: '',
|
||||||
|
locName: '',
|
||||||
|
svcSource: '',
|
||||||
|
address: '',
|
||||||
|
contactName: '',
|
||||||
|
contactPhone: '',
|
||||||
|
qcThreshold: 24,
|
||||||
|
dockingTier: 'B',
|
||||||
|
newNationalNotify: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(update).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('UpdateAgencyDto.dockingTier', () => {
|
describe('UpdateAgencyDto.dockingTier', () => {
|
||||||
|
|||||||
@ -22,6 +22,15 @@ function agencyDefaults(code: string): Record<string, unknown> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** R7:只保留 dto 中有值的字段,过滤 null/undefined,避免 null 覆盖 DB 默认值。 */
|
||||||
|
function definedFields(dto: UpdateAgencyDto): Record<string, unknown> {
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const [key, value] of Object.entries(dto)) {
|
||||||
|
if (value !== null && value !== undefined) out[key] = value;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 机构(=租户)信息读写。Agency 是 TENANT_FREE 模型,主键 code = 当前租户 code,
|
* 机构(=租户)信息读写。Agency 是 TENANT_FREE 模型,主键 code = 当前租户 code,
|
||||||
* 因此读写一律按 `ctx.tenantCode` 定位,而非 BaseCrudService 的 `findById(id)`。
|
* 因此读写一律按 `ctx.tenantCode` 定位,而非 BaseCrudService 的 `findById(id)`。
|
||||||
@ -44,12 +53,13 @@ export class AgencyService {
|
|||||||
async update(dto: UpdateAgencyDto) {
|
async update(dto: UpdateAgencyDto) {
|
||||||
const ctx = getRequestContext()!;
|
const ctx = getRequestContext()!;
|
||||||
const existing = await this.getCurrent();
|
const existing = await this.getCurrent();
|
||||||
|
const data = definedFields(dto);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
return this.prisma.client.agency.create({
|
return this.prisma.client.agency.create({
|
||||||
data: { ...agencyDefaults(ctx.tenantCode), ...dto } as any,
|
data: { ...agencyDefaults(ctx.tenantCode), ...data } as any,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return this.prisma.client.agency.update({ where: { code: ctx.tenantCode }, data: dto });
|
return this.prisma.client.agency.update({ where: { code: ctx.tenantCode }, data });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -179,6 +179,7 @@ export const BUSINESS_ROLES: { code: string; name: string; permissions: string[]
|
|||||||
Permissions.qc.batchPass,
|
Permissions.qc.batchPass,
|
||||||
Permissions.abnormal.list,
|
Permissions.abnormal.list,
|
||||||
Permissions.abnormal.handle,
|
Permissions.abnormal.handle,
|
||||||
|
Permissions.export.export,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ code: 'leader', name: '组长', permissions: [Permissions.staff.list, Permissions.svcOrg.list] },
|
{ code: 'leader', name: '组长', permissions: [Permissions.staff.list, Permissions.svcOrg.list] },
|
||||||
|
|||||||
@ -153,10 +153,10 @@ describe('DocumentService.export', () => {
|
|||||||
expect(recordItemFindMany).toHaveBeenCalledWith({ where: { recordId: { in: ['r1'] } } });
|
expect(recordItemFindMany).toHaveBeenCalledWith({ where: { recordId: { in: ['r1'] } } });
|
||||||
expect(recordAbnormalFindMany).toHaveBeenCalledWith({ where: { elderId: 'e1' } });
|
expect(recordAbnormalFindMany).toHaveBeenCalledWith({ where: { elderId: 'e1' } });
|
||||||
|
|
||||||
// 水印:机构名 + 时间 + 操作人 + R4 文案
|
// 水印:机构名 + 时间 + 操作人(username)+ R4 文案
|
||||||
expect(res.watermark).toMatchObject({
|
expect(res.watermark).toMatchObject({
|
||||||
orgName: '测试机构',
|
orgName: '测试机构',
|
||||||
operator: 'u1',
|
operator: 'admin',
|
||||||
notice: EXPORT_NOTICE,
|
notice: EXPORT_NOTICE,
|
||||||
});
|
});
|
||||||
expect(res.watermark.exportTime).toEqual(expect.any(String));
|
expect(res.watermark.exportTime).toEqual(expect.any(String));
|
||||||
|
|||||||
@ -94,7 +94,7 @@ export class DocumentService {
|
|||||||
watermark: {
|
watermark: {
|
||||||
orgName: agency?.orgName ?? '',
|
orgName: agency?.orgName ?? '',
|
||||||
exportTime: new Date().toISOString(),
|
exportTime: new Date().toISOString(),
|
||||||
operator: ctx.userId,
|
operator: ctx.username,
|
||||||
notice: EXPORT_NOTICE,
|
notice: EXPORT_NOTICE,
|
||||||
},
|
},
|
||||||
elder: {
|
elder: {
|
||||||
|
|||||||
@ -166,6 +166,36 @@ describe('PlanService', () => {
|
|||||||
expect(planCreate).not.toHaveBeenCalled();
|
expect(planCreate).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('R6:svc_org.photoTier=must 且客户端不传 → plan_item.photoTier=must', async () => {
|
||||||
|
const svcOrgFindFirst = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ id: 'svc1', srcType: 'national', srcId: 'nat1', photoTier: 'must' });
|
||||||
|
const planItemCreate = vi.fn().mockResolvedValue({ id: 'pi1' });
|
||||||
|
const { service } = makeService({ svcOrgFindFirst, planItemCreate });
|
||||||
|
|
||||||
|
await runWithRequestContext(ctx, () => service.create({ ...baseDto, items: [{ svcId: 'svc1' }] }));
|
||||||
|
|
||||||
|
expect(planItemCreate).toHaveBeenCalledWith({
|
||||||
|
data: expect.objectContaining({ svcId: 'svc1', photoTier: 'must' }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('R6:客户端 photoTier 覆盖 svc_org 默认(可加严)', async () => {
|
||||||
|
const svcOrgFindFirst = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ id: 'svc1', srcType: 'national', srcId: 'nat1', photoTier: 'free' });
|
||||||
|
const planItemCreate = vi.fn().mockResolvedValue({ id: 'pi1' });
|
||||||
|
const { service } = makeService({ svcOrgFindFirst, planItemCreate });
|
||||||
|
|
||||||
|
await runWithRequestContext(ctx, () =>
|
||||||
|
service.create({ ...baseDto, items: [{ svcId: 'svc1', photoTier: 'must' }] }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(planItemCreate).toHaveBeenCalledWith({
|
||||||
|
data: expect.objectContaining({ svcId: 'svc1', photoTier: 'must' }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('elder 不存在 → ELDER_NOT_FOUND', async () => {
|
it('elder 不存在 → ELDER_NOT_FOUND', async () => {
|
||||||
const elderFindFirst = vi.fn().mockResolvedValue(null);
|
const elderFindFirst = vi.fn().mockResolvedValue(null);
|
||||||
const { service, planCreate } = makeService({ elderFindFirst });
|
const { service, planCreate } = makeService({ elderFindFirst });
|
||||||
|
|||||||
@ -126,18 +126,24 @@ export class PlanService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 校验每个 svc_id 落本机构 L4,并解析 svc 名称/代码快照。 */
|
/** 校验每个 svc_id 落本机构 L4,并解析 svc 名称/代码快照 + photo_tier 默认源。 */
|
||||||
private async resolvePlanItems(items: PlanItemInputDto[]): Promise<ResolvedPlanItem[]> {
|
private async resolvePlanItems(items: PlanItemInputDto[]): Promise<ResolvedPlanItem[]> {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
items.map(async (item) => {
|
items.map(async (item) => {
|
||||||
const svcOrg = await this.requireSvcOrg(item.svcId);
|
const svcOrg = await this.requireSvcOrg(item.svcId);
|
||||||
const snapshot = await this.resolveSvcSnapshot(svcOrg);
|
const snapshot = await this.resolveSvcSnapshot(svcOrg);
|
||||||
return { ...item, svcName: snapshot.svcName, svcCode: snapshot.svcCode };
|
return {
|
||||||
|
...item,
|
||||||
|
svcName: snapshot.svcName,
|
||||||
|
svcCode: snapshot.svcCode,
|
||||||
|
// R6:photo_tier 溯源 svc_org(客户端 DTO 仍可覆盖加严)。
|
||||||
|
photoTier: item.photoTier ?? svcOrg.photoTier ?? 'free',
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async requireSvcOrg(svcId: string): Promise<{ srcType: string; srcId: string }> {
|
private async requireSvcOrg(svcId: string): Promise<{ srcType: string; srcId: string; photoTier: string }> {
|
||||||
const svcOrg = await this.prisma.client.svcOrg.findFirst({ where: { id: svcId } });
|
const svcOrg = await this.prisma.client.svcOrg.findFirst({ where: { id: svcId } });
|
||||||
if (!svcOrg) {
|
if (!svcOrg) {
|
||||||
throw new BusinessException(ErrorCodes.SVC_ORG_NOT_FOUND, '机构项目不存在', HttpStatus.NOT_FOUND);
|
throw new BusinessException(ErrorCodes.SVC_ORG_NOT_FOUND, '机构项目不存在', HttpStatus.NOT_FOUND);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user