fix(c3): record 必拍档不可被 extraFlag 绕过 + 未派单离线补传守卫
- resolvePhotoRequired 先按 svcId 查 order.item_list,命中以 photo_tier 为准, 忽略客户端 extraFlag/photo_required;仅无对应项才回退 DTO - offlineSync 对 assigneeId 为 null(未派单)工单抛 RECORD_ORDER_INVALID,防 null 入 NOT NULL 列 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
55f432b8e7
commit
1966a68bc8
@ -304,6 +304,32 @@ describe('RecordService.itemSave', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('计划内 must 项 + 客户端传 extraFlag=true/photoRequired=false → 仍存 photoRequired=true(不可绕过)', async () => {
|
||||||
|
const recordFindUnique = vi.fn().mockResolvedValue(servingRecord);
|
||||||
|
const orderFindUnique = vi.fn().mockResolvedValue({ id: 'o1', itemList: [{ svcId: 'svc1', photoTier: 'must' }] });
|
||||||
|
const svcOrgFindFirst = vi.fn().mockResolvedValue({ id: 'svc1', srcType: 'national', srcId: 'nat1' });
|
||||||
|
const svcNationalFindFirst = vi.fn().mockResolvedValue({ natName: '协助翻身' });
|
||||||
|
const recordItemCreate = vi.fn().mockResolvedValue({ id: 'ri1' });
|
||||||
|
const { service } = makeService({
|
||||||
|
recordFindUnique,
|
||||||
|
orderFindUnique,
|
||||||
|
svcOrgFindFirst,
|
||||||
|
svcNationalFindFirst,
|
||||||
|
recordItemCreate,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runWithRequestContext(ctx, () =>
|
||||||
|
service.itemSave({
|
||||||
|
recordId: 'r1',
|
||||||
|
items: [{ svcId: 'svc1', execResult: 'done', extraFlag: true, photoRequired: false }],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(recordItemCreate).toHaveBeenCalledWith({
|
||||||
|
data: expect.objectContaining({ svcId: 'svc1', photoRequired: true }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('svc_id 非本机构 L4 → SVC_ORG_NOT_FOUND', async () => {
|
it('svc_id 非本机构 L4 → SVC_ORG_NOT_FOUND', async () => {
|
||||||
const recordFindUnique = vi.fn().mockResolvedValue(servingRecord);
|
const recordFindUnique = vi.fn().mockResolvedValue(servingRecord);
|
||||||
const svcOrgFindFirst = vi.fn().mockResolvedValue(null);
|
const svcOrgFindFirst = vi.fn().mockResolvedValue(null);
|
||||||
@ -449,6 +475,19 @@ describe('RecordService.offlineSync', () => {
|
|||||||
expect(recordCreate).not.toHaveBeenCalled();
|
expect(recordCreate).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('未派单工单(assigneeId 为 null)→ RECORD_ORDER_INVALID', async () => {
|
||||||
|
const orderFindUnique = vi.fn().mockResolvedValue({ id: 'o1', orderStatus: 'to_assign', elderId: 'e1', assigneeId: null });
|
||||||
|
const { service, recordCreate } = makeService({ orderFindUnique });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runWithRequestContext(ctx, () =>
|
||||||
|
service.offlineSync({ clientMd5: 'md5-3', orderId: 'o1', elderId: 'e1', assigneeId: 'staff1', checkinTime: '2026-10-15T02:00:00Z' }),
|
||||||
|
),
|
||||||
|
).rejects.toMatchObject({ response: { code: ErrorCodes.RECORD_ORDER_INVALID } });
|
||||||
|
|
||||||
|
expect(recordCreate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('离线新建完整 record(elder/assignee 以 order 为准 + offline_flag + is_prepaid + items/photos 一次性)', async () => {
|
it('离线新建完整 record(elder/assignee 以 order 为准 + offline_flag + is_prepaid + items/photos 一次性)', async () => {
|
||||||
const orderFindUnique = vi.fn().mockResolvedValue({
|
const orderFindUnique = vi.fn().mockResolvedValue({
|
||||||
id: 'o1',
|
id: 'o1',
|
||||||
|
|||||||
@ -190,6 +190,9 @@ export class RecordService {
|
|||||||
if (!order) {
|
if (!order) {
|
||||||
throw new BusinessException(ErrorCodes.RECORD_ORDER_INVALID, '工单不存在', HttpStatus.NOT_FOUND);
|
throw new BusinessException(ErrorCodes.RECORD_ORDER_INVALID, '工单不存在', HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
if (!order.assigneeId) {
|
||||||
|
throw new BusinessException(ErrorCodes.RECORD_ORDER_INVALID, '工单未派单,无法留痕', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
// elderId/assigneeId 以 order 为准(忽略 DTO 的这两个字段,防 order/elder/assignee 不匹配)。
|
// elderId/assigneeId 以 order 为准(忽略 DTO 的这两个字段,防 order/elder/assignee 不匹配)。
|
||||||
const elder = await this.prisma.client.elder.findFirst({ where: { id: order.elderId } });
|
const elder = await this.prisma.client.elder.findFirst({ where: { id: order.elderId } });
|
||||||
if (!elder) {
|
if (!elder) {
|
||||||
@ -413,15 +416,16 @@ export class RecordService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 必拍档来源是 order.item_list 的 photo_tier(快照自 plan_item),不可信客户端 DTO 的 photo_required。
|
* 必拍档来源是 order.item_list 的 photo_tier(快照自 plan_item),不可信客户端 DTO 的 photo_required/extraFlag。
|
||||||
* 仅 extraFlag 临时加项(order.item_list 无对应项)回退 DTO 值。
|
* 先按 svcId 在 item_list 找匹配项:命中则 photoRequired = (photoTier === 'must');
|
||||||
|
* 仅 item_list 无对应项(真正的临时加项)才回退 DTO 值。
|
||||||
*/
|
*/
|
||||||
private resolvePhotoRequired(item: RecordItemDto, order?: Record<string, any> | null): boolean {
|
private resolvePhotoRequired(item: RecordItemDto, order?: Record<string, any> | null): boolean {
|
||||||
if (item.extraFlag) {
|
|
||||||
return item.photoRequired ?? false;
|
|
||||||
}
|
|
||||||
const itemList = Array.isArray(order?.itemList) ? order.itemList : [];
|
const itemList = Array.isArray(order?.itemList) ? order.itemList : [];
|
||||||
const entry = itemList.find((i: Record<string, any>) => i.svcId === item.svcId);
|
const entry = itemList.find((i: Record<string, any>) => i.svcId === item.svcId);
|
||||||
return entry?.photoTier === 'must';
|
if (entry) {
|
||||||
|
return entry.photoTier === 'must';
|
||||||
|
}
|
||||||
|
return item.photoRequired ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user