恢复付款入口与常开基线(08-01-reimbursement-notification-progress-clarity)
这个提交包含在:
@@ -65,7 +65,7 @@ export function resolveMobileNotificationTargetLink(item: NotificationTarget) {
|
||||
}
|
||||
|
||||
if (item.targetType === "payment_batch") {
|
||||
return `/payment-batches?id=${encodeSegment(item.targetId)}`;
|
||||
return `/mobile/payment-batches?id=${encodeSegment(item.targetId)}`;
|
||||
}
|
||||
|
||||
if (item.targetType === "leave_request") {
|
||||
|
||||
@@ -118,7 +118,6 @@ export function resolveMobileEquivalentPath(pathname: string, search = "") {
|
||||
normalizedPath.startsWith("/projects") ||
|
||||
normalizedPath.startsWith("/contracts-registry") ||
|
||||
normalizedPath.startsWith("/travel-requests") ||
|
||||
normalizedPath.startsWith("/payment-batches") ||
|
||||
normalizedPath.startsWith("/ap-operations") ||
|
||||
normalizedPath.startsWith("/weekly-reports") ||
|
||||
normalizedPath.startsWith("/todos") ||
|
||||
@@ -130,6 +129,10 @@ export function resolveMobileEquivalentPath(pathname: string, search = "") {
|
||||
return "/mobile";
|
||||
}
|
||||
|
||||
if (normalizedPath === "/payment-batches") {
|
||||
return `/mobile/payment-batches${search}`;
|
||||
}
|
||||
|
||||
if (normalizedPath === "/purchase-requests") {
|
||||
const purchaseRequestId = searchParams.get("highlight") ?? searchParams.get("id") ?? searchParams.get("requestId");
|
||||
if (purchaseRequestId) {
|
||||
|
||||
@@ -184,7 +184,10 @@ export function MobilePortalPage() {
|
||||
const paymentBatchEnabled = Boolean(financeSummary?.paymentBatchEnabled);
|
||||
const financePendingCount = financeSummary?.pendingBatchCount ?? 0;
|
||||
const financeExceptionCount = (financeSummary?.failedItemCount ?? 0) + (financeSummary?.riskCount ?? 0);
|
||||
const shouldShowFinanceCard = canUseFinance && !portalHomeQuery.isLoading && !portalHomeQuery.isError && (financePendingCount > 0 || financeExceptionCount > 0);
|
||||
const shouldShowFinanceCard = canUseFinance &&
|
||||
!portalHomeQuery.isLoading &&
|
||||
!portalHomeQuery.isError &&
|
||||
(paymentBatchEnabled || financePendingCount > 0 || financeExceptionCount > 0);
|
||||
const financeCardBadge = paymentBatchEnabled ? "复核与付款" : financePendingCount > 0 ? "待付款" : "异常与复核";
|
||||
|
||||
async function openHomeNotification(item: NotificationItem) {
|
||||
@@ -326,9 +329,14 @@ export function MobilePortalPage() {
|
||||
</div>
|
||||
<div className="rounded-lg bg-zinc-50 px-3 py-2">
|
||||
<div className="text-xs text-zinc-500">处理</div>
|
||||
<div className="mt-1 text-xl font-semibold text-zinc-900">电脑端</div>
|
||||
<div className="mt-1 text-xl font-semibold text-zinc-900">OA</div>
|
||||
</div>
|
||||
</div>
|
||||
{paymentBatchEnabled ? (
|
||||
<Button asChild className="mt-3 w-full" size="sm" type="button" variant="secondary">
|
||||
<Link to="/mobile/payment-batches">进入付款</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -352,6 +352,15 @@ const routeElements = (
|
||||
|
||||
<Route path="mobile/notifications" element={withRouteFallback(<MobileNotificationsPage />)} />
|
||||
|
||||
<Route
|
||||
path="mobile/payment-batches"
|
||||
element={
|
||||
<RequirePermission anyOf={["payment.batch.manage", "risk.review.manage", "payment.batch.review"]}>
|
||||
{withRouteFallback(<PaymentBatchesPage />)}
|
||||
</RequirePermission>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="mobile/wecom-approval-archive"
|
||||
element={
|
||||
|
||||
@@ -24,6 +24,14 @@ type MockNotification = {
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
type FinanceSummaryOverride = {
|
||||
pendingBatchCount: number;
|
||||
failedItemCount: number;
|
||||
riskCount: number;
|
||||
paymentBatchEnabled: boolean;
|
||||
href: string;
|
||||
};
|
||||
|
||||
function envelope(data: unknown) {
|
||||
return { requestId: "mock-dg7-mobile-information-dedupe", data };
|
||||
}
|
||||
@@ -453,7 +461,12 @@ function reimbursementDetail() {
|
||||
};
|
||||
}
|
||||
|
||||
async function mockApis(page: Page, role: RolePreset, notifications: MockNotification[]) {
|
||||
async function mockApis(
|
||||
page: Page,
|
||||
role: RolePreset,
|
||||
notifications: MockNotification[],
|
||||
financeSummaryOverride?: FinanceSummaryOverride,
|
||||
) {
|
||||
await page.route((url) => url.pathname.startsWith("/api/"), async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
@@ -544,7 +557,7 @@ async function mockApis(page: Page, role: RolePreset, notifications: MockNotific
|
||||
isAdminOnly: role === "admin",
|
||||
},
|
||||
financeSummary: isFinance
|
||||
? {
|
||||
? financeSummaryOverride ?? {
|
||||
pendingBatchCount: 2,
|
||||
failedItemCount: 1,
|
||||
riskCount: 3,
|
||||
@@ -889,7 +902,7 @@ test.describe("MIF7 移动信息架构整改", () => {
|
||||
const target = { targetType: "payment_batch", targetId: "batch/dg7" };
|
||||
|
||||
expect(resolveNotificationTargetLink(target)).toBe("/payment-batches?id=batch%2Fdg7");
|
||||
expect(resolveMobileNotificationTargetLink(target)).toBe("/payment-batches?id=batch%2Fdg7");
|
||||
expect(resolveMobileNotificationTargetLink(target)).toBe("/mobile/payment-batches?id=batch%2Fdg7");
|
||||
});
|
||||
|
||||
test.skip(process.env.E2E_MODE === "live", "MIF7 本地专项使用 mock 数据验证信息架构。");
|
||||
@@ -1161,6 +1174,27 @@ test.describe("MIF7 移动信息架构整改", () => {
|
||||
await financeMobile.context.close();
|
||||
}
|
||||
|
||||
const firstUseFinanceMobile = await newMobilePage(browser, 390);
|
||||
try {
|
||||
await mockApis(firstUseFinanceMobile.page, "finance", buildNotifications(), {
|
||||
pendingBatchCount: 0,
|
||||
failedItemCount: 0,
|
||||
riskCount: 0,
|
||||
paymentBatchEnabled: true,
|
||||
href: "/payment-batches",
|
||||
});
|
||||
await loginForMobile(firstUseFinanceMobile.page, "finance");
|
||||
await firstUseFinanceMobile.page.goto("/mobile");
|
||||
const financeSummary = firstUseFinanceMobile.page.getByTestId("mobile-finance-summary");
|
||||
await expect(financeSummary).toBeVisible();
|
||||
await expect(financeSummary).toContainText("待付款");
|
||||
await expect(financeSummary.getByRole("link", { name: "进入付款" })).toHaveAttribute("href", "/mobile/payment-batches");
|
||||
await financeSummary.getByRole("link", { name: "进入付款" }).click();
|
||||
await expect(firstUseFinanceMobile.page).toHaveURL(/\/mobile\/payment-batches$/);
|
||||
} finally {
|
||||
await firstUseFinanceMobile.context.close();
|
||||
}
|
||||
|
||||
const adminMobile = await newMobilePage(browser, 390);
|
||||
try {
|
||||
await mockApis(adminMobile.page, "admin", buildNotifications());
|
||||
|
||||
@@ -19,7 +19,7 @@ type FeatureFlagItem = {
|
||||
|
||||
type FeatureFlagSnapshot = Pick<
|
||||
FeatureFlagItem,
|
||||
"flagKey" | "overrideMode" | "rolloutType" | "departmentIds" | "version" | "effectiveEnabled" | "effectiveReason"
|
||||
"flagKey" | "overrideMode" | "rolloutType" | "departmentIds" | "note" | "version" | "effectiveEnabled" | "effectiveReason"
|
||||
>;
|
||||
|
||||
type ReimbursementDetail = {
|
||||
@@ -99,7 +99,7 @@ const baseUrl = process.env.LIVE_SMOKE_BASE_URL?.trim() || "https://oa.hyinda.co
|
||||
const reportDir = process.env.P2_FIN_LIVE_REPORT_DIR?.trim() ||
|
||||
path.resolve(process.cwd(), "..", "artifacts/qa-flow/p2-fin-live-local");
|
||||
const summaryPath = path.join(reportDir, "live-result.json");
|
||||
const finalFlagState = process.env.P2_FIN_LIVE_FINAL_STATE?.trim() === "force_on" ? "force_on" : "force_off";
|
||||
const finalFlagState = process.env.P2_FIN_LIVE_FINAL_STATE?.trim() === "force_on" ? "force_on" : "restore";
|
||||
const alreadyOnMode =
|
||||
process.env.P2_FIN_LIVE_ALREADY_ON === "1" ||
|
||||
process.env.LAUNCH_AUTO_PAYMENT_BATCH_ALREADY_ON === "1";
|
||||
@@ -130,6 +130,7 @@ function snapshotFlag(item: FeatureFlagItem): FeatureFlagSnapshot {
|
||||
overrideMode: item.overrideMode,
|
||||
rolloutType: item.rolloutType,
|
||||
departmentIds: item.departmentIds,
|
||||
note: item.note,
|
||||
version: item.version,
|
||||
effectiveEnabled: item.effectiveEnabled,
|
||||
effectiveReason: item.effectiveReason,
|
||||
@@ -183,13 +184,15 @@ async function updateFlag(
|
||||
flagKey: string,
|
||||
overrideMode: FeatureFlagItem["overrideMode"],
|
||||
note: string,
|
||||
rolloutType: FeatureFlagItem["rolloutType"] = "global",
|
||||
departmentIds: string[] = [],
|
||||
) {
|
||||
const current = await loadFlag(api, flagKey);
|
||||
const response = await api.put(`/api/system/feature-flags/${flagKey}`, {
|
||||
data: {
|
||||
overrideMode,
|
||||
rolloutType: "global",
|
||||
departmentIds: [],
|
||||
rolloutType,
|
||||
departmentIds,
|
||||
note,
|
||||
expectedVersion: current.version,
|
||||
},
|
||||
@@ -198,12 +201,24 @@ async function updateFlag(
|
||||
return data.item;
|
||||
}
|
||||
|
||||
async function forcePaymentBatchOff(api: APIRequestContext, note: string) {
|
||||
async function restoreFlagSnapshot(api: APIRequestContext, snapshot: FeatureFlagSnapshot) {
|
||||
const current = await loadFlag(api, "payment_batch_v1");
|
||||
if (current.overrideMode === "force_off" && !current.effectiveEnabled) {
|
||||
const configurationMatches =
|
||||
current.overrideMode === snapshot.overrideMode &&
|
||||
current.rolloutType === snapshot.rolloutType &&
|
||||
JSON.stringify(current.departmentIds) === JSON.stringify(snapshot.departmentIds) &&
|
||||
current.note === snapshot.note;
|
||||
if (configurationMatches) {
|
||||
return current;
|
||||
}
|
||||
return updateFlag(api, "payment_batch_v1", "force_off", note);
|
||||
return updateFlag(
|
||||
api,
|
||||
"payment_batch_v1",
|
||||
snapshot.overrideMode,
|
||||
snapshot.note,
|
||||
snapshot.rolloutType,
|
||||
snapshot.departmentIds,
|
||||
);
|
||||
}
|
||||
|
||||
function resolveLivePdfBuffer(marker: string) {
|
||||
@@ -397,6 +412,7 @@ test.describe("公网付款批次受控验收", () => {
|
||||
|
||||
const clients: AuthenticatedApiClient[] = [];
|
||||
let admin: AuthenticatedApiClient | null = null;
|
||||
let paymentFlagBefore: FeatureFlagItem | null = null;
|
||||
let caughtError: unknown = null;
|
||||
let rollbackError: unknown = null;
|
||||
|
||||
@@ -408,7 +424,7 @@ test.describe("公网付款批次受控验收", () => {
|
||||
const financeMgr = await createLiveClientWithRetry("financeMgr");
|
||||
clients.push(admin, employee, manager, finance, financeMgr);
|
||||
|
||||
const paymentFlagBefore = await loadFlag(admin.api, "payment_batch_v1");
|
||||
paymentFlagBefore = await loadFlag(admin.api, "payment_batch_v1");
|
||||
const erpFlagBefore = await loadFlag(admin.api, "erp_sync");
|
||||
summary.stages.preflight = {
|
||||
paymentFlagBefore: snapshotFlag(paymentFlagBefore),
|
||||
@@ -587,19 +603,7 @@ test.describe("公网付款批次受控验收", () => {
|
||||
} finally {
|
||||
if (admin) {
|
||||
try {
|
||||
if (alreadyOnMode) {
|
||||
const finalPaymentFlag = await loadFlag(admin.api, "payment_batch_v1");
|
||||
const finalErpFlag = await loadFlag(admin.api, "erp_sync");
|
||||
summary.stages.finalState = {
|
||||
paymentFlag: snapshotFlag(finalPaymentFlag),
|
||||
erpSyncEffectiveEnabled: finalErpFlag.effectiveEnabled,
|
||||
alreadyOnMode,
|
||||
};
|
||||
expect(finalPaymentFlag.effectiveEnabled).toBe(true);
|
||||
expect(finalPaymentFlag.overrideMode).toBe("force_on");
|
||||
expect(finalFlagState).toBe("force_on");
|
||||
expect(finalErpFlag.effectiveEnabled).toBe(false);
|
||||
} else if (finalFlagState === "force_on" && !caughtError) {
|
||||
if (finalFlagState === "force_on" && !caughtError) {
|
||||
const paymentFlag = await loadFlag(admin.api, "payment_batch_v1");
|
||||
const finalPaymentFlag =
|
||||
paymentFlag.overrideMode === "force_on" && paymentFlag.effectiveEnabled
|
||||
@@ -609,14 +613,18 @@ test.describe("公网付款批次受控验收", () => {
|
||||
summary.stages.finalState = {
|
||||
paymentFlag: snapshotFlag(finalPaymentFlag),
|
||||
erpSyncEffectiveEnabled: finalErpFlag.effectiveEnabled,
|
||||
alreadyOnMode,
|
||||
};
|
||||
expect(finalPaymentFlag.effectiveEnabled).toBe(true);
|
||||
expect(finalPaymentFlag.overrideMode).toBe("force_on");
|
||||
expect(finalErpFlag.effectiveEnabled).toBe(false);
|
||||
} else {
|
||||
const rolledBack = await forcePaymentBatchOff(admin.api, `${issueId} rollback ${marker}`);
|
||||
summary.stages.rollback = { paymentFlagAfterRollback: snapshotFlag(rolledBack) };
|
||||
expect(rolledBack.effectiveEnabled).toBe(false);
|
||||
} else if (paymentFlagBefore) {
|
||||
const restored = await restoreFlagSnapshot(admin.api, snapshotFlag(paymentFlagBefore));
|
||||
summary.stages.rollback = { paymentFlagAfterRestore: snapshotFlag(restored) };
|
||||
expect(restored.overrideMode).toBe(paymentFlagBefore.overrideMode);
|
||||
expect(restored.rolloutType).toBe(paymentFlagBefore.rolloutType);
|
||||
expect(restored.departmentIds).toEqual(paymentFlagBefore.departmentIds);
|
||||
expect(restored.note).toBe(paymentFlagBefore.note);
|
||||
}
|
||||
} catch (error) {
|
||||
rollbackError = error;
|
||||
|
||||
在新工单中引用
屏蔽一个用户