fix(c4): 质控放行自检顺序 + 补正≥3次进质控重点
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f24b86064f
commit
915745226f
@ -41,6 +41,7 @@ function makeService(
|
|||||||
recordAbnormalUpdate?: ReturnType<typeof vi.fn>;
|
recordAbnormalUpdate?: ReturnType<typeof vi.fn>;
|
||||||
recordCorrectionFindMany?: ReturnType<typeof vi.fn>;
|
recordCorrectionFindMany?: ReturnType<typeof vi.fn>;
|
||||||
recordCorrectionCreate?: ReturnType<typeof vi.fn>;
|
recordCorrectionCreate?: ReturnType<typeof vi.fn>;
|
||||||
|
recordCorrectionCount?: ReturnType<typeof vi.fn>;
|
||||||
// client.order / elder / plan
|
// client.order / elder / plan
|
||||||
orderFindMany?: ReturnType<typeof vi.fn>;
|
orderFindMany?: ReturnType<typeof vi.fn>;
|
||||||
orderFindFirst?: ReturnType<typeof vi.fn>;
|
orderFindFirst?: ReturnType<typeof vi.fn>;
|
||||||
@ -72,6 +73,7 @@ function makeService(
|
|||||||
const recordAbnormalUpdate = overrides.recordAbnormalUpdate ?? vi.fn().mockResolvedValue({ id: 'a1' });
|
const recordAbnormalUpdate = overrides.recordAbnormalUpdate ?? vi.fn().mockResolvedValue({ id: 'a1' });
|
||||||
const recordCorrectionFindMany = overrides.recordCorrectionFindMany ?? vi.fn().mockResolvedValue([]);
|
const recordCorrectionFindMany = overrides.recordCorrectionFindMany ?? vi.fn().mockResolvedValue([]);
|
||||||
const recordCorrectionCreate = overrides.recordCorrectionCreate ?? vi.fn().mockResolvedValue({ id: 'c1' });
|
const recordCorrectionCreate = overrides.recordCorrectionCreate ?? vi.fn().mockResolvedValue({ id: 'c1' });
|
||||||
|
const recordCorrectionCount = overrides.recordCorrectionCount ?? vi.fn().mockResolvedValue(0);
|
||||||
|
|
||||||
const orderFindMany = overrides.orderFindMany ?? vi.fn().mockResolvedValue([]);
|
const orderFindMany = overrides.orderFindMany ?? vi.fn().mockResolvedValue([]);
|
||||||
const orderFindFirst = overrides.orderFindFirst ?? vi.fn().mockResolvedValue(null);
|
const orderFindFirst = overrides.orderFindFirst ?? vi.fn().mockResolvedValue(null);
|
||||||
@ -106,7 +108,7 @@ function makeService(
|
|||||||
findUnique: recordAbnormalFindUnique,
|
findUnique: recordAbnormalFindUnique,
|
||||||
update: recordAbnormalUpdate,
|
update: recordAbnormalUpdate,
|
||||||
},
|
},
|
||||||
recordCorrection: { findMany: recordCorrectionFindMany, create: recordCorrectionCreate },
|
recordCorrection: { findMany: recordCorrectionFindMany, create: recordCorrectionCreate, count: recordCorrectionCount },
|
||||||
order: { findMany: orderFindMany, findFirst: orderFindFirst },
|
order: { findMany: orderFindMany, findFirst: orderFindFirst },
|
||||||
elder: { findMany: elderFindMany },
|
elder: { findMany: elderFindMany },
|
||||||
plan: { findFirst: planFindFirst },
|
plan: { findFirst: planFindFirst },
|
||||||
@ -138,6 +140,7 @@ function makeService(
|
|||||||
recordAbnormalUpdate,
|
recordAbnormalUpdate,
|
||||||
recordCorrectionFindMany,
|
recordCorrectionFindMany,
|
||||||
recordCorrectionCreate,
|
recordCorrectionCreate,
|
||||||
|
recordCorrectionCount,
|
||||||
orderFindMany,
|
orderFindMany,
|
||||||
orderFindFirst,
|
orderFindFirst,
|
||||||
elderFindMany,
|
elderFindMany,
|
||||||
@ -199,15 +202,26 @@ describe('QcService.pass', () => {
|
|||||||
expect(qcUpdate).not.toHaveBeenCalled();
|
expect(qcUpdate).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('已处理(非 pending)→ QC_ALREADY_HANDLED(幂等)', async () => {
|
it('已处理(非 pending)且要素齐全 → QC_ALREADY_HANDLED(幂等)', async () => {
|
||||||
const qcFindUnique = vi.fn().mockResolvedValue({ id: 'qc1', recordId: 'r1', qcResult: 'passed' });
|
const qcFindUnique = vi.fn().mockResolvedValue({ id: 'qc1', recordId: 'r1', qcResult: 'passed' });
|
||||||
const { service, qcUpdate } = makeService({ qcFindUnique });
|
const recordFindUnique = vi.fn().mockResolvedValue(passableRecord);
|
||||||
|
const { service, qcUpdate } = makeService({ qcFindUnique, recordFindUnique });
|
||||||
await expect(runWithRequestContext(ctx, () => service.pass('qc1'))).rejects.toMatchObject({
|
await expect(runWithRequestContext(ctx, () => service.pass('qc1'))).rejects.toMatchObject({
|
||||||
response: { code: ErrorCodes.QC_ALREADY_HANDLED },
|
response: { code: ErrorCodes.QC_ALREADY_HANDLED },
|
||||||
});
|
});
|
||||||
expect(qcUpdate).not.toHaveBeenCalled();
|
expect(qcUpdate).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('已处理且缺要素 → QC_ELEMENT_MISSING(1004) 优先于 QC_ALREADY_HANDLED(顺序①在③前)', async () => {
|
||||||
|
const qcFindUnique = vi.fn().mockResolvedValue({ id: 'qc1', recordId: 'r1', qcResult: 'passed' });
|
||||||
|
const recordFindUnique = vi.fn().mockResolvedValue({ ...passableRecord, checkoutTime: null });
|
||||||
|
const { service, qcUpdate } = makeService({ qcFindUnique, recordFindUnique });
|
||||||
|
await expect(runWithRequestContext(ctx, () => service.pass('qc1'))).rejects.toMatchObject({
|
||||||
|
response: { code: ErrorCodes.QC_ELEMENT_MISSING, message: expect.stringContaining('1004') },
|
||||||
|
});
|
||||||
|
expect(qcUpdate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('四要素缺 → QC_ELEMENT_MISSING(1004)', async () => {
|
it('四要素缺 → QC_ELEMENT_MISSING(1004)', async () => {
|
||||||
const qcFindUnique = vi.fn().mockResolvedValue(pendingQc);
|
const qcFindUnique = vi.fn().mockResolvedValue(pendingQc);
|
||||||
const recordFindUnique = vi.fn().mockResolvedValue({ ...passableRecord, checkoutTime: null });
|
const recordFindUnique = vi.fn().mockResolvedValue({ ...passableRecord, checkoutTime: null });
|
||||||
@ -526,9 +540,25 @@ describe('QcService.pendingList(五类聚合)', () => {
|
|||||||
expect(res.pendingQc.list[0]).toMatchObject({
|
expect(res.pendingQc.list[0]).toMatchObject({
|
||||||
id: 'qc1',
|
id: 'qc1',
|
||||||
record: { id: 'r1', elderId: 'e1', assigneeId: 'staff1' },
|
record: { id: 'r1', elderId: 'e1', assigneeId: 'staff1' },
|
||||||
|
correctionCount: 0,
|
||||||
|
focus: false,
|
||||||
});
|
});
|
||||||
expect(res.pendingQc.list[0].waitMinutes).toBe(60);
|
expect(res.pendingQc.list[0].waitMinutes).toBe(60);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('补正 ≥3 次 → 待放行队列项 focus=true(质控重点)', async () => {
|
||||||
|
const qcFindMany = vi.fn().mockResolvedValue([
|
||||||
|
{ id: 'qc1', recordId: 'r1', qcResult: 'pending', createdAt: new Date() },
|
||||||
|
]);
|
||||||
|
const recordFindUnique = vi.fn().mockResolvedValue(passableRecord);
|
||||||
|
const recordCorrectionCount = vi.fn().mockResolvedValue(3);
|
||||||
|
const { service } = makeService({ qcFindMany, recordFindUnique, recordCorrectionCount });
|
||||||
|
|
||||||
|
const res = await runWithRequestContext(ctx, () => service.pendingList());
|
||||||
|
|
||||||
|
expect(recordCorrectionCount).toHaveBeenCalledWith({ where: { recordId: 'r1' } });
|
||||||
|
expect(res.pendingQc.list[0]).toMatchObject({ correctionCount: 3, focus: true });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('QcService.detail', () => {
|
describe('QcService.detail', () => {
|
||||||
|
|||||||
@ -109,7 +109,10 @@ export class QcService {
|
|||||||
|
|
||||||
const pendingQcList = [];
|
const pendingQcList = [];
|
||||||
for (const qc of pendingQcs) {
|
for (const qc of pendingQcs) {
|
||||||
const record = await this.prisma.client.record.findUnique({ where: { id: qc.recordId } });
|
const [record, correctionCount] = await Promise.all([
|
||||||
|
this.prisma.client.record.findUnique({ where: { id: qc.recordId } }),
|
||||||
|
this.prisma.client.recordCorrection.count({ where: { recordId: qc.recordId } }),
|
||||||
|
]);
|
||||||
pendingQcList.push({
|
pendingQcList.push({
|
||||||
...qc,
|
...qc,
|
||||||
record: record
|
record: record
|
||||||
@ -121,6 +124,8 @@ export class QcService {
|
|||||||
recordStatus: record.recordStatus,
|
recordStatus: record.recordStatus,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
correctionCount,
|
||||||
|
focus: correctionCount >= 3,
|
||||||
waitMinutes: Math.max(0, Math.floor((now.getTime() - new Date(qc.createdAt).getTime()) / 60000)),
|
waitMinutes: Math.max(0, Math.floor((now.getTime() - new Date(qc.createdAt).getTime()) / 60000)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -159,15 +164,12 @@ export class QcService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 放行:自检(幂等 → 四要素 1004 → 必拍 3001 → 自审 4001)→ passed。 */
|
/** 放行:自检(四要素 1004 → 必拍 3001 → 幂等 → 自审 4001)→ passed。 */
|
||||||
async pass(id: string) {
|
async pass(id: string) {
|
||||||
const ctx = getRequestContext()!;
|
const ctx = getRequestContext()!;
|
||||||
const qc = await this.requireQc(id);
|
const qc = await this.requireQc(id);
|
||||||
if (qc.qcResult !== 'pending') {
|
|
||||||
throw new BusinessException(ErrorCodes.QC_ALREADY_HANDLED, '该质控已处理(幂等)', HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
const record = await this.requireRecord(qc.recordId);
|
const record = await this.requireRecord(qc.recordId);
|
||||||
await this.assertPassable(record, ctx.userId);
|
await this.assertPassable(qc, record, ctx.userId);
|
||||||
return this.prisma.client.qc.update({
|
return this.prisma.client.qc.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { qcResult: 'passed', qcStaffId: ctx.userId, qcTime: new Date(), passTime: new Date() },
|
data: { qcResult: 'passed', qcStaffId: ctx.userId, qcTime: new Date(), passTime: new Date() },
|
||||||
@ -291,8 +293,8 @@ export class QcService {
|
|||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 放行自检:四要素(1004)→ 必拍已拍(3001)→ 自审自放行(4001)。 */
|
/** 放行自检(按序):四要素(1004)→ 必拍已拍(3001)→ 幂等(46002)→ 自审自放行(4001)。 */
|
||||||
private async assertPassable(record: Record<string, any>, userId: string) {
|
private async assertPassable(qc: Record<string, any>, record: Record<string, any>, userId: string) {
|
||||||
if (!record.checkinTime || !record.checkoutTime || record.durationMin == null || !record.assigneeId) {
|
if (!record.checkinTime || !record.checkoutTime || record.durationMin == null || !record.assigneeId) {
|
||||||
throw new BusinessException(ErrorCodes.QC_ELEMENT_MISSING, '留痕缺必填要素(1004)', HttpStatus.BAD_REQUEST);
|
throw new BusinessException(ErrorCodes.QC_ELEMENT_MISSING, '留痕缺必填要素(1004)', HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
@ -312,6 +314,9 @@ export class QcService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (qc.qcResult !== 'pending') {
|
||||||
|
throw new BusinessException(ErrorCodes.QC_ALREADY_HANDLED, '该质控已处理(幂等)', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
if (record.createdBy === userId) {
|
if (record.createdBy === userId) {
|
||||||
throw new BusinessException(ErrorCodes.QC_SELF_REVIEW, '不可自审自放行(4001)', HttpStatus.BAD_REQUEST);
|
throw new BusinessException(ErrorCodes.QC_SELF_REVIEW, '不可自审自放行(4001)', HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user