H1-H10 优化OCR实时体验闭环
这个提交包含在:
@@ -0,0 +1,52 @@
|
||||
const Fastify = require("fastify");
|
||||
|
||||
const eventsRoutes = require("../../../src/routes/events");
|
||||
const { createAppEventBus } = require("../../../src/services/events");
|
||||
const { AppError } = require("../../../src/utils/AppError");
|
||||
|
||||
async function buildApp() {
|
||||
const app = Fastify();
|
||||
app.decorate("appEvents", createAppEventBus());
|
||||
app.decorate("authenticate", async (request) => {
|
||||
const auth = request.headers.authorization ?? "";
|
||||
if (!auth.startsWith("Bearer ")) {
|
||||
throw new AppError({ code: "AUTH_001", message: "未登录", statusCode: 401 });
|
||||
}
|
||||
request.employee = { id: "employee-1", permissionCodes: [] };
|
||||
});
|
||||
app.decorate("hasPermission", (employee, permissionCode) =>
|
||||
(employee?.permissionCodes ?? []).includes(permissionCode),
|
||||
);
|
||||
app.setErrorHandler((error, request, reply) => {
|
||||
if (error instanceof AppError) {
|
||||
return reply.status(error.statusCode).send({
|
||||
error: {
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
details: error.details ?? {},
|
||||
},
|
||||
});
|
||||
}
|
||||
return reply.status(500).send({ error: { code: "SYS_001", message: "服务异常" } });
|
||||
});
|
||||
await app.register(eventsRoutes, { prefix: "/api" });
|
||||
return app;
|
||||
}
|
||||
|
||||
describe("GET /api/events", () => {
|
||||
it("requires bearer authentication before opening the event stream", async () => {
|
||||
const app = await buildApp();
|
||||
|
||||
try {
|
||||
const response = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/events",
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(401);
|
||||
expect(response.json().error.code).toBe("AUTH_001");
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -38,6 +38,8 @@ function buildMultipartRequest({ filename, contentType, content, fields = [] })
|
||||
|
||||
async function buildApp({
|
||||
ocrProvider = "off",
|
||||
jobDriver = "cron",
|
||||
bossSend = null,
|
||||
invoiceConflicts = [],
|
||||
statusRows = [],
|
||||
permissionCodes = ["invoice.upload"],
|
||||
@@ -75,7 +77,11 @@ async function buildApp({
|
||||
maxUploadSizeBytes: 5 * 1024 * 1024,
|
||||
invoiceArchiveUnusedDays: 30,
|
||||
ocrProvider,
|
||||
jobDriver,
|
||||
});
|
||||
if (bossSend) {
|
||||
app.decorate("boss", { send: bossSend });
|
||||
}
|
||||
app.decorate("authenticate", async (request) => {
|
||||
if (!request.headers.authorization) {
|
||||
throw new AppError({ code: "AUTH_001", message: "未登录", statusCode: 401 });
|
||||
@@ -177,8 +183,13 @@ describe("invoicePool upload route", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("puts VAT uploads into pending_ocr when OCR provider is enabled", async () => {
|
||||
const { app, prisma, tempDir } = await buildApp({ ocrProvider: "mock" });
|
||||
it("puts VAT uploads into pending_ocr and enqueues a directed OCR job when OCR provider is enabled", async () => {
|
||||
const bossSend = vi.fn(async () => ({ id: "ocr-job-1" }));
|
||||
const { app, prisma, tempDir } = await buildApp({
|
||||
ocrProvider: "mock",
|
||||
jobDriver: "pgboss",
|
||||
bossSend,
|
||||
});
|
||||
|
||||
try {
|
||||
const requestPayload = buildMultipartRequest({
|
||||
@@ -214,6 +225,9 @@ describe("invoicePool upload route", () => {
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(bossSend).toHaveBeenCalledWith("ocr.invoice", {
|
||||
invoiceId: "11111111-1111-4111-8111-111111111111",
|
||||
});
|
||||
} finally {
|
||||
await app.close();
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
|
||||
@@ -133,6 +133,7 @@ async function buildApp(options = {}) {
|
||||
],
|
||||
travelRequests: (options.travelRequests ?? [buildTravelRequest()]).map((item) => cloneTravelRequest(item)),
|
||||
auditLogs: [],
|
||||
sequenceNumber: 0,
|
||||
};
|
||||
|
||||
function buildReimbursementSelect(record, select) {
|
||||
@@ -236,6 +237,28 @@ async function buildApp(options = {}) {
|
||||
return found ? cloneTravelRequest(found) : null;
|
||||
}),
|
||||
},
|
||||
documentSequence: {
|
||||
findUnique: vi.fn(async () => null),
|
||||
upsert: vi.fn(async ({ create, update }) => {
|
||||
stateRef.sequenceNumber = (stateRef.sequenceNumber ?? 0) + 1;
|
||||
return {
|
||||
document_type: create.document_type,
|
||||
prefix: update.prefix ?? create.prefix,
|
||||
date_format: update.date_format ?? create.date_format,
|
||||
separator: update.separator ?? create.separator,
|
||||
number_length: update.number_length ?? create.number_length,
|
||||
reset_period: update.reset_period ?? create.reset_period,
|
||||
current_number: stateRef.sequenceNumber,
|
||||
last_reset_date: null,
|
||||
updated_at: new Date("2026-04-08T12:00:00.000Z"),
|
||||
updated_by: update.updated_by ?? create.updated_by ?? null,
|
||||
};
|
||||
}),
|
||||
},
|
||||
documentCategory: {
|
||||
findUnique: vi.fn(async () => null),
|
||||
findFirst: vi.fn(async () => null),
|
||||
},
|
||||
reimbursementForm: {
|
||||
create: vi.fn(async ({ data, select }) => {
|
||||
const record = {
|
||||
@@ -317,6 +340,7 @@ async function buildApp(options = {}) {
|
||||
expenseTypes: state.expenseTypes.map((item) => ({ ...item })),
|
||||
travelRequests: state.travelRequests.map((item) => cloneTravelRequest(item)),
|
||||
auditLogs: state.auditLogs.map((item) => ({ ...item })),
|
||||
sequenceNumber: state.sequenceNumber,
|
||||
};
|
||||
const tx = buildPrismaLike(txState);
|
||||
const result = await callback(tx);
|
||||
@@ -327,6 +351,7 @@ async function buildApp(options = {}) {
|
||||
state.expenseTypes = txState.expenseTypes;
|
||||
state.travelRequests = txState.travelRequests;
|
||||
state.auditLogs = txState.auditLogs;
|
||||
state.sequenceNumber = txState.sequenceNumber;
|
||||
return result;
|
||||
});
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ function buildReimbursementRecord(overrides = {}) {
|
||||
},
|
||||
},
|
||||
],
|
||||
attachments: [],
|
||||
invoice_pool_items: [
|
||||
{
|
||||
id: "invoice-1",
|
||||
|
||||
在新工单中引用
屏蔽一个用户