2026-07-31 16:22:09 +08:00
|
|
|
|
const { ObjectId } = require("mongodb");
|
2026-08-03 16:14:45 +08:00
|
|
|
|
jest.mock("../tencent-im", () => jest.fn());
|
2026-08-05 18:39:56 +08:00
|
|
|
|
jest.mock("../diagnostic-record", () => jest.fn());
|
2026-08-03 16:14:45 +08:00
|
|
|
|
const tencentIM = require("../tencent-im");
|
2026-08-05 18:39:56 +08:00
|
|
|
|
const diagnosticRecord = require("../diagnostic-record");
|
2026-07-31 16:22:09 +08:00
|
|
|
|
const {
|
|
|
|
|
|
AUTOMATION_TYPES,
|
|
|
|
|
|
COLLECTION,
|
2026-08-05 18:39:56 +08:00
|
|
|
|
ERROR_SOURCES,
|
2026-08-24 16:41:11 +08:00
|
|
|
|
MANUAL_OPEN_RX_MESSAGE,
|
|
|
|
|
|
TASK_CANCEL_REASONS,
|
2026-07-31 16:22:09 +08:00
|
|
|
|
TASK_STATUS,
|
2026-08-05 18:39:56 +08:00
|
|
|
|
AutomationError,
|
2026-07-31 16:22:09 +08:00
|
|
|
|
buildAutoPrescriptionParams,
|
|
|
|
|
|
calculateDueAt,
|
|
|
|
|
|
claimNextTask,
|
|
|
|
|
|
enqueueTask,
|
2026-08-05 18:39:56 +08:00
|
|
|
|
getFailureNotificationExt,
|
2026-07-31 16:22:09 +08:00
|
|
|
|
processClaimedTask,
|
|
|
|
|
|
reconcileAutomationTasks,
|
2026-08-24 16:41:11 +08:00
|
|
|
|
scheduleAutoOpenRx,
|
2026-07-31 16:22:09 +08:00
|
|
|
|
} = 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();
|
2026-08-03 16:14:45 +08:00
|
|
|
|
jest.clearAllMocks();
|
2026-07-31 16:22:09 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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 }),
|
2026-08-24 16:41:11 +08:00
|
|
|
|
$unset: { completedAt: "", result: "", cancelReason: "" },
|
2026-07-31 16:22:09 +08:00
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-24 16:41:11 +08:00
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-31 16:22:09 +08:00
|
|
|
|
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 }
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-24 16:41:11 +08:00
|
|
|
|
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
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-03 16:14:45 +08:00
|
|
|
|
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({
|
2026-08-05 18:39:56 +08:00
|
|
|
|
Data: "AUTORXFAIL",
|
2026-08-03 16:14:45 +08:00
|
|
|
|
Desc: "notification",
|
2026-08-05 18:39:56 +08:00
|
|
|
|
Ext: JSON.stringify({
|
|
|
|
|
|
errorSource: ERROR_SOURCES.SYSTEM,
|
|
|
|
|
|
reason: "系统错误:开方医生不存在",
|
|
|
|
|
|
}),
|
2026-08-03 16:14:45 +08:00
|
|
|
|
}),
|
|
|
|
|
|
}),
|
|
|
|
|
|
],
|
|
|
|
|
|
}),
|
|
|
|
|
|
db
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(taskUpdateOne).not.toHaveBeenCalledWith(
|
|
|
|
|
|
expect.anything(),
|
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
|
$set: expect.objectContaining({ status: TASK_STATUS.PENDING }),
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-09-04 09:57:39 +08:00
|
|
|
|
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: "该处方存在不合理用药",
|
|
|
|
|
|
}),
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-03 16:14:45 +08:00
|
|
|
|
test("fails automatic audit immediately and sends the failure reason", async () => {
|
|
|
|
|
|
tencentIM.mockResolvedValue({ success: true });
|
2026-07-31 16:22:09 +08:00
|
|
|
|
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",
|
|
|
|
|
|
}),
|
|
|
|
|
|
},
|
2026-08-03 16:14:45 +08:00
|
|
|
|
"consult-order": {
|
|
|
|
|
|
findOne: jest.fn().mockResolvedValue({ doctorCode: "D001" }),
|
|
|
|
|
|
},
|
2026-07-31 16:22:09 +08:00
|
|
|
|
[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({
|
2026-08-03 16:14:45 +08:00
|
|
|
|
status: TASK_STATUS.FAILED,
|
|
|
|
|
|
completedAt: expect.any(Number),
|
2026-07-31 16:22:09 +08:00
|
|
|
|
lastError: "审方药师暂不在线",
|
|
|
|
|
|
}),
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
2026-08-03 16:14:45 +08:00
|
|
|
|
expect(tencentIM).toHaveBeenCalledWith(
|
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
|
type: "sendSystemNotification",
|
|
|
|
|
|
formAccount: "order-1",
|
|
|
|
|
|
toAccount: "D001",
|
|
|
|
|
|
SyncOtherMachine: 1,
|
|
|
|
|
|
msgBody: [
|
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
|
MsgContent: expect.objectContaining({
|
2026-08-05 18:39:56 +08:00
|
|
|
|
Data: "AUTORXFAIL",
|
2026-08-03 16:14:45 +08:00
|
|
|
|
Desc: "notification",
|
2026-08-05 18:39:56 +08:00
|
|
|
|
Ext: JSON.stringify({
|
|
|
|
|
|
errorSource: ERROR_SOURCES.SYSTEM,
|
|
|
|
|
|
reason: "系统错误:审方药师暂不在线",
|
|
|
|
|
|
}),
|
2026-08-03 16:14:45 +08:00
|
|
|
|
}),
|
|
|
|
|
|
}),
|
|
|
|
|
|
],
|
|
|
|
|
|
}),
|
|
|
|
|
|
db
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(taskUpdateOne).not.toHaveBeenCalledWith(
|
|
|
|
|
|
expect.anything(),
|
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
|
$set: expect.objectContaining({ status: TASK_STATUS.PENDING }),
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
2026-07-31 16:22:09 +08:00
|
|
|
|
});
|
2026-08-05 18:39:56 +08:00
|
|
|
|
|
|
|
|
|
|
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: "系统错误:未知错误",
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-07-31 16:22:09 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
}),
|
|
|
|
|
|
],
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|