1080 lines
31 KiB
JavaScript
1080 lines
31 KiB
JavaScript
const { ObjectId } = require("mongodb");
|
||
jest.mock("../tencent-im", () => jest.fn());
|
||
jest.mock("../diagnostic-record", () => jest.fn());
|
||
const tencentIM = require("../tencent-im");
|
||
const diagnosticRecord = require("../diagnostic-record");
|
||
const {
|
||
AUTOMATION_TYPES,
|
||
COLLECTION,
|
||
ERROR_SOURCES,
|
||
MANUAL_OPEN_RX_MESSAGE,
|
||
TASK_CANCEL_REASONS,
|
||
TASK_STATUS,
|
||
AutomationError,
|
||
buildAutoPrescriptionParams,
|
||
calculateDueAt,
|
||
claimNextTask,
|
||
enqueueTask,
|
||
getFailureNotificationExt,
|
||
processClaimedTask,
|
||
reconcileAutomationTasks,
|
||
scheduleAutoOpenRx,
|
||
} = require("./index");
|
||
|
||
function createCollectionDb(collections) {
|
||
return {
|
||
collection: jest.fn((name) => {
|
||
if (!collections[name]) {
|
||
throw new Error(`unexpected collection: ${name}`);
|
||
}
|
||
return collections[name];
|
||
}),
|
||
};
|
||
}
|
||
|
||
describe("automation task scheduling", () => {
|
||
afterEach(() => {
|
||
jest.restoreAllMocks();
|
||
jest.clearAllMocks();
|
||
});
|
||
|
||
test("uses the configured delay and respects firstSubmitRxIntervel", () => {
|
||
jest.spyOn(Date, "now").mockReturnValue(1_000_000);
|
||
const config = {
|
||
autoOpenRx: true,
|
||
autoOpenRxDelayMinSeconds: 20,
|
||
autoOpenRxDelayMaxSeconds: 40,
|
||
firstSubmitRxIntervel: 60,
|
||
};
|
||
|
||
expect(
|
||
calculateDueAt(AUTOMATION_TYPES.OPEN_RX, config, 1_000_000, () => 0)
|
||
).toBe(1_060_000);
|
||
expect(
|
||
calculateDueAt(
|
||
AUTOMATION_TYPES.OPEN_RX,
|
||
{ ...config, firstSubmitRxIntervel: 10 },
|
||
1_000_000,
|
||
() => 0.999999
|
||
)
|
||
).toBe(1_040_000);
|
||
});
|
||
|
||
test("creates a task idempotently through a unique task key", async () => {
|
||
const updateOne = jest
|
||
.fn()
|
||
.mockResolvedValueOnce({ modifiedCount: 0 })
|
||
.mockResolvedValueOnce({ upsertedCount: 1 });
|
||
const db = createCollectionDb({
|
||
"hlw-config": {
|
||
findOne: jest.fn().mockResolvedValue({
|
||
autoAcceptOrder: true,
|
||
autoAcceptDelayMinSeconds: 2,
|
||
autoAcceptDelayMaxSeconds: 5,
|
||
}),
|
||
},
|
||
[COLLECTION]: { updateOne },
|
||
});
|
||
|
||
const result = await enqueueTask(db, {
|
||
type: AUTOMATION_TYPES.ACCEPT,
|
||
corpId: "corp-1",
|
||
orderId: "order-1",
|
||
baseTime: Date.now(),
|
||
expiresAt: Date.now() + 60_000,
|
||
});
|
||
|
||
expect(result).toMatchObject({
|
||
success: true,
|
||
created: true,
|
||
taskKey: "AUTO_ACCEPT:order-1",
|
||
});
|
||
expect(updateOne).toHaveBeenLastCalledWith(
|
||
{ taskKey: "AUTO_ACCEPT:order-1" },
|
||
expect.objectContaining({
|
||
$setOnInsert: expect.objectContaining({
|
||
status: TASK_STATUS.PENDING,
|
||
attempts: 0,
|
||
}),
|
||
}),
|
||
{ upsert: true }
|
||
);
|
||
});
|
||
|
||
test("reactivates a task cancelled by a previously disabled switch", async () => {
|
||
const updateOne = jest.fn().mockResolvedValue({ modifiedCount: 1 });
|
||
const db = createCollectionDb({
|
||
"hlw-config": {
|
||
findOne: jest.fn().mockResolvedValue({ autoAcceptOrder: true }),
|
||
},
|
||
[COLLECTION]: { updateOne },
|
||
});
|
||
|
||
const result = await enqueueTask(db, {
|
||
type: AUTOMATION_TYPES.ACCEPT,
|
||
corpId: "corp-1",
|
||
orderId: "order-1",
|
||
});
|
||
|
||
expect(result).toMatchObject({
|
||
success: true,
|
||
created: true,
|
||
reactivated: true,
|
||
});
|
||
expect(updateOne).toHaveBeenCalledTimes(1);
|
||
expect(updateOne).toHaveBeenCalledWith(
|
||
{
|
||
taskKey: "AUTO_ACCEPT:order-1",
|
||
status: TASK_STATUS.CANCELLED,
|
||
},
|
||
expect.objectContaining({
|
||
$set: expect.objectContaining({ status: TASK_STATUS.PENDING }),
|
||
$unset: { completedAt: "", result: "", cancelReason: "" },
|
||
})
|
||
);
|
||
});
|
||
|
||
test("cancels store-drug auto open once when any ordered drug is high risk", async () => {
|
||
tencentIM.mockResolvedValue({ success: true });
|
||
const drugId = new ObjectId();
|
||
const lowRiskDrugId = new ObjectId();
|
||
const updateOne = jest
|
||
.fn()
|
||
.mockResolvedValueOnce({ modifiedCount: 0 })
|
||
.mockResolvedValueOnce({ upsertedCount: 1 })
|
||
.mockResolvedValueOnce({ modifiedCount: 0 })
|
||
.mockResolvedValueOnce({ upsertedCount: 0 });
|
||
const db = createCollectionDb({
|
||
"consult-order": {
|
||
findOne: jest.fn().mockResolvedValue({
|
||
_id: new ObjectId(),
|
||
orderId: "order-high-risk",
|
||
corpId: "corp-1",
|
||
doctorCode: "D001",
|
||
consultType: "storeMedicinePurchase",
|
||
drugs: [
|
||
{ _id: lowRiskDrugId.toString() },
|
||
{ _id: drugId.toString() },
|
||
],
|
||
}),
|
||
},
|
||
"hlw-config": {
|
||
findOne: jest.fn().mockResolvedValue({ autoOpenRx: true }),
|
||
},
|
||
"drug-info": {
|
||
findOne: jest.fn().mockResolvedValue({
|
||
_id: drugId,
|
||
name: "高风险药品",
|
||
riskLevel: "高风险",
|
||
}),
|
||
},
|
||
[COLLECTION]: { updateOne },
|
||
});
|
||
|
||
const first = await scheduleAutoOpenRx({
|
||
db,
|
||
orderId: "order-high-risk",
|
||
corpId: "corp-1",
|
||
});
|
||
const second = await scheduleAutoOpenRx({
|
||
db,
|
||
orderId: "order-high-risk",
|
||
corpId: "corp-1",
|
||
});
|
||
|
||
expect(first).toMatchObject({
|
||
success: false,
|
||
skipped: true,
|
||
manualRequired: true,
|
||
message: MANUAL_OPEN_RX_MESSAGE,
|
||
});
|
||
expect(second).toMatchObject({ manualRequired: true });
|
||
expect(updateOne).toHaveBeenNthCalledWith(
|
||
2,
|
||
{ taskKey: "AUTO_OPEN_RX:order-high-risk" },
|
||
{
|
||
$setOnInsert: expect.objectContaining({
|
||
status: TASK_STATUS.CANCELLED,
|
||
cancelReason: TASK_CANCEL_REASONS.HIGH_RISK_DRUG,
|
||
lastError: MANUAL_OPEN_RX_MESSAGE,
|
||
}),
|
||
},
|
||
{ upsert: true }
|
||
);
|
||
expect(tencentIM).toHaveBeenCalledTimes(1);
|
||
expect(tencentIM).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
formAccount: "order-high-risk",
|
||
toAccount: "D001",
|
||
msgBody: [
|
||
expect.objectContaining({
|
||
MsgContent: expect.objectContaining({
|
||
Data: "AUTORXMANUAL",
|
||
Ext: MANUAL_OPEN_RX_MESSAGE,
|
||
}),
|
||
}),
|
||
],
|
||
}),
|
||
db
|
||
);
|
||
});
|
||
|
||
test("checks the online drug collection and allows low-risk tasks to reactivate", async () => {
|
||
const drugId = new ObjectId();
|
||
const updateOne = jest.fn().mockResolvedValue({ modifiedCount: 1 });
|
||
const onlineFindOne = jest.fn().mockResolvedValue(null);
|
||
const db = createCollectionDb({
|
||
"consult-order": {
|
||
findOne: jest.fn().mockResolvedValue({
|
||
_id: new ObjectId(),
|
||
consultType: "onlineMedicinePurchase",
|
||
drugs: [{ _id: drugId.toString() }],
|
||
}),
|
||
},
|
||
"hlw-config": {
|
||
findOne: jest.fn().mockResolvedValue({ autoOpenRx: true }),
|
||
},
|
||
"online-drug-info": { findOne: onlineFindOne },
|
||
[COLLECTION]: { updateOne },
|
||
});
|
||
|
||
await expect(
|
||
scheduleAutoOpenRx({
|
||
db,
|
||
orderId: "order-online-low-risk",
|
||
corpId: "corp-1",
|
||
})
|
||
).resolves.toMatchObject({
|
||
success: true,
|
||
reactivated: true,
|
||
});
|
||
|
||
expect(onlineFindOne).toHaveBeenCalledWith(
|
||
{ _id: { $in: [expect.any(ObjectId)] }, riskLevel: "高风险" },
|
||
{ projection: { _id: 1, name: 1, riskLevel: 1 } }
|
||
);
|
||
expect(updateOne).toHaveBeenCalledWith(
|
||
{
|
||
taskKey: "AUTO_OPEN_RX:order-online-low-risk",
|
||
status: TASK_STATUS.CANCELLED,
|
||
},
|
||
expect.objectContaining({
|
||
$set: expect.objectContaining({ status: TASK_STATUS.PENDING }),
|
||
$unset: { completedAt: "", result: "", cancelReason: "" },
|
||
})
|
||
);
|
||
});
|
||
|
||
test("cancels auto open for a high-risk online drug", async () => {
|
||
tencentIM.mockResolvedValue({ success: true });
|
||
const drugId = new ObjectId();
|
||
const updateOne = jest
|
||
.fn()
|
||
.mockResolvedValueOnce({ modifiedCount: 0 })
|
||
.mockResolvedValueOnce({ upsertedCount: 1 });
|
||
const db = createCollectionDb({
|
||
"consult-order": {
|
||
findOne: jest.fn().mockResolvedValue({
|
||
_id: new ObjectId(),
|
||
doctorCode: "D002",
|
||
consultType: "onlineMedicinePurchase",
|
||
drugs: [{ _id: drugId.toString() }],
|
||
}),
|
||
},
|
||
"hlw-config": {
|
||
findOne: jest.fn().mockResolvedValue({ autoOpenRx: true }),
|
||
},
|
||
"online-drug-info": {
|
||
findOne: jest.fn().mockResolvedValue({
|
||
_id: drugId,
|
||
riskLevel: "高风险",
|
||
}),
|
||
},
|
||
[COLLECTION]: { updateOne },
|
||
});
|
||
|
||
await expect(
|
||
scheduleAutoOpenRx({
|
||
db,
|
||
orderId: "order-online-high-risk",
|
||
corpId: "corp-1",
|
||
})
|
||
).resolves.toMatchObject({
|
||
skipped: true,
|
||
manualRequired: true,
|
||
});
|
||
expect(tencentIM).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
test("treats a concurrent unique-key insert as idempotent success", async () => {
|
||
const duplicateError = Object.assign(new Error("duplicate key"), {
|
||
code: 11000,
|
||
});
|
||
const updateOne = jest
|
||
.fn()
|
||
.mockResolvedValueOnce({ modifiedCount: 0 })
|
||
.mockRejectedValueOnce(duplicateError);
|
||
const db = createCollectionDb({
|
||
"hlw-config": {
|
||
findOne: jest.fn().mockResolvedValue({ autoPassRx: true }),
|
||
},
|
||
[COLLECTION]: { updateOne },
|
||
});
|
||
|
||
await expect(
|
||
enqueueTask(db, {
|
||
type: AUTOMATION_TYPES.PASS_RX,
|
||
corpId: "corp-1",
|
||
orderId: "order-1",
|
||
rxId: new ObjectId().toString(),
|
||
})
|
||
).resolves.toMatchObject({
|
||
success: true,
|
||
created: false,
|
||
});
|
||
});
|
||
|
||
test("claims due and expired-lease tasks with one atomic update", async () => {
|
||
const claimed = {
|
||
_id: new ObjectId(),
|
||
taskKey: "AUTO_ACCEPT:order-1",
|
||
status: TASK_STATUS.RUNNING,
|
||
};
|
||
const findOneAndUpdate = jest.fn().mockResolvedValue(claimed);
|
||
const db = createCollectionDb({
|
||
[COLLECTION]: { findOneAndUpdate },
|
||
});
|
||
|
||
await expect(claimNextTask(db, 10_000)).resolves.toBe(claimed);
|
||
expect(findOneAndUpdate).toHaveBeenCalledWith(
|
||
{
|
||
$or: [
|
||
{
|
||
status: TASK_STATUS.PENDING,
|
||
nextRunAt: { $lte: 10_000 },
|
||
},
|
||
{
|
||
status: TASK_STATUS.RUNNING,
|
||
leaseUntil: { $lte: 10_000 },
|
||
},
|
||
],
|
||
},
|
||
expect.objectContaining({
|
||
$set: expect.objectContaining({
|
||
status: TASK_STATUS.RUNNING,
|
||
leaseOwner: expect.any(String),
|
||
}),
|
||
$inc: { attempts: 1 },
|
||
}),
|
||
{
|
||
sort: { nextRunAt: 1, createdAt: 1 },
|
||
returnDocument: "after",
|
||
}
|
||
);
|
||
});
|
||
|
||
test("cancels a claimed task when its switch has been turned off", async () => {
|
||
const updateOne = jest.fn().mockResolvedValue({ modifiedCount: 1 });
|
||
const db = createCollectionDb({
|
||
"hlw-config": {
|
||
findOne: jest.fn().mockResolvedValue({ autoAcceptOrder: false }),
|
||
},
|
||
[COLLECTION]: { updateOne },
|
||
});
|
||
const task = {
|
||
_id: new ObjectId(),
|
||
type: AUTOMATION_TYPES.ACCEPT,
|
||
corpId: "corp-1",
|
||
orderId: "order-1",
|
||
attempts: 1,
|
||
expiresAt: Date.now() + 60_000,
|
||
};
|
||
|
||
await processClaimedTask(db, task);
|
||
|
||
expect(updateOne).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
_id: task._id,
|
||
status: TASK_STATUS.RUNNING,
|
||
leaseOwner: expect.any(String),
|
||
}),
|
||
expect.objectContaining({
|
||
$set: expect.objectContaining({
|
||
status: TASK_STATUS.CANCELLED,
|
||
completedAt: expect.any(Number),
|
||
}),
|
||
})
|
||
);
|
||
});
|
||
|
||
test("reconciliation recreates a missing task after service restart", async () => {
|
||
jest.spyOn(Date, "now").mockReturnValue(2_000_000);
|
||
const taskUpdateOne = jest
|
||
.fn()
|
||
.mockResolvedValueOnce({ modifiedCount: 0 })
|
||
.mockResolvedValueOnce({ upsertedCount: 1 });
|
||
const order = {
|
||
orderId: "order-recovery",
|
||
corpId: "corp-1",
|
||
orderSource: "ALIPAY_MINI",
|
||
orderStatus: "pending",
|
||
createTime: 1_990_000,
|
||
expireTime: 2_060_000,
|
||
};
|
||
const db = createCollectionDb({
|
||
"consult-order": {
|
||
find: jest.fn().mockReturnValue({
|
||
toArray: jest.fn().mockResolvedValue([order]),
|
||
}),
|
||
findOne: jest.fn().mockResolvedValue({ _id: new ObjectId() }),
|
||
},
|
||
"hlw-config": {
|
||
find: jest.fn().mockReturnValue({
|
||
toArray: jest
|
||
.fn()
|
||
.mockResolvedValue([{ corpId: "corp-1", autoAcceptOrder: true }]),
|
||
}),
|
||
findOne: jest
|
||
.fn()
|
||
.mockResolvedValue({ corpId: "corp-1", autoAcceptOrder: true }),
|
||
},
|
||
"diagnostic-record": {
|
||
find: jest.fn().mockReturnValue({
|
||
toArray: jest.fn().mockResolvedValue([]),
|
||
}),
|
||
},
|
||
[COLLECTION]: { updateOne: taskUpdateOne },
|
||
});
|
||
|
||
await reconcileAutomationTasks(db);
|
||
|
||
expect(taskUpdateOne).toHaveBeenLastCalledWith(
|
||
{ taskKey: "AUTO_ACCEPT:order-recovery" },
|
||
expect.objectContaining({
|
||
$setOnInsert: expect.objectContaining({
|
||
orderId: "order-recovery",
|
||
status: TASK_STATUS.PENDING,
|
||
}),
|
||
}),
|
||
{ upsert: true }
|
||
);
|
||
});
|
||
|
||
test("cancels a queued auto-open task when a drug becomes high risk before execution", async () => {
|
||
tencentIM.mockResolvedValue({ success: true });
|
||
const drugId = new ObjectId();
|
||
const taskUpdateOne = jest.fn().mockResolvedValue({ modifiedCount: 1 });
|
||
const order = {
|
||
orderId: "order-risk-changed",
|
||
corpId: "corp-1",
|
||
orderSource: "ALIPAY_MINI",
|
||
orderStatus: "processing",
|
||
doctorCode: "D001",
|
||
consultType: "storeMedicinePurchase",
|
||
drugs: [{ _id: drugId.toString() }],
|
||
expireTime: Date.now() + 60_000,
|
||
};
|
||
const db = createCollectionDb({
|
||
"hlw-config": {
|
||
findOne: jest.fn().mockResolvedValue({ autoOpenRx: true }),
|
||
},
|
||
"consult-order": {
|
||
findOne: jest.fn().mockResolvedValue(order),
|
||
},
|
||
"diagnostic-record": {
|
||
findOne: jest.fn().mockResolvedValue(null),
|
||
},
|
||
"hlw-doctor": {
|
||
findOne: jest.fn().mockResolvedValue({
|
||
doctorNo: "D001",
|
||
onlineStatus: "online",
|
||
}),
|
||
},
|
||
"drug-info": {
|
||
find: jest.fn().mockReturnValue({
|
||
toArray: jest.fn().mockResolvedValue([
|
||
{
|
||
_id: drugId,
|
||
onSale: true,
|
||
name: "风险变更药品",
|
||
riskLevel: "高风险",
|
||
},
|
||
]),
|
||
}),
|
||
},
|
||
[COLLECTION]: { updateOne: taskUpdateOne },
|
||
});
|
||
const task = {
|
||
_id: new ObjectId(),
|
||
type: AUTOMATION_TYPES.OPEN_RX,
|
||
corpId: "corp-1",
|
||
orderId: "order-risk-changed",
|
||
attempts: 1,
|
||
expiresAt: Date.now() + 60_000,
|
||
};
|
||
|
||
await processClaimedTask(db, task);
|
||
|
||
expect(taskUpdateOne).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
_id: task._id,
|
||
status: TASK_STATUS.RUNNING,
|
||
}),
|
||
expect.objectContaining({
|
||
$set: expect.objectContaining({
|
||
status: TASK_STATUS.CANCELLED,
|
||
cancelReason: TASK_CANCEL_REASONS.HIGH_RISK_DRUG,
|
||
lastError: MANUAL_OPEN_RX_MESSAGE,
|
||
}),
|
||
})
|
||
);
|
||
expect(diagnosticRecord).not.toHaveBeenCalled();
|
||
expect(tencentIM).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
toAccount: "D001",
|
||
msgBody: [
|
||
expect.objectContaining({
|
||
MsgContent: expect.objectContaining({
|
||
Data: "AUTORXMANUAL",
|
||
Ext: MANUAL_OPEN_RX_MESSAGE,
|
||
}),
|
||
}),
|
||
],
|
||
}),
|
||
db
|
||
);
|
||
});
|
||
|
||
test("fails automatic prescription immediately and sends the failure reason", async () => {
|
||
tencentIM.mockResolvedValue({ success: true });
|
||
const taskUpdateOne = jest.fn().mockResolvedValue({ modifiedCount: 1 });
|
||
const order = {
|
||
orderId: "order-1",
|
||
corpId: "corp-1",
|
||
orderSource: "ALIPAY_MINI",
|
||
orderStatus: "processing",
|
||
doctorCode: "D001",
|
||
expireTime: Date.now() + 60_000,
|
||
};
|
||
const db = createCollectionDb({
|
||
"hlw-config": {
|
||
findOne: jest.fn().mockResolvedValue({ autoOpenRx: true }),
|
||
},
|
||
"consult-order": {
|
||
findOne: jest.fn().mockResolvedValue(order),
|
||
},
|
||
"diagnostic-record": {
|
||
findOne: jest.fn().mockResolvedValue(null),
|
||
},
|
||
"hlw-doctor": {
|
||
findOne: jest.fn().mockResolvedValue(null),
|
||
},
|
||
[COLLECTION]: { updateOne: taskUpdateOne },
|
||
});
|
||
const task = {
|
||
_id: new ObjectId(),
|
||
type: AUTOMATION_TYPES.OPEN_RX,
|
||
corpId: "corp-1",
|
||
orderId: "order-1",
|
||
attempts: 1,
|
||
expiresAt: Date.now() + 60_000,
|
||
};
|
||
|
||
await processClaimedTask(db, task);
|
||
|
||
expect(taskUpdateOne).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
_id: task._id,
|
||
status: TASK_STATUS.RUNNING,
|
||
}),
|
||
expect.objectContaining({
|
||
$set: expect.objectContaining({
|
||
status: TASK_STATUS.FAILED,
|
||
completedAt: expect.any(Number),
|
||
lastError: "开方医生不存在",
|
||
}),
|
||
})
|
||
);
|
||
expect(tencentIM).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
type: "sendSystemNotification",
|
||
formAccount: "order-1",
|
||
toAccount: "D001",
|
||
SyncOtherMachine: 1,
|
||
msgBody: [
|
||
expect.objectContaining({
|
||
MsgContent: expect.objectContaining({
|
||
Data: "AUTORXFAIL",
|
||
Desc: "notification",
|
||
Ext: JSON.stringify({
|
||
errorSource: ERROR_SOURCES.SYSTEM,
|
||
reason: "系统错误:开方医生不存在",
|
||
}),
|
||
}),
|
||
}),
|
||
],
|
||
}),
|
||
db
|
||
);
|
||
expect(taskUpdateOne).not.toHaveBeenCalledWith(
|
||
expect.anything(),
|
||
expect.objectContaining({
|
||
$set: expect.objectContaining({ status: TASK_STATUS.PENDING }),
|
||
})
|
||
);
|
||
});
|
||
|
||
test("blocks automatic prescription when rational-drug review fails", async () => {
|
||
tencentIM.mockResolvedValue({ success: true });
|
||
diagnosticRecord.mockResolvedValue({
|
||
success: false,
|
||
message: "该处方存在不合理用药",
|
||
});
|
||
const drugId = new ObjectId();
|
||
const taskUpdateOne = jest.fn().mockResolvedValue({ modifiedCount: 1 });
|
||
const order = {
|
||
orderId: "order-review-failed",
|
||
corpId: "corp-1",
|
||
orderSource: "ALIPAY_MINI",
|
||
orderStatus: "processing",
|
||
doctorCode: "D001",
|
||
patientId: "P001",
|
||
name: "患者甲",
|
||
description: "鼻塞流涕",
|
||
medInfo: { dise_codg: "J00", dise_name: "普通感冒" },
|
||
drugs: [
|
||
{
|
||
_id: drugId.toString(),
|
||
dosage: 10,
|
||
dosage_unit: "mg",
|
||
quantity: 1,
|
||
usageName: "口服",
|
||
frequencyName: "每日两次",
|
||
unit: "盒",
|
||
},
|
||
],
|
||
expireTime: Date.now() + 60_000,
|
||
};
|
||
const db = createCollectionDb({
|
||
"hlw-config": {
|
||
findOne: jest.fn().mockResolvedValue({ autoOpenRx: true }),
|
||
find: jest.fn().mockReturnValue({
|
||
toArray: jest.fn().mockResolvedValue([
|
||
{ key: "store-medicine-dosage-unit", list: [{ code: "MG", name: "mg" }] },
|
||
{ key: "store-medicine-frequence", list: [{ code: "BID", name: "每日两次" }] },
|
||
{ key: "store-medicine-administration", list: [{ code: "PO", name: "口服" }] },
|
||
{ key: "medicine-package-unit", list: [{ code: "盒", name: "盒" }] },
|
||
]),
|
||
}),
|
||
},
|
||
"consult-order": { findOne: jest.fn().mockResolvedValue(order) },
|
||
"diagnostic-record": { findOne: jest.fn().mockResolvedValue(null) },
|
||
"hlw-doctor": {
|
||
findOne: jest.fn().mockResolvedValue({
|
||
doctorNo: "D001",
|
||
signatureUrl: "doctor-sign",
|
||
onlineStatus: "online",
|
||
}),
|
||
},
|
||
"drug-info": {
|
||
find: jest.fn().mockReturnValue({
|
||
toArray: jest.fn().mockResolvedValue([
|
||
{
|
||
_id: drugId,
|
||
onSale: true,
|
||
name: "测试药品",
|
||
erpId: "ERP-1",
|
||
insurance_code: "MED-1",
|
||
dosage_form: "片剂",
|
||
days: 3,
|
||
dosage_unit: "mg",
|
||
administration_method: "口服",
|
||
freq: "每日两次",
|
||
unit: "盒",
|
||
},
|
||
]),
|
||
}),
|
||
},
|
||
[COLLECTION]: { updateOne: taskUpdateOne },
|
||
});
|
||
const task = {
|
||
_id: new ObjectId(),
|
||
type: AUTOMATION_TYPES.OPEN_RX,
|
||
corpId: "corp-1",
|
||
orderId: order.orderId,
|
||
attempts: 1,
|
||
expiresAt: Date.now() + 60_000,
|
||
};
|
||
|
||
await processClaimedTask(db, task);
|
||
|
||
expect(diagnosticRecord).toHaveBeenCalledTimes(1);
|
||
expect(diagnosticRecord).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
type: "getYbReviewResult",
|
||
corpId: order.corpId,
|
||
params: expect.objectContaining({ orderId: order.orderId }),
|
||
}),
|
||
db
|
||
);
|
||
expect(taskUpdateOne).toHaveBeenCalledWith(
|
||
expect.objectContaining({ _id: task._id, status: TASK_STATUS.RUNNING }),
|
||
expect.objectContaining({
|
||
$set: expect.objectContaining({
|
||
status: TASK_STATUS.FAILED,
|
||
lastError: "该处方存在不合理用药",
|
||
}),
|
||
})
|
||
);
|
||
});
|
||
|
||
test("fails automatic audit immediately and sends the failure reason", async () => {
|
||
tencentIM.mockResolvedValue({ success: true });
|
||
const rxId = new ObjectId();
|
||
const taskUpdateOne = jest.fn().mockResolvedValue({ modifiedCount: 1 });
|
||
const db = createCollectionDb({
|
||
"hlw-config": {
|
||
findOne: jest.fn().mockResolvedValue({ autoPassRx: true }),
|
||
},
|
||
"diagnostic-record": {
|
||
findOne: jest.fn().mockResolvedValue({
|
||
_id: rxId,
|
||
orderId: "order-1",
|
||
status: "INIT",
|
||
pharmacistNo: "P001",
|
||
}),
|
||
},
|
||
"hlw-doctor": {
|
||
findOne: jest.fn().mockResolvedValue({
|
||
doctorNo: "P001",
|
||
onlineStatus: "offline",
|
||
}),
|
||
},
|
||
"consult-order": {
|
||
findOne: jest.fn().mockResolvedValue({ doctorCode: "D001" }),
|
||
},
|
||
[COLLECTION]: { updateOne: taskUpdateOne },
|
||
});
|
||
const task = {
|
||
_id: new ObjectId(),
|
||
type: AUTOMATION_TYPES.PASS_RX,
|
||
corpId: "corp-1",
|
||
orderId: "order-1",
|
||
rxId: rxId.toString(),
|
||
attempts: 1,
|
||
expiresAt: Date.now() + 60_000,
|
||
};
|
||
|
||
await processClaimedTask(db, task);
|
||
|
||
expect(taskUpdateOne).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
_id: task._id,
|
||
status: TASK_STATUS.RUNNING,
|
||
}),
|
||
expect.objectContaining({
|
||
$set: expect.objectContaining({
|
||
status: TASK_STATUS.FAILED,
|
||
completedAt: expect.any(Number),
|
||
lastError: "审方药师暂不在线",
|
||
}),
|
||
})
|
||
);
|
||
expect(tencentIM).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
type: "sendSystemNotification",
|
||
formAccount: "order-1",
|
||
toAccount: "D001",
|
||
SyncOtherMachine: 1,
|
||
msgBody: [
|
||
expect.objectContaining({
|
||
MsgContent: expect.objectContaining({
|
||
Data: "AUTORXFAIL",
|
||
Desc: "notification",
|
||
Ext: JSON.stringify({
|
||
errorSource: ERROR_SOURCES.SYSTEM,
|
||
reason: "系统错误:审方药师暂不在线",
|
||
}),
|
||
}),
|
||
}),
|
||
],
|
||
}),
|
||
db
|
||
);
|
||
expect(taskUpdateOne).not.toHaveBeenCalledWith(
|
||
expect.anything(),
|
||
expect.objectContaining({
|
||
$set: expect.objectContaining({ status: TASK_STATUS.PENDING }),
|
||
})
|
||
);
|
||
});
|
||
|
||
test("labels HIS prescription upload failures in the notification", async () => {
|
||
tencentIM.mockResolvedValue({ success: true });
|
||
diagnosticRecord.mockResolvedValue({
|
||
success: false,
|
||
failList: [{ message: "[his]处方上传失败: HIS拒绝接收处方" }],
|
||
});
|
||
const rxId = new ObjectId();
|
||
const taskUpdateOne = jest.fn().mockResolvedValue({ modifiedCount: 1 });
|
||
const db = createCollectionDb({
|
||
"hlw-config": {
|
||
findOne: jest.fn().mockResolvedValue({ autoPassRx: true }),
|
||
},
|
||
"diagnostic-record": {
|
||
findOne: jest.fn().mockResolvedValue({
|
||
_id: rxId,
|
||
orderId: "order-his-failure",
|
||
status: "INIT",
|
||
pharmacistNo: "P001",
|
||
}),
|
||
},
|
||
"hlw-doctor": {
|
||
findOne: jest.fn().mockResolvedValue({
|
||
doctorNo: "P001",
|
||
onlineStatus: "online",
|
||
}),
|
||
},
|
||
"consult-order": {
|
||
findOne: jest.fn().mockResolvedValue({ doctorCode: "D001" }),
|
||
},
|
||
[COLLECTION]: { updateOne: taskUpdateOne },
|
||
});
|
||
const task = {
|
||
_id: new ObjectId(),
|
||
type: AUTOMATION_TYPES.PASS_RX,
|
||
corpId: "corp-1",
|
||
orderId: "order-his-failure",
|
||
rxId: rxId.toString(),
|
||
attempts: 1,
|
||
expiresAt: Date.now() + 60_000,
|
||
};
|
||
|
||
await processClaimedTask(db, task);
|
||
|
||
expect(tencentIM).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
msgBody: [
|
||
expect.objectContaining({
|
||
MsgContent: expect.objectContaining({
|
||
Data: "AUTORXFAIL",
|
||
Ext: JSON.stringify({
|
||
errorSource: ERROR_SOURCES.HIS,
|
||
reason: "HIS上传错误:HIS拒绝接收处方",
|
||
}),
|
||
}),
|
||
}),
|
||
],
|
||
}),
|
||
db
|
||
);
|
||
expect(taskUpdateOne).toHaveBeenCalledWith(
|
||
expect.anything(),
|
||
expect.objectContaining({
|
||
$set: expect.objectContaining({
|
||
status: TASK_STATUS.FAILED,
|
||
lastError: "[his]处方上传失败: HIS拒绝接收处方",
|
||
}),
|
||
})
|
||
);
|
||
});
|
||
|
||
test("keeps ordinary automatic audit failures classified as system errors", async () => {
|
||
tencentIM.mockResolvedValue({ success: true });
|
||
diagnosticRecord.mockResolvedValue({
|
||
success: false,
|
||
failList: [{ message: "处方数据更新失败" }],
|
||
});
|
||
const rxId = new ObjectId();
|
||
const db = createCollectionDb({
|
||
"hlw-config": {
|
||
findOne: jest.fn().mockResolvedValue({ autoPassRx: true }),
|
||
},
|
||
"diagnostic-record": {
|
||
findOne: jest.fn().mockResolvedValue({
|
||
_id: rxId,
|
||
orderId: "order-system-failure",
|
||
status: "INIT",
|
||
pharmacistNo: "P001",
|
||
}),
|
||
},
|
||
"hlw-doctor": {
|
||
findOne: jest.fn().mockResolvedValue({
|
||
doctorNo: "P001",
|
||
onlineStatus: "online",
|
||
}),
|
||
},
|
||
"consult-order": {
|
||
findOne: jest.fn().mockResolvedValue({ doctorCode: "D001" }),
|
||
},
|
||
[COLLECTION]: {
|
||
updateOne: jest.fn().mockResolvedValue({ modifiedCount: 1 }),
|
||
},
|
||
});
|
||
const task = {
|
||
_id: new ObjectId(),
|
||
type: AUTOMATION_TYPES.PASS_RX,
|
||
corpId: "corp-1",
|
||
orderId: "order-system-failure",
|
||
rxId: rxId.toString(),
|
||
attempts: 1,
|
||
expiresAt: Date.now() + 60_000,
|
||
};
|
||
|
||
await processClaimedTask(db, task);
|
||
|
||
expect(tencentIM).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
msgBody: [
|
||
expect.objectContaining({
|
||
MsgContent: expect.objectContaining({
|
||
Ext: JSON.stringify({
|
||
errorSource: ERROR_SOURCES.SYSTEM,
|
||
reason: "系统错误:处方数据更新失败",
|
||
}),
|
||
}),
|
||
}),
|
||
],
|
||
}),
|
||
db
|
||
);
|
||
});
|
||
|
||
test("uses the system unknown-error fallback when no error message exists", () => {
|
||
expect(getFailureNotificationExt()).toEqual({
|
||
errorSource: ERROR_SOURCES.SYSTEM,
|
||
reason: "系统错误:未知错误",
|
||
});
|
||
expect(
|
||
getFailureNotificationExt(
|
||
new AutomationError("", { source: ERROR_SOURCES.SYSTEM })
|
||
)
|
||
).toEqual({
|
||
errorSource: ERROR_SOURCES.SYSTEM,
|
||
reason: "系统错误:未知错误",
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("automatic prescription assembly", () => {
|
||
test("reloads master data and produces the existing prescription contract", async () => {
|
||
const drugId = new ObjectId();
|
||
const findConfig = jest.fn((query) => {
|
||
if (query.group) {
|
||
return {
|
||
toArray: jest.fn().mockResolvedValue([
|
||
{
|
||
key: "store-medicine-dosage-unit",
|
||
list: [{ code: "MG", name: "mg" }],
|
||
},
|
||
{
|
||
key: "store-medicine-frequence",
|
||
list: [{ code: "BID", name: "每日两次" }],
|
||
},
|
||
{
|
||
key: "store-medicine-administration",
|
||
list: [{ code: "PO", name: "口服" }],
|
||
},
|
||
{
|
||
key: "medicine-package-unit",
|
||
list: [{ code: "盒", name: "盒" }],
|
||
},
|
||
]),
|
||
};
|
||
}
|
||
throw new Error("unexpected config find");
|
||
});
|
||
const db = createCollectionDb({
|
||
"hlw-doctor": {
|
||
findOne: jest.fn().mockResolvedValue({
|
||
doctorNo: "D001",
|
||
signatureUrl: "doctor-sign",
|
||
onlineStatus: "online",
|
||
}),
|
||
},
|
||
"hlw-diagnosis": {
|
||
find: jest.fn().mockReturnValue({
|
||
toArray: jest
|
||
.fn()
|
||
.mockResolvedValue([{ code: "J00", name: "普通感冒" }]),
|
||
}),
|
||
},
|
||
"drug-info": {
|
||
find: jest.fn().mockReturnValue({
|
||
toArray: jest.fn().mockResolvedValue([
|
||
{
|
||
_id: drugId,
|
||
onSale: true,
|
||
name: "测试药品",
|
||
erpId: "ERP-1",
|
||
insurance_code: "MED-1",
|
||
product_id: "P-1",
|
||
dosage_form: "片剂",
|
||
days: 3,
|
||
dosage_unit: "mg",
|
||
administration_method: "口服",
|
||
freq: "每日两次",
|
||
unit: "盒",
|
||
specification: "10mg*10片",
|
||
package_amount: 10,
|
||
recommended_quantity: 1,
|
||
},
|
||
]),
|
||
}),
|
||
},
|
||
"hlw-config": {
|
||
find: findConfig,
|
||
findOne: jest
|
||
.fn()
|
||
.mockResolvedValue({ medicinePurchaseRxDuration: 30 }),
|
||
},
|
||
});
|
||
const order = {
|
||
corpId: "corp-1",
|
||
orderId: "order-1",
|
||
orderSource: "ALIPAY_MINI",
|
||
consultType: "storeMedicinePurchase",
|
||
doctorCode: "D001",
|
||
doctorName: "医生甲",
|
||
patientId: "P001",
|
||
name: "患者甲",
|
||
diseases: ["普通感冒"],
|
||
description: "鼻塞流涕",
|
||
pastHistoryStr: "两天前起病",
|
||
drugs: [
|
||
{
|
||
_id: drugId.toString(),
|
||
dosage: 10,
|
||
dosage_unit: "mg",
|
||
quantity: 1,
|
||
usageName: "口服",
|
||
frequencyName: "每日两次",
|
||
unit: "盒",
|
||
},
|
||
],
|
||
expireTime: Date.now() + 60_000,
|
||
};
|
||
|
||
const params = await buildAutoPrescriptionParams(db, order);
|
||
|
||
expect(params).toMatchObject({
|
||
complaint: "普通感冒 鼻塞流涕",
|
||
presentIllness: "两天前起病",
|
||
doctorCAUserId: "doctor-sign",
|
||
prescriptionType: "storeMedicinePurchase",
|
||
diagnosisList: [{ code: "J00", name: "普通感冒", desc: "" }],
|
||
drugs: [
|
||
expect.objectContaining({
|
||
_id: drugId.toString(),
|
||
erpId: "ERP-1",
|
||
dosage_unit_code: "MG",
|
||
frequencyCode: "BID",
|
||
usageCode: "PO",
|
||
quantity: 1,
|
||
days: 3,
|
||
}),
|
||
],
|
||
});
|
||
});
|
||
});
|