完善借款、编号、付款与预算并发及验收(09-06-release-gate-integrity)
汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。 统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。 纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。 关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。 预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。 完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
这个提交包含在:
@@ -1,4 +1,5 @@
|
||||
import { type APIResponse } from "@playwright/test";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
import { expect, test } from "./helpers/fixtures";
|
||||
import {
|
||||
@@ -55,6 +56,7 @@ type EmployeeSummary = {
|
||||
type ExpenseTypeListResponse = {
|
||||
items: Array<{
|
||||
id: string;
|
||||
code: string;
|
||||
enabled: boolean;
|
||||
name: string;
|
||||
}>;
|
||||
@@ -79,11 +81,7 @@ type SubmittedReimbursementSeed = {
|
||||
|
||||
type AuthedApi = Awaited<ReturnType<typeof createAuthenticatedApiClient>>["api"];
|
||||
|
||||
function formatDate(offsetDays: number) {
|
||||
const value = new Date();
|
||||
value.setUTCDate(value.getUTCDate() + offsetDays);
|
||||
return value.toISOString().slice(0, 10);
|
||||
}
|
||||
const requireServer = createRequire(new URL("../../../server/package.json", import.meta.url));
|
||||
|
||||
async function readApiData<T>(response: APIResponse) {
|
||||
const payload = (await response.json()) as { data?: T; error?: { code?: string; message?: string } };
|
||||
@@ -118,7 +116,7 @@ async function ensureFeatureFlags(api: Awaited<ReturnType<typeof createAuthentic
|
||||
}
|
||||
|
||||
async function createSubmittedReimbursement(api: AuthedApi, label: string): Promise<SubmittedReimbursementSeed> {
|
||||
const reimbursementDate = formatDate(30 + Math.floor(Math.random() * 45));
|
||||
const reimbursementDate = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit" }).format(new Date());
|
||||
const description = `Playwright 通知补盲 ${label}`;
|
||||
const created = await apiPostData<{ id: string }>(api, "/api/reimbursements", {
|
||||
reimbursementDate,
|
||||
@@ -126,25 +124,27 @@ async function createSubmittedReimbursement(api: AuthedApi, label: string): Prom
|
||||
});
|
||||
|
||||
const expenseTypes = await apiGetData<ExpenseTypeListResponse>(api, "/api/expense-types?enabled=true");
|
||||
const expenseTypeId = expenseTypes.items[0]?.id;
|
||||
const expenseTypeId = expenseTypes.items.find((item) => item.enabled && item.code === "office_postal")?.id;
|
||||
if (!expenseTypeId) {
|
||||
throw new Error("未找到可用报销费用类型");
|
||||
throw new Error("未找到合成通知用例必需的邮寄快递费用类型");
|
||||
}
|
||||
|
||||
await apiPostData(api, `/api/reimbursements/${created.id}/items`, {
|
||||
const item = await apiPostData<{ id: string }>(api, `/api/reimbursements/${created.id}/items`, {
|
||||
expenseTypeId,
|
||||
expenseDate: reimbursementDate,
|
||||
amount: "1683.00",
|
||||
amount: "10.00",
|
||||
description,
|
||||
});
|
||||
|
||||
const document = await requireServer("pdf-lib").PDFDocument.create();
|
||||
document.addPage().drawText(`Synthetic R75 notification ${label} ${Date.now()}`, { x: 40, y: 700 });
|
||||
const upload = await readApiData<InvoiceUploadResponse>(
|
||||
await api.post("/api/invoice-pool/upload", {
|
||||
multipart: {
|
||||
file: {
|
||||
name: `notify-${label}.pdf`,
|
||||
mimeType: "application/pdf",
|
||||
buffer: Buffer.from(`%PDF-1.4\nPW_R75_NOTIFY_${label}_${Date.now()}_${Math.random().toString(16).slice(2)}\n%%EOF\n`, "utf8"),
|
||||
buffer: Buffer.from(await document.save()),
|
||||
},
|
||||
ocr_type: "general",
|
||||
},
|
||||
@@ -166,9 +166,14 @@ async function createSubmittedReimbursement(api: AuthedApi, label: string): Prom
|
||||
|
||||
await apiPostData(api, `/api/invoice-pool/${upload.invoicePoolId}/bind`, {
|
||||
reimbursementId: created.id,
|
||||
linkedItemId: item.id,
|
||||
});
|
||||
|
||||
const submitted = await apiPostData<SubmittedReimbursementSeed>(api, `/api/reimbursements/${created.id}/submit`, {});
|
||||
const response = await api.post(`/api/reimbursements/${created.id}/submit`, { data: {} });
|
||||
const payload = await response.json() as { data?: SubmittedReimbursementSeed; error?: { code?: string; details?: { reasonCode?: string } } };
|
||||
expect(response.ok(), JSON.stringify({ status: response.status(), code: payload.error?.code, reasonCode: payload.error?.details?.reasonCode, fields: Object.keys(payload.error?.details ?? {}) })).toBe(true);
|
||||
expect(payload.data).toBeDefined();
|
||||
const submitted = payload.data!;
|
||||
return {
|
||||
reimbursementId: submitted.reimbursementId,
|
||||
formNumber: submitted.formNumber,
|
||||
@@ -198,7 +203,7 @@ async function fetchEmployeeByKeyword(
|
||||
const encoded = encodeURIComponent(keyword);
|
||||
const payload = await apiGetData<{ items: EmployeeSummary[] }>(
|
||||
api,
|
||||
`/api/employees?page=1&pageSize=20&q=${encoded}`,
|
||||
`/api/employees?page=1&pageSize=20&scope=management&q=${encoded}`,
|
||||
);
|
||||
return payload.items.find((item) => item.employeeNo === keyword || item.email === keyword) ?? null;
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户