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 () => {
|
||||
const recordFindUnique = vi.fn().mockResolvedValue(servingRecord);
|
||||
const svcOrgFindFirst = vi.fn().mockResolvedValue(null);
|
||||
@ -449,6 +475,19 @@ describe('RecordService.offlineSync', () => {
|
||||
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 () => {
|
||||
const orderFindUnique = vi.fn().mockResolvedValue({
|
||||
id: 'o1',
|
||||
|
||||
@ -190,6 +190,9 @@ export class RecordService {
|
||||
if (!order) {
|
||||
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 不匹配)。
|
||||
const elder = await this.prisma.client.elder.findFirst({ where: { id: order.elderId } });
|
||||
if (!elder) {
|
||||
@ -413,15 +416,16 @@ export class RecordService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 必拍档来源是 order.item_list 的 photo_tier(快照自 plan_item),不可信客户端 DTO 的 photo_required。
|
||||
* 仅 extraFlag 临时加项(order.item_list 无对应项)回退 DTO 值。
|
||||
* 必拍档来源是 order.item_list 的 photo_tier(快照自 plan_item),不可信客户端 DTO 的 photo_required/extraFlag。
|
||||
* 先按 svcId 在 item_list 找匹配项:命中则 photoRequired = (photoTier === 'must');
|
||||
* 仅 item_list 无对应项(真正的临时加项)才回退 DTO 值。
|
||||
*/
|
||||
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 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