fix: 需求调整
This commit is contained in:
parent
be51c7be1b
commit
c9c262e3a1
@ -21,6 +21,11 @@ const ERROR_SOURCES = {
|
|||||||
SYSTEM: "SYSTEM",
|
SYSTEM: "SYSTEM",
|
||||||
HIS: "HIS",
|
HIS: "HIS",
|
||||||
};
|
};
|
||||||
|
const TASK_CANCEL_REASONS = {
|
||||||
|
HIGH_RISK_DRUG: "HIGH_RISK_DRUG",
|
||||||
|
};
|
||||||
|
const MANUAL_OPEN_RX_MESSAGE =
|
||||||
|
"订单中含高风险药品,已停止自动开方,请医生人工开方。";
|
||||||
const LEASE_MS = 2 * 60 * 1000;
|
const LEASE_MS = 2 * 60 * 1000;
|
||||||
const POLL_INTERVAL_MS = 1000;
|
const POLL_INTERVAL_MS = 1000;
|
||||||
const RECONCILE_INTERVAL_MS = 30 * 1000;
|
const RECONCILE_INTERVAL_MS = 30 * 1000;
|
||||||
@ -43,6 +48,14 @@ class AutomationError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ManualPrescriptionRequiredError extends AutomationError {
|
||||||
|
constructor(message = MANUAL_OPEN_RX_MESSAGE) {
|
||||||
|
super(message);
|
||||||
|
this.name = "ManualPrescriptionRequiredError";
|
||||||
|
this.cancelReason = TASK_CANCEL_REASONS.HIGH_RISK_DRUG;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getTaskKey(type, orderId, rxId) {
|
function getTaskKey(type, orderId, rxId) {
|
||||||
if (type === AUTOMATION_TYPES.PASS_RX) {
|
if (type === AUTOMATION_TYPES.PASS_RX) {
|
||||||
return `${type}:${rxId}`;
|
return `${type}:${rxId}`;
|
||||||
@ -83,7 +96,15 @@ function calculateDueAt(type, config, baseTime = Date.now(), random = Math.rando
|
|||||||
|
|
||||||
async function enqueueTask(
|
async function enqueueTask(
|
||||||
db,
|
db,
|
||||||
{ type, corpId, orderId, rxId = "", baseTime = Date.now(), expiresAt }
|
{
|
||||||
|
type,
|
||||||
|
corpId,
|
||||||
|
orderId,
|
||||||
|
rxId = "",
|
||||||
|
baseTime = Date.now(),
|
||||||
|
expiresAt,
|
||||||
|
config: providedConfig,
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
if (!corpId || !orderId || !Object.values(AUTOMATION_TYPES).includes(type)) {
|
if (!corpId || !orderId || !Object.values(AUTOMATION_TYPES).includes(type)) {
|
||||||
return { success: false, message: "自动化任务参数错误" };
|
return { success: false, message: "自动化任务参数错误" };
|
||||||
@ -92,7 +113,7 @@ async function enqueueTask(
|
|||||||
return { success: false, message: "自动审方任务缺少处方ID" };
|
return { success: false, message: "自动审方任务缺少处方ID" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig(db, corpId);
|
const config = providedConfig || (await getConfig(db, corpId));
|
||||||
const stageConfig = getTypeConfig(config, type);
|
const stageConfig = getTypeConfig(config, type);
|
||||||
if (!stageConfig.enabled) {
|
if (!stageConfig.enabled) {
|
||||||
return { success: false, skipped: true, message: "自动化配置未开启" };
|
return { success: false, skipped: true, message: "自动化配置未开启" };
|
||||||
@ -129,6 +150,7 @@ async function enqueueTask(
|
|||||||
$unset: {
|
$unset: {
|
||||||
completedAt: "",
|
completedAt: "",
|
||||||
result: "",
|
result: "",
|
||||||
|
cancelReason: "",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -163,6 +185,111 @@ async function enqueueTask(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDrugCollectionName(order) {
|
||||||
|
return order && order.consultType === "onlineMedicinePurchase"
|
||||||
|
? "online-drug-info"
|
||||||
|
: "drug-info";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrderDrugIds(order) {
|
||||||
|
const drugs = order && Array.isArray(order.drugs) ? order.drugs : [];
|
||||||
|
return drugs
|
||||||
|
.map((item) => (item && ObjectId.isValid(item._id) ? new ObjectId(item._id) : null))
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findHighRiskDrug(db, order) {
|
||||||
|
const ids = getOrderDrugIds(order);
|
||||||
|
if (!ids.length) return null;
|
||||||
|
return db.collection(getDrugCollectionName(order)).findOne(
|
||||||
|
{ _id: { $in: ids }, riskLevel: "高风险" },
|
||||||
|
{ projection: { _id: 1, name: 1, riskLevel: 1 } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancelAutoOpenForHighRisk({
|
||||||
|
db,
|
||||||
|
orderId,
|
||||||
|
corpId,
|
||||||
|
doctorCode = "",
|
||||||
|
baseTime,
|
||||||
|
expiresAt,
|
||||||
|
}) {
|
||||||
|
const now = Date.now();
|
||||||
|
const taskKey = getTaskKey(AUTOMATION_TYPES.OPEN_RX, orderId, "");
|
||||||
|
const collection = db.collection(COLLECTION);
|
||||||
|
const cancelData = {
|
||||||
|
status: TASK_STATUS.CANCELLED,
|
||||||
|
leaseUntil: 0,
|
||||||
|
leaseOwner: "",
|
||||||
|
lastError: MANUAL_OPEN_RX_MESSAGE,
|
||||||
|
cancelReason: TASK_CANCEL_REASONS.HIGH_RISK_DRUG,
|
||||||
|
completedAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
const transitioned = await collection.updateOne(
|
||||||
|
{
|
||||||
|
taskKey,
|
||||||
|
$or: [
|
||||||
|
{ status: TASK_STATUS.PENDING },
|
||||||
|
{
|
||||||
|
status: TASK_STATUS.CANCELLED,
|
||||||
|
cancelReason: { $ne: TASK_CANCEL_REASONS.HIGH_RISK_DRUG },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ $set: cancelData }
|
||||||
|
);
|
||||||
|
|
||||||
|
let shouldNotify = transitioned.modifiedCount === 1;
|
||||||
|
if (!shouldNotify) {
|
||||||
|
const cancelledTask = {
|
||||||
|
taskKey,
|
||||||
|
type: AUTOMATION_TYPES.OPEN_RX,
|
||||||
|
corpId,
|
||||||
|
orderId,
|
||||||
|
rxId: "",
|
||||||
|
status: TASK_STATUS.CANCELLED,
|
||||||
|
dueAt: Number.isFinite(baseTime) ? baseTime : now,
|
||||||
|
nextRunAt: Number.isFinite(baseTime) ? baseTime : now,
|
||||||
|
expiresAt: Number.isFinite(expiresAt) ? expiresAt : now + 30 * 60 * 1000,
|
||||||
|
attempts: 0,
|
||||||
|
leaseUntil: 0,
|
||||||
|
leaseOwner: "",
|
||||||
|
lastError: MANUAL_OPEN_RX_MESSAGE,
|
||||||
|
cancelReason: TASK_CANCEL_REASONS.HIGH_RISK_DRUG,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
completedAt: now,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const inserted = await collection.updateOne(
|
||||||
|
{ taskKey },
|
||||||
|
{ $setOnInsert: cancelledTask },
|
||||||
|
{ upsert: true }
|
||||||
|
);
|
||||||
|
shouldNotify = inserted.upsertedCount === 1;
|
||||||
|
} catch (error) {
|
||||||
|
if (!error || error.code !== 11000) throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldNotify) {
|
||||||
|
await sendManualOpenNotification(
|
||||||
|
db,
|
||||||
|
{ type: AUTOMATION_TYPES.OPEN_RX, corpId, orderId },
|
||||||
|
doctorCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
skipped: true,
|
||||||
|
manualRequired: true,
|
||||||
|
message: MANUAL_OPEN_RX_MESSAGE,
|
||||||
|
taskKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function scheduleAutoAccept({ db, orderId, corpId, baseTime, expiresAt }) {
|
async function scheduleAutoAccept({ db, orderId, corpId, baseTime, expiresAt }) {
|
||||||
const eligible = await db.collection("consult-order").findOne(
|
const eligible = await db.collection("consult-order").findOne(
|
||||||
{ orderId, corpId, orderSource: "ALIPAY_MINI" },
|
{ orderId, corpId, orderSource: "ALIPAY_MINI" },
|
||||||
@ -183,17 +310,33 @@ async function scheduleAutoAccept({ db, orderId, corpId, baseTime, expiresAt })
|
|||||||
async function scheduleAutoOpenRx({ db, orderId, corpId, baseTime, expiresAt }) {
|
async function scheduleAutoOpenRx({ db, orderId, corpId, baseTime, expiresAt }) {
|
||||||
const eligible = await db.collection("consult-order").findOne(
|
const eligible = await db.collection("consult-order").findOne(
|
||||||
{ orderId, corpId, orderSource: "ALIPAY_MINI" },
|
{ orderId, corpId, orderSource: "ALIPAY_MINI" },
|
||||||
{ projection: { _id: 1 } }
|
{ projection: { _id: 1, drugs: 1, consultType: 1, doctorCode: 1 } }
|
||||||
);
|
);
|
||||||
if (!eligible) {
|
if (!eligible) {
|
||||||
return { success: false, skipped: true, message: "非支付宝小程序订单" };
|
return { success: false, skipped: true, message: "非支付宝小程序订单" };
|
||||||
}
|
}
|
||||||
|
const config = await getConfig(db, corpId);
|
||||||
|
if (!getTypeConfig(config, AUTOMATION_TYPES.OPEN_RX).enabled) {
|
||||||
|
return { success: false, skipped: true, message: "自动化配置未开启" };
|
||||||
|
}
|
||||||
|
const highRiskDrug = await findHighRiskDrug(db, eligible);
|
||||||
|
if (highRiskDrug) {
|
||||||
|
return cancelAutoOpenForHighRisk({
|
||||||
|
db,
|
||||||
|
orderId,
|
||||||
|
corpId,
|
||||||
|
doctorCode: eligible.doctorCode,
|
||||||
|
baseTime,
|
||||||
|
expiresAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
return enqueueTask(db, {
|
return enqueueTask(db, {
|
||||||
type: AUTOMATION_TYPES.OPEN_RX,
|
type: AUTOMATION_TYPES.OPEN_RX,
|
||||||
orderId,
|
orderId,
|
||||||
corpId,
|
corpId,
|
||||||
baseTime,
|
baseTime,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
|
config,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,7 +411,7 @@ async function markTask(db, task, status, extra = {}) {
|
|||||||
TASK_STATUS.CANCELLED,
|
TASK_STATUS.CANCELLED,
|
||||||
TASK_STATUS.FAILED,
|
TASK_STATUS.FAILED,
|
||||||
].includes(status);
|
].includes(status);
|
||||||
await db.collection(COLLECTION).updateOne(
|
return db.collection(COLLECTION).updateOne(
|
||||||
{
|
{
|
||||||
_id: task._id,
|
_id: task._id,
|
||||||
status: TASK_STATUS.RUNNING,
|
status: TASK_STATUS.RUNNING,
|
||||||
@ -354,6 +497,67 @@ function getFailureNotificationExt(error) {
|
|||||||
return { errorSource, reason };
|
return { errorSource, reason };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendManualOpenNotification(db, task, knownDoctorCode = "") {
|
||||||
|
let doctorCode = knownDoctorCode;
|
||||||
|
if (!doctorCode) {
|
||||||
|
try {
|
||||||
|
const order = await db.collection("consult-order").findOne(
|
||||||
|
{ orderId: task.orderId, corpId: task.corpId },
|
||||||
|
{ projection: { doctorCode: 1 } }
|
||||||
|
);
|
||||||
|
doctorCode = order && order.doctorCode ? order.doctorCode : "";
|
||||||
|
} catch (lookupError) {
|
||||||
|
console.error(
|
||||||
|
"[自动化任务] 查询人工开方提示接收人异常:",
|
||||||
|
lookupError.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doctorCode) {
|
||||||
|
console.error(
|
||||||
|
`[自动化任务] 未找到订单 ${task.orderId} 的医生,无法发送人工开方提示`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await tencentIM(
|
||||||
|
{
|
||||||
|
type: "sendSystemNotification",
|
||||||
|
corpId: task.corpId,
|
||||||
|
formAccount: task.orderId,
|
||||||
|
toAccount: doctorCode,
|
||||||
|
SyncOtherMachine: 1,
|
||||||
|
msgBody: [
|
||||||
|
{
|
||||||
|
MsgType: "TIMCustomElem",
|
||||||
|
MsgContent: {
|
||||||
|
Data: "AUTORXMANUAL",
|
||||||
|
Desc: "notification",
|
||||||
|
Ext: MANUAL_OPEN_RX_MESSAGE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
db
|
||||||
|
);
|
||||||
|
if (!result || !result.success) {
|
||||||
|
console.error(
|
||||||
|
`[自动化任务] 发送人工开方提示失败: ${result?.message || "未知错误"}`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (notificationError) {
|
||||||
|
console.error(
|
||||||
|
"[自动化任务] 发送人工开方提示异常:",
|
||||||
|
notificationError.message
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function sendFailureNotification(db, task, error) {
|
async function sendFailureNotification(db, task, error) {
|
||||||
let doctorCode = "";
|
let doctorCode = "";
|
||||||
|
|
||||||
@ -571,10 +775,16 @@ async function resolvePrescriptionDrugs(db, order) {
|
|||||||
order.consultType === "onlineMedicinePurchase" ? "online-drug-info" : "drug-info";
|
order.consultType === "onlineMedicinePurchase" ? "online-drug-info" : "drug-info";
|
||||||
const masterDrugs = await db
|
const masterDrugs = await db
|
||||||
.collection(collectionName)
|
.collection(collectionName)
|
||||||
.find({ _id: { $in: ids }, onSale: true })
|
.find({ _id: { $in: ids } })
|
||||||
.toArray();
|
.toArray();
|
||||||
if (masterDrugs.length !== orderDrugs.length) {
|
if (masterDrugs.length !== orderDrugs.length) {
|
||||||
throw new AutomationError("订单中存在已下架或不存在的药品");
|
throw new AutomationError("订单中存在不存在的药品");
|
||||||
|
}
|
||||||
|
if (masterDrugs.some((item) => item.riskLevel === "高风险")) {
|
||||||
|
throw new ManualPrescriptionRequiredError();
|
||||||
|
}
|
||||||
|
if (masterDrugs.some((item) => item.onSale !== true)) {
|
||||||
|
throw new AutomationError("订单中存在已下架的药品");
|
||||||
}
|
}
|
||||||
|
|
||||||
const medicineConfig = await getMedicineConfig(db, order.corpId);
|
const medicineConfig = await getMedicineConfig(db, order.corpId);
|
||||||
@ -884,7 +1094,15 @@ async function processClaimedTask(db, task) {
|
|||||||
: new AutomationError(error.message || "自动化任务执行失败", {
|
: new AutomationError(error.message || "自动化任务执行失败", {
|
||||||
retryable: true,
|
retryable: true,
|
||||||
});
|
});
|
||||||
if (isSingleAttemptTask(task.type)) {
|
if (automationError.cancelReason === TASK_CANCEL_REASONS.HIGH_RISK_DRUG) {
|
||||||
|
const marked = await markTask(db, task, TASK_STATUS.CANCELLED, {
|
||||||
|
lastError: automationError.message,
|
||||||
|
cancelReason: TASK_CANCEL_REASONS.HIGH_RISK_DRUG,
|
||||||
|
});
|
||||||
|
if (marked && (marked.modifiedCount === 1 || marked.matchedCount === 1)) {
|
||||||
|
await sendManualOpenNotification(db, task);
|
||||||
|
}
|
||||||
|
} else if (isSingleAttemptTask(task.type)) {
|
||||||
await markTask(db, task, TASK_STATUS.FAILED, {
|
await markTask(db, task, TASK_STATUS.FAILED, {
|
||||||
lastError: automationError.message,
|
lastError: automationError.message,
|
||||||
});
|
});
|
||||||
@ -1069,7 +1287,10 @@ module.exports = {
|
|||||||
TASK_STATUS,
|
TASK_STATUS,
|
||||||
AUTOMATION_TYPES,
|
AUTOMATION_TYPES,
|
||||||
ERROR_SOURCES,
|
ERROR_SOURCES,
|
||||||
|
TASK_CANCEL_REASONS,
|
||||||
|
MANUAL_OPEN_RX_MESSAGE,
|
||||||
AutomationError,
|
AutomationError,
|
||||||
|
ManualPrescriptionRequiredError,
|
||||||
getFailureNotificationExt,
|
getFailureNotificationExt,
|
||||||
calculateDueAt,
|
calculateDueAt,
|
||||||
enqueueTask,
|
enqueueTask,
|
||||||
|
|||||||
@ -7,6 +7,8 @@ const {
|
|||||||
AUTOMATION_TYPES,
|
AUTOMATION_TYPES,
|
||||||
COLLECTION,
|
COLLECTION,
|
||||||
ERROR_SOURCES,
|
ERROR_SOURCES,
|
||||||
|
MANUAL_OPEN_RX_MESSAGE,
|
||||||
|
TASK_CANCEL_REASONS,
|
||||||
TASK_STATUS,
|
TASK_STATUS,
|
||||||
AutomationError,
|
AutomationError,
|
||||||
buildAutoPrescriptionParams,
|
buildAutoPrescriptionParams,
|
||||||
@ -16,6 +18,7 @@ const {
|
|||||||
getFailureNotificationExt,
|
getFailureNotificationExt,
|
||||||
processClaimedTask,
|
processClaimedTask,
|
||||||
reconcileAutomationTasks,
|
reconcileAutomationTasks,
|
||||||
|
scheduleAutoOpenRx,
|
||||||
} = require("./index");
|
} = require("./index");
|
||||||
|
|
||||||
function createCollectionDb(collections) {
|
function createCollectionDb(collections) {
|
||||||
@ -126,11 +129,183 @@ describe("automation task scheduling", () => {
|
|||||||
},
|
},
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
$set: expect.objectContaining({ status: TASK_STATUS.PENDING }),
|
$set: expect.objectContaining({ status: TASK_STATUS.PENDING }),
|
||||||
$unset: { completedAt: "", result: "" },
|
$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 () => {
|
test("treats a concurrent unique-key insert as idempotent success", async () => {
|
||||||
const duplicateError = Object.assign(new Error("duplicate key"), {
|
const duplicateError = Object.assign(new Error("duplicate key"), {
|
||||||
code: 11000,
|
code: 11000,
|
||||||
@ -285,6 +460,91 @@ describe("automation task scheduling", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 () => {
|
test("fails automatic prescription immediately and sends the failure reason", async () => {
|
||||||
tencentIM.mockResolvedValue({ success: true });
|
tencentIM.mockResolvedValue({ success: true });
|
||||||
const taskUpdateOne = jest.fn().mockResolvedValue({ modifiedCount: 1 });
|
const taskUpdateOne = jest.fn().mockResolvedValue({ modifiedCount: 1 });
|
||||||
|
|||||||
@ -114,7 +114,8 @@ exports.formatUploadRxData = function (data) {
|
|||||||
sin_doscnt: i.dosage,
|
sin_doscnt: i.dosage,
|
||||||
sin_dosunt: i.dosage_unit,
|
sin_dosunt: i.dosage_unit,
|
||||||
unit: i.unit,
|
unit: i.unit,
|
||||||
appr_flag: i.forceSelfPay === 'Y' ? '2' : '1'
|
appr_flag: i.forceSelfPay === 'Y' ? '2' : '1',
|
||||||
|
dose_time_name: i.dose_time_name,
|
||||||
}
|
}
|
||||||
if (item.appr_flag === '2') {
|
if (item.appr_flag === '2') {
|
||||||
//[his要求]当appr_flag=2时必填,如果这个药品维护了限制使用范围的,传1,否则就传2
|
//[his要求]当appr_flag=2时必填,如果这个药品维护了限制使用范围的,传1,否则就传2
|
||||||
|
|||||||
@ -227,9 +227,9 @@ async function addConsultDiagnosis(item) {
|
|||||||
const chatCount = automated
|
const chatCount = automated
|
||||||
? 3
|
? 3
|
||||||
: await db.collection("im-chat-msg").countDocuments({
|
: await db.collection("im-chat-msg").countDocuments({
|
||||||
From_Account: doctorCode,
|
From_Account: doctorCode,
|
||||||
To_Account: orderId,
|
To_Account: orderId,
|
||||||
});
|
});
|
||||||
timeData.chatCountQueryTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
|
timeData.chatCountQueryTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||||
if (!automated && chatCount < 3) {
|
if (!automated && chatCount < 3) {
|
||||||
return {
|
return {
|
||||||
@ -1143,6 +1143,9 @@ async function auditDiagnosis(item, db) {
|
|||||||
async function uploadRx2His(rx, order) {
|
async function uploadRx2His(rx, order) {
|
||||||
try {
|
try {
|
||||||
const uploadData = formatUploadRxData(rx, order);
|
const uploadData = formatUploadRxData(rx, order);
|
||||||
|
console.log('='.repeat(10))
|
||||||
|
console.log('='.repeat(10))
|
||||||
|
console.log(JSON.stringify(uploadData))
|
||||||
const { success, message } = await hnHis({
|
const { success, message } = await hnHis({
|
||||||
type: 'hlwuploadprescription',
|
type: 'hlwuploadprescription',
|
||||||
data: uploadData
|
data: uploadData
|
||||||
|
|||||||
@ -5,6 +5,31 @@ const { getDrugList } = require('./verify');
|
|||||||
const { validateApplicableDiagnosisIds } = require('./applicable-diagnosis');
|
const { validateApplicableDiagnosisIds } = require('./applicable-diagnosis');
|
||||||
let db = "";
|
let db = "";
|
||||||
|
|
||||||
|
function getConfigOptionNames(list) {
|
||||||
|
return [...new Set((Array.isArray(list) ? list : [])
|
||||||
|
.map(item => typeof item === "string" ? item : item && item.name)
|
||||||
|
.filter(Boolean))];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getHlwDrugOptionLists(corpId) {
|
||||||
|
if (typeof corpId !== "string" || !corpId.trim()) return {};
|
||||||
|
const records = await db.collection("hlw-config").find(
|
||||||
|
{ group: `${corpId.trim()}-medicine-related` },
|
||||||
|
{ projection: { _id: 0, key: 1, list: 1 } }
|
||||||
|
).toArray();
|
||||||
|
const configMap = records.reduce((map, item) => {
|
||||||
|
if (item.key && Array.isArray(item.list)) map[item.key] = item.list;
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
const optionLists = {
|
||||||
|
unit: getConfigOptionNames(configMap["medicine-package-unit"]),
|
||||||
|
frequency: getConfigOptionNames(configMap["store-medicine-frequence"]),
|
||||||
|
administration_method: getConfigOptionNames(configMap["store-medicine-administration"]),
|
||||||
|
dose_time_name: getConfigOptionNames(configMap["medicine-use-time"]),
|
||||||
|
};
|
||||||
|
return Object.fromEntries(Object.entries(optionLists).filter(([, list]) => list.length));
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = async (item, mongodb) => {
|
module.exports = async (item, mongodb) => {
|
||||||
db = mongodb;
|
db = mongodb;
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
@ -217,6 +242,7 @@ async function addHlwDrugInfo(ctx) {
|
|||||||
return { success: false, message: "参数错误" }
|
return { success: false, message: "参数错误" }
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const optionLists = await getHlwDrugOptionLists(ctx.corpId);
|
||||||
const peopleInfo = {
|
const peopleInfo = {
|
||||||
creator: typeof ctx.operator === "string" ? ctx.operator : "",
|
creator: typeof ctx.operator === "string" ? ctx.operator : "",
|
||||||
creatorId: typeof ctx.operatorId === "string" ? ctx.operatorId : "",
|
creatorId: typeof ctx.operatorId === "string" ? ctx.operatorId : "",
|
||||||
@ -226,7 +252,7 @@ async function addHlwDrugInfo(ctx) {
|
|||||||
const { onSale, applicableDiagnosisIds, ...data } = params;
|
const { onSale, applicableDiagnosisIds, ...data } = params;
|
||||||
let newDrug = {};
|
let newDrug = {};
|
||||||
if (Object.keys(data).length) {
|
if (Object.keys(data).length) {
|
||||||
const [item] = getDrugList([data], true, peopleInfo);
|
const [item] = getDrugList([data], true, peopleInfo, 'hlw', optionLists);
|
||||||
const { msgs, ...drug } = item;
|
const { msgs, ...drug } = item;
|
||||||
if (msgs.length) {
|
if (msgs.length) {
|
||||||
return { success: false, message: msgs.join(",") }
|
return { success: false, message: msgs.join(",") }
|
||||||
@ -259,6 +285,7 @@ async function editHlwDrugInfo(ctx) {
|
|||||||
return { success: false, message: "参数错误" }
|
return { success: false, message: "参数错误" }
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const optionLists = await getHlwDrugOptionLists(ctx.corpId);
|
||||||
const diagnosisIds = await validateApplicableDiagnosisIds(params, db);
|
const diagnosisIds = await validateApplicableDiagnosisIds(params, db);
|
||||||
const { onSale, applicableDiagnosisIds, ...data } = params;
|
const { onSale, applicableDiagnosisIds, ...data } = params;
|
||||||
const currentDrug = await db.collection("drug-info").findOne(
|
const currentDrug = await db.collection("drug-info").findOne(
|
||||||
@ -282,7 +309,7 @@ async function editHlwDrugInfo(ctx) {
|
|||||||
updaterId: typeof ctx.operatorId === "string" ? ctx.operatorId : "",
|
updaterId: typeof ctx.operatorId === "string" ? ctx.operatorId : "",
|
||||||
updateTime: Date.now()
|
updateTime: Date.now()
|
||||||
}
|
}
|
||||||
const [item] = getDrugList([data], false, peopleInfo);
|
const [item] = getDrugList([data], false, peopleInfo, 'hlw', optionLists);
|
||||||
const { msgs, ...drug } = item;
|
const { msgs, ...drug } = item;
|
||||||
if (msgs.length) {
|
if (msgs.length) {
|
||||||
return { success: false, message: msgs.join(",") }
|
return { success: false, message: msgs.join(",") }
|
||||||
@ -332,12 +359,13 @@ async function importHlwDrugInfo(ctx) {
|
|||||||
return { success: false, message: "参数错误" }
|
return { success: false, message: "参数错误" }
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const optionLists = await getHlwDrugOptionLists(ctx.corpId);
|
||||||
const peopleInfo = {
|
const peopleInfo = {
|
||||||
creator: typeof ctx.operator === "string" ? ctx.operator : "",
|
creator: typeof ctx.operator === "string" ? ctx.operator : "",
|
||||||
creatorId: typeof ctx.operatorId === "string" ? ctx.operatorId : "",
|
creatorId: typeof ctx.operatorId === "string" ? ctx.operatorId : "",
|
||||||
createTime: Date.now(),
|
createTime: Date.now(),
|
||||||
}
|
}
|
||||||
const datalist = getDrugList(drugs, true, peopleInfo);
|
const datalist = getDrugList(drugs, true, peopleInfo, 'hlw', optionLists);
|
||||||
const createTime = dayjs().valueOf();
|
const createTime = dayjs().valueOf();
|
||||||
const list = datalist.filter(i => i.msgs.length == 0).map(i => {
|
const list = datalist.filter(i => i.msgs.length == 0).map(i => {
|
||||||
const { msgs, ...drug } = i;
|
const { msgs, ...drug } = i;
|
||||||
|
|||||||
@ -22,6 +22,7 @@ const drugFieldDict = [
|
|||||||
{ name: '通用名拼音码', field: 'generic_pinyin', verify: (val, label) => validator.verifyString(val, label) },
|
{ name: '通用名拼音码', field: 'generic_pinyin', verify: (val, label) => validator.verifyString(val, label) },
|
||||||
{ name: '抗生素级别', field: 'antibiotic_level', verify: (val, label) => validator.verifyListItem(val, [0, 1, 2, 3], false, label, 0) },
|
{ name: '抗生素级别', field: 'antibiotic_level', verify: (val, label) => validator.verifyListItem(val, [0, 1, 2, 3], false, label, 0) },
|
||||||
{ name: '肿瘤使用', field: 'tumor_use', verify: (val, label) => validator.verifyListItem(val, [0, 1, 2, 3], false, label, 0) },
|
{ name: '肿瘤使用', field: 'tumor_use', verify: (val, label) => validator.verifyListItem(val, [0, 1, 2, 3], false, label, 0) },
|
||||||
|
{ name: '风险等级', field: 'riskLevel', verify: (val, label) => validator.verifyListItem(val, ['低风险', '高风险'], false, label) },
|
||||||
// 仅在线药品库使用:管理分类(处方药 / OTC)与剂型
|
// 仅在线药品库使用:管理分类(处方药 / OTC)与剂型
|
||||||
{ name: '管理分类', field: 'manage_category', verify: (val, label) => validator.verifyString(val, label) },
|
{ name: '管理分类', field: 'manage_category', verify: (val, label) => validator.verifyString(val, label) },
|
||||||
{ name: '剂型', field: 'dosage_form', verify: (val, label) => validator.verifyString(val, label) },
|
{ name: '剂型', field: 'dosage_form', verify: (val, label) => validator.verifyString(val, label) },
|
||||||
@ -32,6 +33,7 @@ const drugFieldDict = [
|
|||||||
{ name: '给药标准方式', field: 'administration_method', verify: (val, label) => validator.verifyListItem(val, usageList.map(i => i.name), false, label) },
|
{ name: '给药标准方式', field: 'administration_method', verify: (val, label) => validator.verifyListItem(val, usageList.map(i => i.name), false, label) },
|
||||||
{ name: '用药标准天数', field: 'days', verify: (val, label) => validateNumber(val, label) },
|
{ name: '用药标准天数', field: 'days', verify: (val, label) => validateNumber(val, label) },
|
||||||
{ name: '发药标准数量', field: 'recommended_quantity', verify: (val, label) => validateNumber(val, label) },
|
{ name: '发药标准数量', field: 'recommended_quantity', verify: (val, label) => validateNumber(val, label) },
|
||||||
|
{ name: '用药时机', field: 'dose_time_name', required: true, libType: 'hlw', verify: (val, label) => validator.verifyString(val, label, 1) },
|
||||||
{ name: '用药证明', field: 'medication_proof_desc', verify: (val, label) => validator.verifyString(val, label, 0, 500) },
|
{ name: '用药证明', field: 'medication_proof_desc', verify: (val, label) => validator.verifyString(val, label, 0, 500) },
|
||||||
{ name: '使用限制说明', field: 'usage_restriction_desc', verify: (val, label) => validator.verifyString(val, label, 0, 500) },
|
{ name: '使用限制说明', field: 'usage_restriction_desc', verify: (val, label) => validator.verifyString(val, label, 0, 500) },
|
||||||
{ name: '适用性别', field: 'suitableGender', verify: (val, label) => validator.verifyListItem(val, ['common', 'male', 'female'], false, label) },
|
{ name: '适用性别', field: 'suitableGender', verify: (val, label) => validator.verifyListItem(val, ['common', 'male', 'female'], false, label) },
|
||||||
@ -50,7 +52,7 @@ function validateNumber(val, label, len = 0, required = false) {
|
|||||||
return validator.verifyNumber(val, label, len)
|
return validator.verifyNumber(val, label, len)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDrugList(arr, verifyEveryField = true, extraInfo = {}, libType = 'hlw') {
|
function getDrugList(arr, verifyEveryField = true, extraInfo = {}, libType = 'hlw', optionLists = {}) {
|
||||||
const fieldDict = drugFieldDict.filter(item => !item.libType || item.libType === libType);
|
const fieldDict = drugFieldDict.filter(item => !item.libType || item.libType === libType);
|
||||||
return arr.map(drug => {
|
return arr.map(drug => {
|
||||||
const res = fieldDict.reduce((obj, item) => {
|
const res = fieldDict.reduce((obj, item) => {
|
||||||
@ -59,7 +61,9 @@ function getDrugList(arr, verifyEveryField = true, extraInfo = {}, libType = 'hl
|
|||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
const val = typeof drug[key] === 'string' ? drug[key].trim() : drug[key];
|
const val = typeof drug[key] === 'string' ? drug[key].trim() : drug[key];
|
||||||
const [pass, res] = item.verify(val, item.name);
|
const [pass, res] = Array.isArray(optionLists[item.field])
|
||||||
|
? validator.verifyListItem(val, optionLists[item.field], Boolean(item.required), item.name)
|
||||||
|
: item.verify(val, item.name);
|
||||||
if (pass) {
|
if (pass) {
|
||||||
obj[item.field] = res;
|
obj[item.field] = res;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user